您好,登錄后才能下訂單哦!
在計算機并發(fā)領域編程中總是會與鎖打交道,鎖又有很多種,互斥鎖、自旋鎖等等。
鎖總是伴隨著線程、進程這樣的詞匯出現(xiàn),阮一峰有 一篇文章 對這些名詞進行了簡單易懂的解釋。
我的理解是,使用線程、進程是為了實現(xiàn)并發(fā)從而獲得性能的提升(利用多核CPU,多臺服務器),但這種并發(fā)由于調(diào)度的不確定性,很容易出亂子,為了(在一些共享資源、關鍵節(jié)點上)不出亂子,又需要對資源加鎖,在操作這個資源時控制這種并發(fā),將亂子消滅。
很多語言都提供了一些線程級別的鎖實現(xiàn)以及一些相應的工具,但在進程方面就無能為力了。而一個服務部署到生產(chǎn)環(huán)境,往往會部署多個實例,這種情況下,就經(jīng)常會用到給不同進程用的鎖,分布式鎖便是在分布式系統(tǒng)中對某共享資源進行加鎖的構件。
現(xiàn)在來試著展示一下在Python項目中如何使用簡單的分布式互斥鎖。
不使用分布式鎖會怎樣
先用一個簡單的實例來演示一下,不使用分布式鎖會出怎樣的亂子。
假設商城系統(tǒng)要做秒殺活動,在redis中記錄著 count:1 的信息,到秒殺時間點的時候,會收到許多的請求,這時各應用程序去查redis中count的值,若count還大于0,則將count-1,這樣其他請求就不再能秒殺到了。
# -*- coding: utf-8 -*- import os import arrow import redis from multiprocessing import Pool HOT_KEY = 'count' r = redis.Redis(host='localhost', port=6379) def seckilling(): name = os.getpid() v = r.get(HOT_KEY) if int(v) > 0: print name, ' decr redis.' r.decr(HOT_KEY) else: print name, ' can not set redis.', v def run_without_lock(name): while True: if arrow.now().second % 5 == 0: seckilling() return if __name__ == '__main__': p = Pool(16) r.set(HOT_KEY, 1) for i in range(16): p.apply_async(run_without_lock, args=(i, )) print 'now 16 processes are going to get lock!' p.close() p.join() print('All subprocesses done.')
以上代碼使用多進程來模仿這種并發(fā)請求場景,程序開始的時候?qū)ount設為1,之后各進程開始進入等待,當秒數(shù)為5的時候,所有進程同時去訪問秒殺函數(shù),來看一下效果:
運行結果
redis查詢展示
從程序打印與查redis的結果來說并未如愿,本來秒殺商品只有一件,但卻被成功搶購到了4次。這是由于各進程在get count的值時,對redis值更新的指令已經(jīng)發(fā)出而還未進行完畢,會讓其他進程認為自己可以購得。
這種問題可歸為 不可重復讀 種類的數(shù)據(jù)并發(fā)問題。
在這種毫無保護的情況下,其他常見并發(fā)問題幻讀、臟讀、第一第二類丟失更新等都有可能發(fā)生,這里不再一一舉例。
使用ZooKeeper作分布式鎖
作為致力于解決分布式協(xié)同問題的知名工具,利用zookeeper提供的API和它對于節(jié)點唯一性與順序一致性的保證可以實現(xiàn)分式式鎖。
實現(xiàn)思路為,各進程去創(chuàng)建 /exclusive_lock/lock
的結點,zookeeper保證只有一個client可以創(chuàng)建成功,那么便認為創(chuàng)建成功的那個client獲得了鎖,當它處理完業(yè)務后,將該node刪除,其他client會監(jiān)聽到這個事件,并再次嘗試創(chuàng)建該節(jié)點,如此進行下去。
Kazoo 庫實現(xiàn)了這種Lock,使用起來非常簡單,編程人員可以不用再去自己實現(xiàn)acquire,release等鎖的通用接口。
同時在Python中,對鎖的使用往往可以通過優(yōu)雅的上下文管理器with。
def run_with_zk_lock(name): zk = KazooClient() zk.start() lock = zk.Lock("/lockpath", "my-identifier") while True: if arrow.now().second % 5 == 0: with lock: seckilling() return
使用zk結果
redis查詢展示
當秒殺發(fā)生時,只有獲得鎖的進程可以去進行秒殺操作。
在鎖的幫助下,程序按照預想的方式運行了。
使用redis作分布式鎖
在redis的網(wǎng)站有一篇文章 專門介紹如何使用redis作為分布式鎖,文尾還附帶了對此文章的反對文章以及再次回擊的文章,有點精彩。
文章提到了一個redlock的分布式鎖設計。
設置鎖的redis命令為 SET resource_name my_random_value NX PX 30000
,當加NX參數(shù)時,若resouce_name不存在才會創(chuàng)建,若不存在則會向client返回不同的結果,利用這個機制,便只有一個client可以set成功,就像上面的zk一樣了。
但是,實現(xiàn)這樣一個分布式鎖遠不止這么簡單,redis并不像zk一樣是一個分布式協(xié)同工具,會向client做出分布式中各種一致性及容錯、可用性的保證。
redis本身也是集群部署的,它們之間有著異步復制時間差、容錯等問題可能會出現(xiàn),要真正做到這個鎖的實現(xiàn)在線上大規(guī)模分布式系統(tǒng)中可用,真的是要考慮各種情況,很不容易。
關于如何在語言上實現(xiàn)一個鎖的接口,redlock的原理與代碼實現(xiàn),以及上述kazoo包里實現(xiàn)lock的源碼,我會在另一篇專門的文章中說一下。
redlock-py 包是python語言中對上述文章的實現(xiàn),我們現(xiàn)在使用它來進行嘗試。
rlock = RedLock([{"host": "localhost", "port": 6379, "db": 0}, ]) def run_with_redis_lock(name): while True: if arrow.now().second % 5 == 0: with rlock: seckilling() return
redis鎖運行結果
運行結果和上面使用zk一樣,符合程序設計預期。
以上只是基于python語言的一些代碼展示,通過使用兩個第三方包,來使用分布式鎖來避免并發(fā)程序中混亂的產(chǎn)生。
但其實這中間是有一個斷層的,即,這兩個工具都是提供了一個機制,而并不是直接對外提供了操作鎖的API,那么如何利用這個機制來實現(xiàn)這樣的鎖正是這兩個第三方做的事情。
簡單看過它們實現(xiàn)的源碼,以及threading中一些lock的代碼,發(fā)現(xiàn)在鎖的實現(xiàn)上是有著共通之處的,都有通用的acquire與release方法,然后將 enter 與 exit 指向前面兩個方法來實現(xiàn)上下文管理器with的用法。
此外,還可以利用關系型數(shù)據(jù)庫如MySQL固有的鎖機制來作為分布式鎖,但由于數(shù)據(jù)庫往往是系統(tǒng)的瓶頸所在,沒有必要為它引入不必要的壓力。同時,MySQL中的鎖、隔離級別也有一大堆可說的,在github上找了一下也并未找到一個成熟的像上面的基于MySQL實現(xiàn)的對外暴露鎖通用API的第三方包,故未能在上面加以展示。
想要說清楚這個事情并沒有那么容易,之后我會嘗試搞清楚如何寫一個比較地道的鎖,并對上面兩個第三方包的具體實現(xiàn)加以研究,爭取把這個斷層補上。之后,或許可以嘗試實現(xiàn)一下基于MySQL的類似第三方包,這需要對MySQL的一些機制搞得更加清楚才行。
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持億速云。
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。