最近在用python的一款非同步web框架sanic搭建web服務,遇到一個需要加特定鎖的場景:同一使用者併發處理訂單時需要排隊處理,但不同使用者不需要排隊。
如果僅僅使用async with asyncio.Lock()的話。會使所有請求都排隊處理。
1 import asyncio 2 import datetime 3 4 lock = asyncio.Lock() 5 6 7 async def place_order(user_id, order_id): 8 async with lock: 9 # 模擬下單處理 10 print(f"{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S:%f')} Processing order {order_id} for user {user_id}") 11 await asyncio.sleep(1) # 假設處理需要 1 秒 12 print(f"{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S:%f')} Order {order_id} for user {user_id} is done") 13 14 15 # 定義一個測試函式 16 async def test(): 17 # 建立四個任務,模擬兩個 UserID 的併發請求 18 tasks = [ 19 asyncio.create_task(place_order(1, 101)), 20 asyncio.create_task(place_order(1, 102)), 21 asyncio.create_task(place_order(2, 201)), 22 asyncio.create_task(place_order(2, 202)), 23 ] 24 # 等待所有任務完成 25 await asyncio.gather(*tasks) 26 27 28 if __name__ == '__main__': 29 # 執行測試函式 30 asyncio.run(test())
這顯然不是想要的結果,第二種方案是定義一個字典,key使用user_id,value為asyncio.Lock(),每次執行前從字典裡面獲取lock,相同的user_id將會使用同一個lock,那就實現了功能。
import asyncio import datetime locks = {} async def place_order(user_id, order_id): if user_id not in locks: locks[user_id] = asyncio.Lock() async with locks[user_id]: # 模擬下單處理 print(f"{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S:%f')} Processing order {order_id} for user {user_id}") await asyncio.sleep(1) # 假設處理需要 1 秒 print(f"{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S:%f')} Order {order_id} for user {user_id} is done") # 定義一個測試函式 async def test(): # 建立四個任務,模擬兩個 UserID 的併發請求 tasks = [ asyncio.create_task(place_order(1, 101)), asyncio.create_task(place_order(1, 102)), asyncio.create_task(place_order(2, 201)), asyncio.create_task(place_order(2, 202)), ] # 等待所有任務完成 await asyncio.gather(*tasks) if __name__ == '__main__': # 執行測試函式 asyncio.run(test())
但是這個方案會有缺點是,user_id執行完成之後沒有釋放資源,當請求的user_id變多之後,勢必會造成佔用過多的資源。繼續改進方案,將locks的value加一個計數器,當獲取lock時計數器加1,使用完之後計數器-1,當計數器變為小於等於0時,釋放locks對應的key。最後將這個功能封裝為一個類方便其他地方呼叫。
import asyncio mutex_locks = {} class MutexObj: def __init__(self): self.lock = asyncio.Lock() self.count = 0 class Mutex: def __init__(self, key: str): if key not in mutex_locks: mutex_locks[key] = MutexObj() self.__mutex_obj = mutex_locks[key] self.__key = key def lock(self): """ 獲取鎖 :return: """ self.__mutex_obj.count += 1 return self.__mutex_obj.lock def release(self): """ 釋放鎖 :return: """ self.__mutex_obj.count -= 1 if self.__mutex_obj.count <= 0: del mutex_locks[self.__key]
import asyncio import datetime from utils.mutex import Mutex, mutex_locks locks = {} async def place_order(user_id, order_id): mutex = Mutex(user_id) async with mutex.lock(): try: # 模擬下單處理 print(f"{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S:%f')} Processing order {order_id} for user {user_id}") await asyncio.sleep(1) # 假設處理需要 1 秒 print(f"{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S:%f')} Order {order_id} for user {user_id} is done") except Exception as ex: print(ex) finally: mutex.release() print('=====================') print(mutex_locks) print('=====================') # 定義一個測試函式 async def test(): # 建立四個任務,模擬兩個 UserID 的併發請求 tasks = [ asyncio.create_task(place_order(1, 101)), asyncio.create_task(place_order(1, 102)), asyncio.create_task(place_order(2, 201)), asyncio.create_task(place_order(2, 202)), ] # 等待所有任務完成 await asyncio.gather(*tasks) if __name__ == '__main__': # 執行測試函式 asyncio.run(test())
至此實現了我的需求,此方案只考慮了單應用場景,如果是分散式部署,需要更換方案如redis鎖,這裡暫且不考慮。如果有其他實現方式,歡迎留言交流。