日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 >

分布式锁的3种实现(数据库、缓存[redis]、Zookeeper)

發布時間:2025/3/15 35 豆豆
生活随笔 收集整理的這篇文章主要介紹了 分布式锁的3种实现(数据库、缓存[redis]、Zookeeper) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

鎖是開發過程中十分常見的工具,在處理高并發請求的時候和訂單數據的時候往往需要鎖來幫助我們保證數據的安全。

一、分布式鎖的場景

  • 場景1.前端點擊太快,導致后端重復調用接口。兩次調用一個接口,這樣就會產生同一個請求執行了兩次,而從用戶的角度出發,他是因為太卡而點了兩次,他的目標是執行一次請求。

  • 場景2.對于高并發場景,我們往往需要引入分布式緩存,來加快整個系統的響應速度。但是緩存是有失效機制的,如果某一時刻緩存失效,而此時有大量的請求過來,那么所有的請求會瞬間直接打到DB上,那么這么大的并發量,DB可能是扛不住的。那么這里需要引入一個保護機制。當發生“緩存擊穿”的時候加鎖,從而保護DB不被拖垮。

看完了上面的場景,其實分布式鎖的場景一直在我們身邊。說分布式鎖之前,應該先說一下java提供的鎖,比較能單機解決的并發問題,沒必要引入分布式的解決方案。java提供了兩種內置的鎖的實現,一種是由JVM實現的synchronized和JDK提供的Lock,當你的應用是單機或者說單進程應用時,可以使用synchronized或Lock來實現鎖。但是,當你的應用涉及到多機、多進程共同完成時,例如現在的互聯網架構,一般都是分布式的RPC框架來支撐,那么這樣你的Server有多個,由于負載均衡的路由規則隨機,相同的請求可能會打到不同的Server上進行處理,那么這時候就需要一個全局鎖來實現多個線程(不同的進程)之間的同步。實現全局的鎖需要依賴一個第三方系統,此系統需要滿足高可用、一致性比較強同時能應付高并發的請求。常見的處理辦法有三種:數據庫、緩存、分布式協調系統。數據庫和緩存是比較常用的,但是分布式協調系統是不常用的。

二、數據庫實現分布式鎖

