SpringBoot實現分佈式鎖

SpringBoot.Redis實現分佈式鎖

[提前聲明]
文章由作者:張耀峯 結合自己生產中的使用經驗整理,最終形成簡單易懂的文章
寫作不易,轉載請註明,謝謝!
spark代碼案例地址: https://github.com/Mydreamandreality/sparkResearch


線程鎖

哎?我們不是要實現分佈式鎖嗎,爲啥扯到了線程鎖?
不要急,防止有些朋友不熟悉分佈式鎖的概念,在實現分佈式鎖之前,咱們先了解下線程鎖
線程鎖可能對這個概念也有些人不是很清楚,但如果我說synchronize同步關鍵字,大家是不是就知道了

  • 線程鎖:主要用來給類,方法,代碼加鎖,當某個方法或者某塊代碼使用synchronize關鍵字來修飾,那麼在同一時刻最多只能有一個線程執行該代碼,如果同一時刻有多個線程訪問該代碼,其它未搶到資源的線程標記爲阻塞狀態,直到獲取到鎖的線程執行完畢自動釋放其餘線程才能執行
  • 線程鎖synchronize在單機部署的狀態下,確實可以保證線程安全,但是如果是集羣或者分佈式部署呢?
集羣模式下線程鎖爲何無法滿足需求?
  • 分佈式的CAP理論:

任何一個分佈式系統都無法同時滿足一致性(Consistency)、可用性(Availability)和分區容錯性(Partition tolerance),最多隻能同時滿足兩項

  • 現在許多項目考慮到性能和擴展性都通過分佈式的方式進行部署,分佈式中的數據一致性一直是一個比較重要的問題,基於CAP的理論,在系統最開始設計的時候,就需要對這三點進行取捨,在我們的場景中,一般來講都是犧牲強一致性,來提升高可用,系統只需要保證最終一致性即可
  • 如果要保證數據的最終一致性可以通過分佈式鎖,分佈式事務等一些技術來實現,很多時候要達到最終一致性,也要保證一個方法在同一時刻只能被一個線程執行
  • 上面我們也說了,在單機環境中,synchronize也確實可以解決我們的問題,但是在分佈式環境中,就不可以了,爲什麼?
  • 我們要知道,分佈式系統和單機系統最大的區別就是,單機系統中是多線程,分佈式系統中是多進程
  • 多線程可以共享一個Jvm中的堆內存,所以可以採取內存作爲標記存儲的位置,多進程的情況下,有可能這些進程都不在同一臺物理機上,是無法共享同一Jvm中的堆內存,所以就需要把標記存儲在一個第三方上面,保證所有進程可見

講到這裏大家也大概能反推出synchronize的底層實現了吧,其實就是Jvm在方法常量池中的方法表結構訪問標誌區來判斷某個方法是否同步方法,方法被調用的時候,調用的指令就會檢查方法的訪問標記有沒有被設置同步,如果設置了,當前執行線程就會持有一個標記,然後執行方法,最後執行完再釋放這個標記

設計分佈式鎖
  • 通過上面的瞭解,大家應該可以再次反推出分佈式鎖的實現了吧
  • 在實現分佈式鎖之前,我們先設計下分佈式鎖的實現
我們需要什麼樣的分佈式鎖?

(我說一下如果是我們場景下的設計)

  • 首先性能肯定要好,獲取鎖和釋放鎖的性能一定要高
  • 阻塞鎖就可以滿足我們的需求(還有非阻塞,公平鎖等等各種方式)
  • 不能出現死鎖的情況
    然後基於以上的幾點,我們來開發分佈式鎖
