隨著社交網路使用者規模的擴大,使用單一 Redis 執行個體難以滿足效能需求。本文介紹如何運用 Redis 分片技術,搭配 Lua 指令碼,最佳化使用者關注、時間線等核心功能的查詢效率。首先,我們建立分片連線物件,根據鍵值分配到對應的分片,以分散資料儲存壓力。接著,改寫使用者關注功能,使用分片連線處理關注者和被關注者列表,並利用 Redis 事務確保操作原子性。針對關注者/被關注者列表的規模擴充套件,我們設計了 KeyDataShardedConnection 類別,根據使用者 ID 分配分片,解決潛在效能瓶頸。此外,我們也探討了分片 ZRANGEBYSCORE 操作的實作方法,以支援跨分片查詢。最後,引入 Lua 指令碼,將同步狀態更新和鎖機制等操作移至伺服器端執行,減少網路往返次數,進一步提升效能。
縮放複雜查詢:使用分片技術最佳化社交網路
在前面的章節中,我們已經討論瞭如何使用Redis來實作一個基本的社交網路,包括使用者關注、時間線等功能。然而,隨著使用者數量的增長,我們需要對系統進行擴充套件以滿足更高的效能需求。本章節將重點介紹如何使用分片技術來最佳化社交網路中的複雜查詢。
使用分片連線處理時間線
首先,我們需要一個能夠處理分片連線的物件。這個物件將根據給定的鍵值傳回對應的分片連線。下面是實作這個功能的類別:
class KeyShardedConnection(object):
def __init__(self, component, shards):
self.component = component
self.shards = shards
def __getitem__(self, key):
return get_sharded_connection(
self.component, key, self.shards)
內容解密:
__init__方法初始化物件,接受兩個引數:component和shards。component表示元件名稱,而shards表示分片的數量。__getitem__方法是Python中的特殊方法,當物件被當作字典使用時會被呼叫。它根據提供的鍵值傳回對應的分片連線。get_sharded_connection函式根據元件名稱、鍵值和分片數量傳回對應的分片連線。
更新使用者關注功能
接下來,我們需要更新使用者關注功能的實作,以使用分片連線。下面是更新後的 follow_user 函式:
sharded_timelines = KeyShardedConnection('timelines', 8)
sharded_followers = KeyDataShardedConnection('followers', 16)
def follow_user(conn, uid, other_uid):
fkey1 = 'following:%s' % uid
fkey2 = 'followers:%s' % other_uid
if conn.zscore(fkey1, other_uid):
print "already followed", uid, other_uid
return None
now = time.time()
pipeline = conn.pipeline(True)
pipeline.zadd(fkey1, other_uid, now)
pipeline.zadd(fkey2, uid, now)
pipeline.zcard(fkey1)
pipeline.zcard(fkey2)
following, followers = pipeline.execute()[-2:]
pipeline.hset('user:%s' % uid, 'following', following)
pipeline.hset('user:%s' % other_uid, 'followers', followers)
pipeline.execute()
pkey = 'profile:%s' % other_uid
status_and_score = sharded_timelines[pkey].zrevrange(
pkey, 0, HOME_TIMELINE_SIZE - 1, withscores=True)
if status_and_score:
hkey = 'home:%s' % uid
pipe = sharded_timelines[hkey].pipeline(True)
pipe.zadd(hkey, **dict(status_and_score))
pipe.zremrangebyrank(hkey, 0, -HOME_TIMELINE_SIZE - 1)
pipe.execute()
return True
內容解密:
- 首先,我們建立了兩個
KeyShardedConnection物件:sharded_timelines和sharded_followers,分別用於處理時間線和關注者/被關注者的分片連線。 follow_user函式首先檢查使用者是否已經關注了目標使用者,如果已經關注則直接傳回。- 使用 Redis 的事務(pipeline)來執行多個操作,包括更新關注列表和關注者列表。
- 更新使用者的關注數量和被關注數量。
- 將目標使用者的最新狀態更新到當前使用者的時間線中。
- 使用
sharded_timelines物件來取得對應的分片連線,並執行相關操作。
縮放關注者/被關注者列表
對於關注者/被關注者列表,由於某些使用者可能會有非常多的關注者或被關注者,我們需要對這些列表進行分片處理。下面是實作這個功能的類別:
class KeyDataShardedConnection(object):
# 實作細節省略
pass
內容解密:
- 這個類別用於處理關注者/被關注者列表的分片連線。
- 它將根據使用者ID和對方的ID來確定使用哪個分片連線,以確保雙方的關係資料儲存在同一個分片上。
練習:更新列表時間線支援分片
請嘗試更新第8章中的列表時間線支援任務,以支援分片的關注者列表。你能否保持其效能與原始版本幾乎相同?提示:如果遇到困難,我們在列表10.15中提供了一個完全更新的版本。
隨著社交網路的進一步發展,我們可能需要考慮更多的最佳化方案,例如進一步最佳化查詢效能、增加資料的冗餘性等。同時,也需要考慮如何更好地管理和維護分片資料,以確保系統的穩定性和可靠性。
擴充套件複雜查詢的實作方法
在處理大規模的社交網路應用程式時,擴充套件複雜查詢是一項重要的任務。本章節將探討如何在 Redis 中實作複雜查詢的擴充套件,特別是在處理粉絲和關注者列表的情境下。
實作分片連線
首先,我們需要一個能夠處理分片連線的機制。以下是一個實作範例:
class KeyDataShardedConnection(object):
def __init__(self, component, shards):
self.component = component
self.shards = shards
def __getitem__(self, ids):
id1, id2 = map(int, ids)
if id2 < id1:
id1, id2 = id2, id1
key = "%s:%s"%(id1, id2)
return get_sharded_connection(
self.component, key, self.shards)
內容解密:
- 這個類別用於建立分片連線,初始化時需要提供元件名稱和分片數量。
- 當使用字典查詢方式存取物件時,會呼叫
__getitem__方法,並傳入一對 ID。 - 這對 ID 會被轉換為整數,並根據大小順序進行排序,以確保每次查詢的結果都是一致的。
- 使用排序後的 ID 組成一個鍵值,並利用這個鍵值取得對應的分片連線。
處理關注和粉絲列表
在處理關注和粉絲列表時,我們需要更新相關的 ZSET 操作。以下是更新後的程式碼:
sconn = sharded_followers[uid, other_uid]
if sconn.zscore(fkey1, other_uid):
return None
now = time.time()
spipe = sconn.pipeline(True)
spipe.zadd(fkey1, other_uid, now)
spipe.zadd(fkey2, uid, now)
following, followers = spipe.execute()
pipeline = conn.pipeline(True)
pipeline.hincrby('user:%s'%uid, 'following', int(following))
pipeline.hincrby('user:%s'%other_uid, 'followers', int(followers))
pipeline.execute()
內容解密:
- 首先,檢查
other_uid是否已經存在於fkey1中,如果存在則傳回None。 - 使用目前時間戳記將
other_uid新增到fkey1,並將uid新增到fkey2。 - 更新
uid的關注數量和other_uid的粉絲數量。 - 使用 pipeline 來確保操作的原子性。
實作分片 ZRANGEBYSCORE
為了支援分片 ZSET 的 ZRANGEBYSCORE 操作,我們需要實作一個函式來處理這個查詢:
def sharded_zrangebyscore(component, shards, key, min, max, num):
data = []
for shard in xrange(shards):
conn = get_redis_connection("%s:%s"%(component, shard))
data.extend(conn.zrangebyscore(
key, min, max, start=0, num=num, withscores=True))
def key(pair):
return pair[1], pair[0]
data.sort(key=key)
return data[:num]
內容解密:
- 這個函式接受元件名稱、分片數量、鍵值、最小值、最大值和數量等引數。
- 它會遍歷所有分片,並從每個分片中取得符合條件的資料。
- 將所有取得的資料合併,並根據分數和成員進行排序。
- 傳回排序後的資料中前
num個專案。
圖表翻譯:
graph LR
A[開始] --> B{檢查是否已關注}
B -->|是| C[傳回 None]
B -->|否| D[新增關注/粉絲資訊]
D --> E[更新使用者統計資料]
E --> F[取得其他使用者的時間軸資料]
F --> G[更新首頁時間軸]
圖表翻譯:
此圖表展示了處理關注和粉絲列表的流程。首先檢查是否已關注,如果已關注則傳回 None。如果未關注,則新增關注和粉絲資訊,並更新使用者的統計資料。接著,取得其他使用者的時間軸資料,並更新首頁時間軸。
隨著社交網路應用程式的不斷發展,未來我們可能會遇到更多複雜的查詢需求。因此,繼續改進和最佳化分片機制和查詢操作將是未來的重要工作。此外,如何在保證效能的同時,進一步提高資料的一致性和完整性,也將是未來研究的重要方向。
使用Lua指令碼最佳化Redis操作
隨著Redis 2.6版本的推出,Redis引入了伺服器端指令碼功能,支援使用Lua程式語言進行操作。這使得我們能夠在Redis內部執行多種操作,從而簡化程式碼並提升效能。本章將探討使用Lua指令碼的優勢,並透過實際案例展示如何使用Lua改寫之前的解決方案。
為何使用Lua指令碼
在客戶端執行操作時,通常需要多次與Redis進行互動,這不僅增加了網路延遲,也可能導致操作的非原子性。Lua指令碼允許我們在Redis伺服器端執行多條命令,這樣可以減少網路往返次數,並且確保操作的原子性。
Lua指令碼的優勢
- 原子性操作:Lua指令碼在執行時不會被其他命令打斷,確保了操作的原子性。
- 效能提升:減少了客戶端與伺服器之間的網路往返次數,提升了操作效率。
- 簡化程式碼:將複雜的操作封裝在Lua指令碼中,可以簡化客戶端的程式碼邏輯。
使用Lua改寫社交網路功能
在第8章中,我們實作了一個社交網路的功能,包括發布狀態更新和將狀態更新同步到關注者的時間軸。現在,我們可以使用Lua指令碼來最佳化這個過程。
原始的同步狀態更新函式
def syndicate_status(uid, post, start=0, on_lists=False):
root = 'followers'
key = 'followers:%s' % uid
base = 'home:%s'
if on_lists:
root = 'list:out'
key = 'list:out:%s' % uid
base = 'list:statuses:%s'
followers = sharded_zrangebyscore(root, sharded_followers.shards, key, start, 'inf', POSTS_PER_PASS)
to_send = defaultdict(list)
for follower, start in followers:
timeline = base % follower
shard = shard_key('timelines', timeline, sharded_timelines.shards, 2)
to_send[shard].append(timeline)
for timelines in to_send.values():
pipe = sharded_timelines[timelines[0]].pipeline(False)
for timeline in timelines:
pipe.zadd(timeline, **post)
pipe.zremrangebyrank(timeline, 0, -HOME_TIMELINE_SIZE-1)
pipe.execute()
conn = redis.Redis()
if len(followers) >= POSTS_PER_PASS:
execute_later(conn, 'default', 'syndicate_status', [uid, post, start, on_lists])
elif not on_lists:
execute_later(conn, 'default', 'syndicate_status', [uid, post, 0, True])
使用Lua指令碼最佳化同步操作
我們可以將同步狀態更新到時間軸的操作封裝在一個Lua指令碼中,這樣可以確保操作的原子性並提升效能。
local timeline = KEYS[1]
local post = ARGV[1]
redis.call('ZADD', timeline, post['score'], post['value'])
redis.call('ZREMRANGEBYRANK', timeline, 0, -HOME_TIMELINE_SIZE-1)
def syndicate_status(uid, post, start=0, on_lists=False):
root = 'followers'
key = 'followers:%s' % uid
base = 'home:%s'
if on_lists:
root = 'list:out'
key = 'list:out:%s' % uid
base = 'list:statuses:%s'
followers = sharded_zrangebyscore(root, sharded_followers.shards, key, start, 'inf', POSTS_PER_PASS)
to_send = defaultdict(list)
for follower, start in followers:
timeline = base % follower
shard = shard_key('timelines', timeline, sharded_timelines.shards, 2)
to_send[shard].append(timeline)
lua_script = """
local timeline = KEYS[1]
local post = cjson.decode(ARGV[1])
redis.call('ZADD', timeline, post['score'], post['value'])
redis.call('ZREMRANGEBYRANK', timeline, 0, -HOME_TIMELINE_SIZE-1)
"""
for timelines in to_send.values():
pipe = sharded_timelines[timelines[0]].pipeline(False)
for timeline in timelines:
pipe.eval(lua_script, 1, timeline, cjson.encode(post))
pipe.execute()
conn = redis.Redis()
if len(followers) >= POSTS_PER_PASS:
execute_later(conn, 'default', 'syndicate_status', [uid, post, start, on_lists])
elif not on_lists:
execute_later(conn, 'default', 'syndicate_status', [uid, post, 0, True])
內容解密:
上述Python程式碼利用了Redis的Lua指令碼功能,將狀態更新同步到關注者時間軸的操作原子化。首先,我們定義了一個Lua指令碼,該指令碼接受一個時間軸鍵(timeline)和一個狀態更新(post)作為引數,使用ZADD命令將狀態更新新增到時間軸,並使用ZREMRANGEBYRANK命令移除過舊的狀態更新。然後,在Python程式碼中,我們使用sharded_zrangebyscore函式取得關注者的ID,根據這些ID構建時間軸鍵,並將這些鍵分組到不同的分片中。接著,我們使用Redis的eval命令執行Lua指令碼,將狀態更新同步到每個時間軸中。最後,我們根據取得的關注者數量決定是否需要進一步處理。
使用Lua改寫鎖和訊號量
在第6章中,我們實作了鎖和訊號量來控制對分享資源的存取。現在,我們可以使用Lua指令碼來改寫這些功能,以提升效能和公平性。
原始的鎖實作
def acquire_lock(conn, lock_name, acquire_timeout=10):
# ...
while timestamp < time.time():
# ...
if conn.setnx(lock_name, timestamp):
return timestamp
# ...
def release_lock(conn, lock_name, identifier):
pipe = conn.pipeline(True)
while True:
try:
pipe.watch(lock_name)
if pipe.get(lock_name) == identifier:
pipe.multi()
pipe.delete(lock_name)
pipe.execute()
return True
pipe.unwatch()
break
except redis.exceptions.WatchError:
pass
return False
使用Lua指令碼最佳化鎖的釋放
local lock_name = KEYS[1]
local identifier = ARGV[1]
if redis.call('GET', lock_name) == identifier then
return redis.call('DEL', lock_name)
end
return 0
def release_lock(conn, lock_name, identifier):
lua_script = """
local lock_name = KEYS[1]
local identifier = ARGV[1]
if redis.call('GET', lock_name) == identifier then
return redis.call('DEL', lock_name)
end
return 0
"""
return conn.eval(lua_script, 1, lock_name, identifier)
內容解密:
在改寫鎖的釋放操作時,我們定義了一個Lua指令碼,該指令碼檢查鎖的值是否與給定的識別符號(identifier)匹配,如果匹配,則刪除鎖。這樣可以確保只有持有鎖的客戶端才能釋放鎖。Lua指令碼的使用確保了檢查和刪除操作的原子性,避免了競爭條件。
隨著Redis的不斷發展,Lua指令碼的功能將會更加強大和靈活。未來,我們可以期待更多根據Lua指令碼的創新應用和最佳實踐。