# 好像要鎖一下ㄟ(悲觀鎖、樂觀鎖) [TOC] ### 情境 - 一般的電商網站:在電商網站中我們很常會遇到需要搶票、商品,在搶購的過程中除了高併發,同時也要考慮到商品會不會賣超,像是明明只有5個商品,卻有10個人搶到商品並結帳,這個時候就需要透過`悲觀鎖、樂觀鎖`來解決了 - 脫機玩家瘋狂打遊戲內任務領獎,導致多次領取獎勵,所以上`悲觀鎖`解決 ### 簡單介紹悲觀鎖、樂觀鎖 - 想詳細了解可以點擊[這裡](https://medium.com/dean-lin/%E7%9C%9F%E6%AD%A3%E7%90%86%E8%A7%A3%E8%B3%87%E6%96%99%E5%BA%AB%E7%9A%84%E6%82%B2%E8%A7%80%E9%8E%96-vs-%E6%A8%82%E8%A7%80%E9%8E%96-2cabb858726d) - **悲觀鎖** - 透過DB的**Transaction**機制,限制一筆資料一次只有一個人可以讀寫 - pos: - 使用DB Transaction強迫執行的順序 - neg: - 使用Transaction會導致其他SQL command針對這個data除了查詢功能外全部卡死,也可能造成吞吐量下降 - **樂觀鎖** - 藉由一個每次更新都會自增的值來解決 - e.g ```sql UPDATE test SET num = num - 1, version = version + 1 WHERE id = 1 AND version = 0; ``` - pos: - 因為不需要加鎖,所以可以避免悲觀鎖吞吐量問題 - neg: - 因為樂觀鎖是我們人為實現的,所以換一個業務場景可能會不適用 ### Django實現悲觀鎖 - 透過`select_for_update()`,本身是行鎖(鎖定該行資料,別人無法進行操作),能鎖定所有匹配的行,直到事務結束 ```python= class PessimisticView(APIView): @transaction.atomic # 必須使用transaction def post(self, request, *args, **kwargs): result = CustomUser.objects.select_for_update().filter(first_name="123456") print(result) time.sleep(10) # 當return後會自動釋放鎖 return HttpResponse("hello world") # 當然也可以使用with的方式來撰寫 def post2(self, request, *args, **kwargs): with transaction.atomic(): result = CustomUser.objects.select_for_update().filter(first_name="123456") print(result) time.sleep(10) # 當return後會自動釋放鎖 return HttpResponse("hello world") ``` #### 驗證是否有行鎖 - 在上面的程式中增加`time.sleep(10)`,然後先執行上面的api,在執行一般的update sql語法,發現sql語法卡了約9s左右的時間,符合上面設定的10s ```sql= mysql> UPDATE accounts_customuser SET email = 'xxxx@gmail.com' WHERE first_name = '123456'; Query OK, 1 row affected (8.95 sec) ``` ### Django實現樂觀鎖 - 透過`django-concurrency`package來做到每次`save()`時,都會更改version欄位,藉此來防止同時二次更新 ```python= # 創建對應的model,再用python manage.py 工具來同步到DB內 class OrderModel(models.Model): version = IntegerVersionField() # 創建、更新時候會自動更改 name = models.CharField(max_length=100) # 先創建一筆資料,此時version已經有值了(如下圖1) OrderModel.objects.create(name="eddy") class OptimisticView(APIView): def get(self, request, *args, **kwargs): a = ConcurrentModel.objects.get(pk=1) a.name = "eddy2" b = ConcurrentModel.objects.get(pk=1) b.name = "eddy3" # 會修改該筆資料的version(如下圖2) a.save() # 會噴這個錯誤concurrency.exceptions.RecordModifiedError: Record has been modified # 因為version已經跟當時不一樣了 b.save() return HttpResponse("hello world") ``` - 圖1 ![](https://i.imgur.com/yEYIMtG.png) - 圖2 ![](https://i.imgur.com/HJYVCM3.png) ### 結論 - 在Django中提供了很方便的語法、資料格式,來解決上鎖的問題 - 至於要使用哪種鎖,可能還是要根據情境來做選擇,各有優缺 ### 參考資料 1. [真正理解資料庫的悲觀鎖-vs-樂觀鎖](https://medium.com/dean-lin/%E7%9C%9F%E6%AD%A3%E7%90%86%E8%A7%A3%E8%B3%87%E6%96%99%E5%BA%AB%E7%9A%84%E6%82%B2%E8%A7%80%E9%8E%96-vs-%E6%A8%82%E8%A7%80%E9%8E%96-2cabb858726d) 2. [Django 事務操作、悲觀鎖、樂觀鎖](https://zhuanlan.zhihu.com/p/372957129) 3. [Django 管理併發操作 select_for_update](https://www.twblogs.net/a/5d6cbd43bd9eee5327fefc49)