使用Redis實現分佈式鎖
  • 增加Redis的Pom文件
        <!--Redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- redis依賴commons-pool 這個依賴一定要添加 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
  • 我們使用Redis的setnx(Set If Not Exists)

  • setnx的解釋:如果指定的key不存在則寫入

  • 在RedisClient中setnx寫入成功返回1,否則返回0

  • 在JavaRedisApi中寫入成功返回True,否則返回False

    • 可以成功寫入代表當前的方法並沒有被其它進程佔用
    • 寫入失敗代表當前的方法正在被其它進程佔用
    • Redis本身是單線程IO多路複用技術,不存在線程安全的問題,所以不用考慮setInx本身線程安全的問題
  • 瞭解了setnx的用法後,我們的思路就是:

    • 獲取鎖:使用setnx在Redis中標記當前方法正在被其它進程佔用(指定Key)
    • 釋放鎖:刪除我們在Redis中的標記(指定Key)
    • 避免死鎖:如果極端情況下,獲取鎖後執行方法異常導致服務掛了,那麼鎖是不會釋放的,有可能會死鎖,所以在獲取鎖後,需要設置過期時間,防止死鎖
  • 如下所示:
    在這裏插入圖片描述

  • 思路整理清楚後,conding就簡單多了

  • 創建RedisUtils工具類

package com.util;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import v1.exceptionding.DingCloverExceptionEnum;
import v1.exceptionding.DingException;
import v1.util.ToolUtil;

import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * @author 孤
 * @version v1.0
 * @Developers 張耀烽
 * @serviceProvider 四葉草安全(SeClover)
 * @description Redis工具
 * @date 2020/1/7
 */
@Component
public class RedisUtils {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * Redis分佈式鎖
     *
     * @return
     */
    public boolean tryLock(String key, String value, long timeout) {
        if (timeout == 0) {
            timeout = 60 * 3;
        }
        boolean isSuccess = redisTemplate.opsForValue().setIfAbsent(key, value);
        //設置過期時間,防止死鎖
        if (isSuccess) {
            redisTemplate.expire(key, timeout, TimeUnit.SECONDS);
        }
        return isSuccess;
    }

    /**
     * Redis 分佈式鎖釋放
     *
     * @param key
     * @param value
     */
    public void unLock(String key, String value) {
        try {
            String currentValue = redisTemplate.opsForValue().get(key);
            if (ToolUtil.isNotEmpty(currentValue) && ToolUtil.equals(currentValue, value)) {
                redisTemplate.opsForValue().getOperations().delete(key);
            }
        } catch (Exception e) {
        //這個是我的自定義異常,你可以刪了
            throw new DingException(DingCloverExceptionEnum.InternalServerError);
        }
    }
}
  • 獲取鎖和釋放鎖的工具寫好後,定義我們的業務代碼
    public void mockLock() {
        RedisUtils redisUtils = SpringContextHolder.getBean(RedisUtils.class);
        InetAddress addr = null;
        try {
            addr = InetAddress.getLocalHost();
        } catch (UnknownHostException e) {
            e.printStackTrace();
        }
        //獲取本機ip
        String ip = addr.getHostAddress();
        //此key存放的值爲任務執行的ip,
        // expire_time 不能設置爲永久,避免死鎖
        boolean lock = redisUtils.tryLock("lock_key", ip, 0);
        if (lock) {
            System.out.println("獲取分佈式鎖成功");
            run();
            //釋放鎖
            redisUtils.unLock("lock_key",ip);
            System.out.println("釋放分佈式鎖成功");
        } else {
            System.out.println("獲得分佈式鎖失敗");
            ip = (String) redisUtils.get(lock_key);
            System.out.println(ip+"正在執行該任務");
            return;
        }
    }

    public void run() throws InterruptedException {
        System.out.println("業務執行中");
        Thread.sleep(60 * 3);
        System.out.println("業務執行結束");
    }

關於鎖的總結

  • 鎖的實現方式和設計有很多方式,Mysql,zookeeper等等都可以,其中的坑也有很多,鎖的兩大設計模式即是 樂觀鎖,悲觀鎖 之後我會抽空把鎖這塊的知識點彙總然後分享
  • 有任何問題可以留言交流!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章