利用DB來實現分布式鎖,有兩種方案。兩種方案各有好壞,但是總體效果都不是很好。但是實現還是比較簡單的。

  • 利用主鍵唯一規則:
    我們知道數據庫是有唯一主鍵規則的,主鍵不能重復,對于重復的主鍵會拋出主鍵沖突異常。
    了解JDK reentrantlock的人都知道,reentrantlock是利用了OS的CAS特性實現的鎖。主要是維護一個全局的狀態,每次競爭鎖都會CAS修改鎖的狀態,修改成功之后就占用了鎖,失敗的加入到同步隊列中,等待喚醒。其實這和分布式鎖實現方案基本是一致的,首先我們利用主鍵唯一規則,在爭搶鎖的時候向DB中寫一條記錄,這條記錄主要包含鎖的id、當前占用鎖的線程名、重入的次數和創建時間等,如果插入成功表示當前線程獲取到了鎖,如果插入失敗那么證明鎖被其他人占用,等待一會兒繼續爭搶,直到爭搶到或者超時為止。
    這里我主要寫了一個簡單的實現:
  • import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit;/*** 利用mysql實現可重入分布式鎖*/ public class MysqlprimaryLock {private static Connection connection;static {try {Class.forName("com.mysql.jdbc.Driver");} catch (ClassNotFoundException e) {e.printStackTrace();}String url = "jdbc:mysql://10.0.0.212:3308/dbwww_lock?user=lock_admin&password=lock123";try {connection = DriverManager.getConnection(url);} catch (SQLException e) {e.printStackTrace();}}/*** 加鎖* @param lockID*/public void lock(String lockID) {acquire(lockID);}/*** 獲取鎖* @param lockID* @return*/public boolean acquire(String lockID) {String sql = "insert into test_lock('id','count','thName','addtime') VALUES (?,?,?,?)";while (true) {try {PreparedStatement statement = connection.prepareStatement(sql);statement.setString(1, lockID);statement.setInt(2, 1);statement.setLong(1, System.currentTimeMillis());boolean ifsucess = statement.execute();//如果成功,那么就是獲取到了鎖if (ifsucess)return true;} catch (SQLException e) {e.printStackTrace();}try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}continue;}}/*** 超時獲取鎖* @param lockID* @param timeOuts* @return* @throws InterruptedException*/public boolean acquire(String lockID, long timeOuts) throws InterruptedException {String sql = "insert into test_lock('id','count','thName','addtime') VALUES (?,?,?,?)";long futureTime = System.currentTimeMillis() + timeOuts;long ranmain = timeOuts;long timerange = 500;while (true) {CountDownLatch latch = new CountDownLatch(1);try {PreparedStatement statement = connection.prepareStatement(sql);statement.setString(1, lockID);statement.setInt(2, 1);statement.setLong(1, System.currentTimeMillis());boolean ifsucess = statement.execute();//如果成功,那么就是獲取到了鎖if (ifsucess)return true;} catch (SQLException e) {e.printStackTrace();}latch.await(timerange, TimeUnit.MILLISECONDS);ranmain = futureTime - System.currentTimeMillis();if (ranmain <= 0)break;if (ranmain < timerange) {timerange = ranmain;}continue;}return false;}/*** 釋放鎖* @param lockID* @return* @throws SQLException*/public boolean unlock(String lockID) throws SQLException {String sql = "DELETE from test_lock where id = ?";PreparedStatement statement = connection.prepareStatement(sql);statement.setString(1, lockID);boolean ifsucess = statement.execute();if (ifsucess)return true;return false;} }

    這里是利用主鍵沖突規則,加入了id’,‘count’,‘thName’,‘addtime’,count主要是為了重入計數,thName為了判斷占用鎖的線程,addtime是記錄占用時間。上面代碼沒有實現重入的邏輯。重入主要實現思路是,在每次獲取鎖之前去取當前鎖的信息,如果鎖的線程是當前線程,那么更新鎖的count+1,并且執行鎖之后的邏輯。如果不是當前鎖,那么進行重試。釋放的時候也要進行count-1,最后減到0時,刪除鎖標識釋放鎖。

    • 優點:實現簡單

    • 缺點:沒有超時保護機制,mysql存在單點,并發量大的時候請求量太大、沒有線程喚醒機制,用異常去控制邏輯多少優點惡心。

    • 對于超時保護:如果可能,可以采用定時任務去掃描超過一定閾值的鎖,并刪除。但是也會存在,鎖住的任務執行時間很長,刪除鎖會導致并發問題。所以需要對超時時間有一個很好的預估。

    • 對于單點問題:有條件可以搞一個主從,但是為了一個鎖來搞一個主從是不是優點浪費?同時主從切換的時候系統不可用,這也是一個問題。

    • 并發量大的時候請求量太大:因為這種實現方式是沒有鎖的喚醒機制的,不像reentrantlock在同步隊列中的節點,可以通過喚醒來避免多次的循環請求。但是分布式環境數據庫這種鎖的實現是不能做到喚醒的。所以只能將獲取鎖的時間間隔調高,避免死循環給系統和DB帶來的巨大壓力。這樣也犧牲了系統的吞吐量,因為總會有一定的間隔鎖是空閑的。

    用異常去控制邏輯多少有點惡心:就不說了,每次失敗都拋異?!?/p>

  • 利用Mysql行鎖的特性:
  • Mysql是有表鎖、頁鎖和行鎖的機制的,可以利用這個機制來實現鎖。這里盡量使用行鎖,它的吞吐量是最高的。

    /*** 超時獲取鎖* @param lockID* @param timeOuts* @return* @throws InterruptedException*/ public boolean acquireByUpdate(String lockID, long timeOuts) throws InterruptedException, SQLException {String sql = "SELECT id from test_lock where id = ? for UPDATE ";long futureTime = System.currentTimeMillis() + timeOuts;long ranmain = timeOuts;long timerange = 500;connection.setAutoCommit(false);while (true) {CountDownLatch latch = new CountDownLatch(1);try {PreparedStatement statement = connection.prepareStatement(sql);statement.setString(1, lockID);statement.setInt(2, 1);statement.setLong(1, System.currentTimeMillis());boolean ifsucess = statement.execute();//如果成功,那么就是獲取到了鎖if (ifsucess)return true;} catch (SQLException e) {e.printStackTrace();}latch.await(timerange, TimeUnit.MILLISECONDS);ranmain = futureTime - System.currentTimeMillis();if (ranmain <= 0)break;if (ranmain < timerange) {timerange = ranmain;}continue;}return false; } /*** 釋放鎖* @param lockID* @return* @throws SQLException*/ public void unlockforUpdtate(String lockID) throws SQLException {connection.commit(); }

    利用for update加顯式的行鎖,這樣就能利用這個行級的排他鎖來實現分布式鎖了,同時unlock的時候只要釋放commit這個事務,就能達到釋放鎖的目的。

    • 優點:實現簡單

    • 缺點:連接池爆滿和事務超時的問題單點的問題,單點問題,行鎖升級為表鎖的問題,并發量大的時候請求量太大、沒有線程喚醒機制。

    • 連接池爆滿和事務超時的問題單點的問題:利用事務進行加鎖的時候,query需要占用數據庫連接,在行鎖的時候連接不釋放,這就會導致連接池爆滿。同時由于事務是有超時時間的,過了超時時間自動回滾,會導致鎖的釋放,這個超時時間要把控好。

    • 對于單點問題:同上。

    • 并發量大的時候請求量太大:同上。

    • 行鎖升級為表鎖的問題:Mysql行鎖默認需要走索引,如果不走索引會導致鎖表,如果可以,在sql中可以強制指定索引。

    三、緩存分布式鎖(redis、memcached)

    緩存實現分布式鎖還是比較常見的,因為緩存比較輕量,并且緩存的響應快、吞吐高。最重要的是還有自動失效的機制來保證鎖一定能釋放。`緩存的分布式鎖主要通過Redis實現,當然其他的緩存也是可以的。

  • 基于SetNX實現:
  • setNX是Redis提供的一個原子操作,如果指定key存在,那么setNX失敗,如果不存在會進行Set操作并返回成功。我們可以利用這個來實現一個分布式的鎖,主要思路就是,set成功表示獲取鎖,set失敗表示獲取失敗,失敗后需要重試。
    具體看下偽代碼:

    這種方式也是有優點和缺點:

    • 優點:實現簡單,吞吐量十分客觀,對于高并發情況應付自如,自帶超時保護,對于網絡抖動的情況也可以利用超時刪除策略保證不會阻塞所有流程。

    • 缺點:單點問題、沒有線程喚醒機制、網絡抖動可能會引起鎖刪除失敗。

    • 對單點問題:因為redis一般都是單實例使用,那么對于單點問題,可以做一個主從。當然主從切換的時候也是不可用的,因為主從同步是異步的,可能會并發問題。如果對于主從還是不能保證可靠性的話,可以上Redis集群,對于Redis集群,因為使用了類一致性Hash算法,雖然不能避免節點下線的并發問題(當前的任務沒有執行完,其他任務就開始執行),但是能保證Redis是可用的??捎眯缘膯栴}是出了問題之后的備選方案,如果我們系統天天都出問題還玩毛啊,對于突發情況犧牲一兩個請求還是沒問題的。

    • 對于線程喚醒機制:分布式鎖大多都是這樣輪訓獲取鎖的,所以控制住你的重試頻率,也不會導致負載特別高的。可能就是吞吐量低點而已。

    • 對于鎖刪除失敗:分布式鎖基本都有這個問題,可以對key設置失效時間。這個超時時間需要把控好,過大那么系統吞吐量低,很容易導致超時。如果過小那么會有并發問題,部分耗時時間比較長的任務就要遭殃了。

    關于redis作為分布式鎖是否安全的一篇好文章

    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {//1、占分布式鎖。去redis占坑String uuid = UUID.randomUUID().toString();Boolean lock=redisTemplate.opsForValue().setIfAbsent("lock",uuid,300,TimeUnit.SECONDS);if(lock){System.out.println("獲取分布式鎖成功...");//加鎖成執行業務//2、設置過期時間,必須和加鎖是同步的,原子的//redisTemplate.expire("lock",30,TimeUnit.SECONDS);Map<String, List<Catelog2Vo>> dataFromDb;try{dataFromDb = getDataFromDb();}finally {//lua腳本String script = "if redis.call('get', KEYS[1]) == ARGV[1] thenreturn redis.call('del', KEYS[1]) else return 0 end";//刪除鎖Long lock1 = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class) , Arrays.asList("lock"), uuid);} return dataFromDb;}else{//加鎖失敗..synchronized ()//休眠100msSystem.out.println("獲取鎖失敗..重試");try{Thread.sleep(200);}catch(Exception e){ } return getCatalogJsonFromDbWithRedisLock();//自旋的方式 } }
  • 使用Redisson完成分布式鎖
    Redisson 是架設在Redis 基礎上的一個 Java 駐內存數據網格(In-Memory Data Grid)。充分的利用了 Redis 鍵值數據庫提供的一系列優勢,基于Java 實用工具包中常用接口,為使用者提供了一系列具有分布式特性的常用工具類。使得原本作為協調單機多線程并發程序的工具包獲得了協調分布式多機多線程并發系統的能力,大大降低了設計和研發大規模分布式系統的難度。同時結合各富特色的分布式服務,更進一步簡化了分布式環境中程序相互之間的協作。
    官方文檔
  • 更詳細的關于redis實現分布式鎖的文章

    四、基于Zookeeper的分布式鎖

    Zookeeper是一個分布式一致性協調框架,主要可以實現選主、配置管理和分布式鎖等常用功能,因為Zookeeper的寫入都是順序的,在一個節點創建之后,其他請求再次創建便會失敗,同時可以對這個節點進行Watch,如果節點刪除會通知其他節點搶占鎖。Zookeeper實現分布式鎖雖然是比較重量級的,但實現的鎖功能十分健全,由于Zookeeper本身需要維護自己的一致性,所以性能上較Redis還是有一定差距的。

    五、對比

    • Mysql實現比較簡單,不需要引入第三個應用,但實現多少有些重,性能不是很好。
    • Redis的話實現比較簡單,同時性能很好,引入集群可以提高可用性。同時定期失效的機制可以解決因網絡抖動鎖刪除失敗的問題,所以我比較傾向Redis實現。
    • Zookeeper實現是有些重的,同時我們還需要維護Zookeeper集群,實現起來還是比較復雜的,實現不好的話還會引起“羊群效應”。如果不是原有系統就依賴Zookeeper,同時壓力不大的情況下。一般不使用Zookeeper實現分布式鎖。

    參考文章

    總結

    以上是生活随笔為你收集整理的分布式锁的3种实现(数据库、缓存[redis]、Zookeeper)的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。