自動補全
程式說明
實現給一個組別中的成員發郵件時,輸入字首能找出匹配的收件人;程式假設所有名字是由字母組成。
設計思路
比如,查詢abc字首的名稱:
- → 查詢範圍是(abbz, abd)
- → zrange取出該範圍
- → 但問題是這兩個元素的可能不存在
- → 向有序集合插入兩個特殊元素,一個在abbz之後,一個在abd之前
- → 根據這兩個特殊元素取排名→根據排名取範圍
- → 已知:ASCII編碼裡面,z後面的第一個字元是 '{', a前面的第一個是反引號 ' ` '
- →得到 abd 之前的且所有以 abc 為字首的合法名字之後的元素「 abc{ 」 →作為查詢的結束元素
- →得到 abbz 之後的第一個元素是 abb{,這個元素位置 abc 之前→用作起始元素
- →特殊情況:如果是查詢的字首是aba,則起始元素是ab`
- → 由此得到查詢的範圍,即開始元素是字首的最後一個字元後退一個ASCII字元 + ' { '(如果是a則變為' ` '), 結束元素是字首直接加' { '
程式碼實現
import uuid
import bisect
import redis
conn = redis.Redis(host='127.0.0.1', port=6379)
valid_characters = '`abcdefghijklmnopqrstuvwxyz{'
# 獲得起始和結束範圍
def find_prefix_range(prefix):
posn = bisect.bisect_left(valid_characters, prefix[-1:])
suffix = valid_characters[(posn or 1) - 1]
return prefix[:-1] + suffix + '{', prefix + '{'
# 自動補全程式
def autocomplete_on_prefix(conn, guild, prefix):
# 獲取起始和結束字元
start, end = find_prefix_range(prefix)
identifier = str(uuid.uuid4())
# 將開始和結束加上一個唯一識別符號,避免其他使用者同時操作時相互干擾
start += identifier
end += identifier
# 要查詢的組
zset_name = 'members:' + guild
# 向有序集合中插入起始和結束字元
conn.zadd(zset_name, {start:0, end:0})
pipeline = conn.pipeline(True)
while 1:
try:
pipeline.watch(zset_name)
# 獲得起始和結束元素的位置
sindex = pipeline.zrank(zset_name, start)
eindex = pipeline.zrank(zset_name, end)
# 這裡用於讓取出的元素不超過10個
erange = min(sindex + 9, eindex - 2)
pipeline.multi()
# 移除插入的兩個元素
pipeline.zrem(zset_name, start, end)
# 獲得搜尋結果
pipeline.zrange(zset_name, sindex, erange)
items = pipeline.execute()[-1]
break
except redis.exceptions.WatchError:
continue
# 移除可能由其他客戶端插入的用於搜尋的元素(含有{)
return [item.decode('utf-8') for item in items if b'{' not in item]
# 加入小組
def join_guild(conn, guild, user):
conn.zadd('members:' + guild, {user:0})
# 退出小組
def leave_guild(conn, guild, user):
conn.zrem('members:' + guild, user)
# 測試執行
join_guild(conn, 'aa', 'abc')
join_guild(conn, 'aa', 'abcd')
join_guild(conn, 'aa', 'abcz')
join_guild(conn, 'aa', 'abcx')
join_guild(conn, 'aa', 'abcy')
join_guild(conn, 'aa', 'abci')
join_guild(conn, 'aa', 'abcj')
join_guild(conn, 'aa', 'abd')
items = autocomplete_on_prefix(conn, 'aa', 'abc')
print(items)
# 輸出: ['abc', 'abcd', 'abci', 'abcj', 'abcx', 'abcy', 'abcz']
有序集合常規用途
- 快速判斷某個元素是否存在於集合裡面(ZSCORE key member,不存在這返回nil)
- 檢視某個成員在有序集合中的位置或索引(ZRANK key-name member)
- 取出某個範圍的元素(ZRANGE key-name start offset [WITHSCORES])
這裡的用法
所有score設為0,當所有成員分值都相同時,按照成員的名字來進行排序; 而當所有成員分值都是0的時候,成員按照字串的二進位制進行排序。
分散式鎖
實現一個分散式鎖,核心的命令是:setnx(SET if Not eXists),它的作用是當設定一個KEY時,如果該KEY不存在,才能設定成功。那麼當一個客戶端設定一個KEY成功,並將其值設定為一個唯一識別符號,其他客戶端使用同樣的命令則會設定失敗,也就是說,其他客戶端在該KEY存在的週期內,是無法成功設定該KEY的值的(程式上返回false),這相當於設定該KEY的值成功的客戶端獲得了鎖,當該客戶端使用完畢並將其刪除,其他客戶端才能夠設定該KEY。
當系統負載較大的時候,WATCH鎖導致程式進行重試的次數劇增,分散式鎖無此問題。
程式實現
# 獲取鎖
def acquire_lock(conn, lockname, acquire_timeout=10):
identifier = str(uuid.uuid4())
end = time.time() + acquire_timeout
# 在超時時間內不停重試,直到設定KEY的值成功,也即獲得了「鎖」
while time.time() < end:
if conn.setnx('lock:' + lockname, identifier):
# 加鎖成功返回識別符號
return identifier
time.sleep(.001)
return False
# 4.4.3節中的函式改寫
def purchase_item_with_lock(conn, buyerid, itemid, sellerid):
buyer = "users:%s" % buyerid
seller = "users:%s" % sellerid
item = "%s.%s" % (itemid, sellerid)
inventory = "inventory:%s" % buyerid
# 加鎖(這裡對整個市場加鎖)
# 也可以改變鎖的粒度,只對操作到的商品加鎖
locked = acquire_lock(conn, 'market:')
if not locked:
return False
pipe = conn.pipeline(True)
try:
pipe.zscore("market:", item)
pipe.hget(buyer, 'funds')
price, funds = pipe.execute()
if price is None or price > funds:
return None
pipe.hincrby(seller, 'funds', int(price))
pipe.hincrby(buyer, 'funds', int(-price))
pipe.sadd(inventory, itemid)
pipe.zrem("market:", item)
pipe.execute()
return True
finally:
# 釋放鎖
release_lock(conn, 'market:', locked)
# 釋放鎖
def release_lock(conn, lockname, identifier):
pipe = conn.pipeline(True)
lockname = 'lock:' + lockname
while True:
try:
pipe.watch(lockname)
if pipe.get(lockname) == identifier:
pipe.multi()
pipe.delete(lockname)
pipe.execute()
return True
pipe.unwatch()
break
except redis.exceptions.WatchError:
pass
return False
帶超時的鎖
可以取得鎖之後,給鎖加一個超時時間,可以避免因為不可預料的情況,持有者一直持有鎖。
acquire_lock_with_timeout(conn, lockname, acquire_timeout=10, lock_timeout=10):
identifier = str(uuid.uuid4())
lockname = 'lock:' + lockname
lock_timeout = int(math.ceil(lock_timeout))
end = time.time() + acquire_timeout
while time.time() < end:
if conn.setnx(lockname, identifier):
# 給鎖設定超時時間
conn.expire(lockname, lock_timeout)
return identifier
# 如果鎖已存在,順便設定下超時時間
elif conn.ttl(lockname) < 0:
conn.expire(lockname, lock_timeout)
time.sleep(.001)
return False
計數訊號量
-
計數訊號量是一種鎖,用於限制訪問同一資源的程式數
-
可以把上一節建立的鎖看成只能被一個程式訪問的計數訊號量
-
同樣有獲取鎖、釋放鎖的操作,但獲取鎖失敗時,傾向於立即返回失敗結果,而不是重試
-
實現超時限制特性:1.使用EXPIRE 2.使用有序集合
例子
儲存訊號量資訊的有序集合:
-
佔坑
def acquire_semaphore(conn, semname, limit, timeout=10): identifier = str(uuid.uuid4()) now = time.time() pipeline = conn.pipeline(True) # 清理超時的坑位 pipeline.zremrangebyscore(semname, '-inf', now - timeout) pipeline.zadd(semname, {identifier::now}) # 佔個坑先 pipeline.zrank(semname, identifier) # 獲取位置 if pipeline.execute()[-1] < limit: # 如果坑位還足夠,就搶佔成功 return identifier # 坑位不足,佔不到坑,就只能灰溜溜地走了 conn.zrem(semname, identifier) return None
-
釋放坑位
def release_semaphore(conn, semname, identifier): return conn.zrem(semname, identifier)
公平訊號量
解決系統時間可能不一致,導致系統時間較慢的客戶端能搶先佔有訊號量的問題。
實現思路:新增一個計數器,再新增一個儲存計數訊號量資訊的有序集合(A),這個有序集合以最新的計數器的值為score,還需要另一個儲存計數訊號量的有序集(B),score值為時間戳,用來清理超時的訊號量。B清理調超時訊號量後,與A求交集(zinterstore),B的聚合權重設為0,最終結果儲存到A,這樣一來,A通過B間接地清理掉超時訊號量。實現程式碼如下: -
取得計數訊號量
def acquire_fair_semaphore(conn, semname, limit, timeout=10): identifier = str(uuid.uuid4()) czset = semname + ':owner' # 訊號量集合(B) ctr = semname + ':counter' # 計數器 now = time.time() pipeline = conn.pipeline(True) # 刪除(A)中超時的,然後與czset(B)求交集來間接刪除czset中超時的 pipeline.zremrangebyscore(semname, '-inf', now - timeout) # 求交集,semname聚合權重為0,這樣不影響(A)的score值 pipeline.zinterstore(czset, {czset: 1, semname: 0}) pipeline.incr(ctr) counter = pipeline.execute()[-1] pipeline.zadd(semname, identifier, now) # 以時間戳為score的zset pipeline.zadd(czset, identifier, counter) # 以計數為score的zset pipeline.zrank(czset, identifier) # 獲取位置 if pipeline.execute()[-1] < limit: return identifier pipeline.zrem(semname, identifier) pipeline.zrem(czset, identifier) pipeline.execute() return None
-
釋放鎖
比非公平訊號量多了一個集合要刪除。
def release_fair_semaphore(conn, semname, identifier): pipeline = conn.pipeline(True) pipeline.zrem(semname, identifier) pipeline.zrem(semname + ':owner', identifier) return pipeline.execute()[0]
-
重新整理訊號量
前面設定的計數訊號量10s就超時,需要進行重新整理,防止過期
# 重新整理訊號量 def refresh_fair_semaphore(conn, semname, identifier): # zadd操作,元素不存在時執行新增操作,返回1;存在時則更新元素,返回0 if conn.zadd(semname, identifier, time.time()): # 如果是新增,將判斷語句新增的刪除,返回False,表示已過期 release_fair_semaphore(conn, semname, identifier) return False # 如果是更新,直接跳到這裡,返回True,表示重新整理成功 return True
-
獲取訊號量之加鎖版
用於消除競爭條件
def acquire_semaphore_with_lock(conn, semname, limit, timeout=10): # 設定一個超短時間過期的鎖 identifier = acquire_lock(conn, semname, acquire_timeout=.01) # 如果設定成功 if identifier: try: # 獲取公平訊號量 return acquire_fair_semaphore(conn, semname, limit, timeout) finally: # 將獲得的鎖刪除 release_lock(conn, semname, identifier)
-
計數訊號量總結
- 如果對系統的時鐘差異可以接受,可以使用第一種
- 不能接受系統的時鐘差異,可以使用公平訊號量
- 希望訊號量一直執行正確,使用第三種,加鎖獲取訊號量(推薦)
任務佇列
import redis
conn = redis.Redis(host='127.0.0.1', port=6379)
# 已售出商品郵件佇列
def send_sold_email_via_queue(conn, seller, item, price, buyer):
data = {
'seller_id': seller,
'item_id': item,
'price': price,
'buyer_id': buyer,
'time': time.time()
}
conn.rpush('queue:email', json.dumps(data))
# 讀取列表中的發郵件任務,傳送郵件
def process_sold_email_queue(conn):
while not QUIT:
packed = conn.blpop(['queue:email'], 30)
if not packed:
continue
to_send = json.loads(packed[1])
try:
fetch_data_and_send_sold_email(to_send)
except EmailSendError as err:
log_error("Failed to send sold email", err, to_send)
else:
log_success("Sent sold email", to_send)
# 可以執行多種任務的佇列
def worker_watch_queue(conn, queue, callbacks):
while not QUIT:
packed = conn.blpop([queue], 30)
if not packed:
continue
name, args = json.loads(packed[1])
# 如果任務沒有在callbacks中註冊
if name not in callbacks:
log_error("Unknown callback %s"%name)
continue
# 呼叫在callbacks中已註冊的函式
callbacks[name](*args)
# 任務優先順序
# 實現原理:blpop可傳入多個list,排在前面的list優先處理
def worker_watch_queues(conn, queues, callbacks):
while not QUIT:
packed = conn.blpop(queues, 30) # queues是由多個list組成的list
if not packed:
continue
name, args = json.loads(packed[1])
if name not in callbacks:
log_error("Unknown callback %s"%name)
continue
callbacks[name](*args)
# 延遲任務
# 實現思路:把所有要延遲的任務加到有序集合,執行時間作為score
# 另外的一個程式用來查詢有序集合裡面是否有要立即執行的任務
# 如果有的話,就從有序集合移除,新增到佇列中
def execute_later(conn, queue, name, args, delay=0):
identifier = str(uuid.uuid4())
item = json.dumps([identifier, queue, name, args])
if delay > 0: # 新增到有序集合 延遲任務執行
conn.zadd('delayed:', item, time.time() + delay)
else: # 立即執行的任務
conn.rpush('queue:' + queue, item)
return identifier
# 執行延遲任務
def poll_queue(conn):
while not QUIT:
# 取出第一個元素
item = conn.zrange('delayed:', 0, 0, withscores=True)
# 如果沒有元素 休眠0.01s
if not item or item[0][1] > time.time():
time.sleep(.01)
continue
# 取出要執行的任務
item = item[0][0]
identifier, queue, function, args = json.loads(item)
locked = acquire_lock(conn, identifier)
if not locked:
continue
# 從有序集合移除該任務 並 新增到任務佇列
if conn.zrem('delayed:', item):
conn.rpush('queue:' + queue, item)
release_lock(conn, identifier, locked)