# 好像要鎖一下ㄟ(悲觀鎖、樂觀鎖) [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  - 圖2  ### 結論 - 在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)
×
Sign in
Email
Password
Forgot password
or
Sign in via Google
Sign in via Facebook
Sign in via X(Twitter)
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
Continue with a different method
New to HackMD?
Sign up
By signing in, you agree to our
terms of service
.