手把手教你 SpringBoot 分佈式鎖的實現

你知道的越多,不知道的就越多,業餘的像一棵小草!

你來,我們一起精進!你不來,我和你的競爭對手一起精進!

編輯:業餘草

來源:https://urlify.cn/632yIv

推薦:https://www.xttblog.com/?p=5035

前言

前段時間面試了一個高級程序員,問他了一些分佈式鎖的知識,回答的還不錯。後來進了我們的團隊,剛好讓他接手一個新項目,需要用到分佈式鎖,我說這是你的強項,你來實現。

誰知,項目上線後,各種問題接連爆發出來。後來我找他談話,主要是說,千萬不要重複造輪子,直接吧 A 項目中的 Redisson 用法實現複製過來一份不就行了,幹嘛非要自己搞一套,還搞出問題。。。

今天,我們一起來手把手來實現一個高效的 SpringBoot 分佈式鎖!本文通過 Spring Boot 整合 redisson 來實現分佈式鎖,並結合 demo 測試結果。

分析設計要點

當我們在設計分佈式鎖的時候,我們應該考慮分佈式鎖至少要滿足的一些條件,同時考慮如何高效的設計分佈式鎖,這裏我認爲以下幾點是必須要考慮的。

1、互斥

在分佈式高併發的條件下,我們最需要保證,同一時刻只能有一個線程獲得鎖,這是最基本的一點。

2、防止死鎖

在分佈式高併發的條件下,比如有個線程獲得鎖的同時,還沒有來得及去釋放鎖,就因爲系統故障或者其它原因使它無法執行釋放鎖的命令,導致其它線程都無法獲得鎖,造成死鎖。

所以分佈式非常有必要設置鎖的有效時間,確保系統出現故障後,在一定時間內能夠主動去釋放鎖,避免造成死鎖的情況。

3、性能

對於訪問量大的共享資源,需要考慮減少鎖等待的時間,避免導致大量線程阻塞。

所以在鎖的設計時,需要考慮兩點。

1、鎖的顆粒度要儘量小。比如你要通過鎖來減庫存,那這個鎖的名稱你可以設置成是商品的ID,而不是任取名稱。這樣這個鎖只對當前商品有效,鎖的顆粒度小。

2、鎖的範圍儘量要小。比如只要鎖2行代碼就可以解決問題的,那就不要去鎖10行代碼了。

4、重入

我們知道ReentrantLock是可重入鎖,那它的特點就是:同一個線程可以重複拿到同一個資源的鎖。重入鎖非常有利於資源的高效利用。關於這點之後會做演示。

針對以上Redisson都能很好的滿足,下面就來使用它。

首先看下常規Redis鎖的處理圖

代碼實現

添加依賴

<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--redisson-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.10.6</version>
</dependency>

配置信息

spring:
# redis
  redis:
    host: 47.103.5.190
    port: 6379
    jedis:
      pool:
# 連接池最大連接數(使用負值表示沒有限制)
        max-active: 100
# 連接池中的最小空閒連接
        max-idle: 10
# 連接池最大阻塞等待時間(使用負值表示沒有限制)
        max-wait: -1
# 連接超時時間(毫秒)
      timeout: 5000
#默認是索引爲0的數據庫
      database: 0

配置類

/**
 * redisson 配置,下面是單節點配置:
 *
 * @author gourd
 */
@Configuration
publicclassRedissonConfig{
@Value("${spring.redis.host}")
privateString host;
@Value("${spring.redis.port}")
privateString port;
@Value("${spring.redis.password:}")
privateString password;
@Bean
publicRedissonClient redissonClient() {
Config config = newConfig();
//單節點
        config.useSingleServer().setAddress("redis://"+ host + ":"+ port);
if(StringUtils.isEmpty(password)) {
            config.useSingleServer().setPassword(null);
} else{
            config.useSingleServer().setPassword(password);
}
//添加主從配置
// config.useMasterSlaveServers().setMasterAddress("").setPassword("").addSlaveAddress(new String[]{"",""});
// 集羣模式配置 setScanInterval()掃描間隔時間,單位是毫秒, //可以用"rediss://"來啓用SSL連接
// config.useClusterServers().setScanInterval(2000).addNodeAddress("redis://127.0.0.1:7000", "redis://127.0.0.1:7001").addNodeAddress("redis://127.0.0.1:7002");
returnRedisson.create(config);
}
}

Redisson 工具類

/**
 * redis分佈式鎖幫助類
 *
 * @author gourd
 *
 */
publicclassRedisLockUtil{
privatestaticDistributedLocker distributedLocker = SpringContextHolder.getBean("distributedLocker",DistributedLocker.class);
/**
     * 加鎖
     * @param lockKey
     * @return
     */
publicstaticRLocklock(String lockKey) {
return distributedLocker.lock(lockKey);
}
/**
     * 釋放鎖
     * @param lockKey
     */
publicstaticvoid unlock(String lockKey) {
        distributedLocker.unlock(lockKey);
}
/**
     * 釋放鎖
     * @param lock
     */
publicstaticvoid unlock(RLocklock) {
        distributedLocker.unlock(lock);
}
/**
     * 帶超時的鎖
     * @param lockKey
     * @param timeout 超時時間   單位:秒
     */
publicstaticRLocklock(String lockKey, int timeout) {
return distributedLocker.lock(lockKey, timeout);
}
/**
     * 帶超時的鎖
     * @param lockKey
     * @param unit 時間單位
     * @param timeout 超時時間
     */
publicstaticRLocklock(String lockKey, int timeout,TimeUnit unit ) {
return distributedLocker.lock(lockKey, unit, timeout);
}
/**
     * 嘗試獲取鎖
     * @param lockKey
     * @param waitTime 最多等待時間
     * @param leaseTime 上鎖後自動釋放鎖時間
     * @return
     */
publicstaticboolean tryLock(String lockKey, int waitTime, int leaseTime) {
return distributedLocker.tryLock(lockKey, TimeUnit.SECONDS, waitTime, leaseTime);
}
/**
     * 嘗試獲取鎖
     * @param lockKey
     * @param unit 時間單位
     * @param waitTime 最多等待時間
     * @param leaseTime 上鎖後自動釋放鎖時間
     * @return
     */
publicstaticboolean tryLock(String lockKey, TimeUnit unit, int waitTime, int leaseTime) {
return distributedLocker.tryLock(lockKey, unit, waitTime, leaseTime);
}
/**
     * 獲取計數器
     *
     * @param name
     * @return
     */
publicstaticRCountDownLatch getCountDownLatch(String name){
return distributedLocker.getCountDownLatch(name);
}
/**
     * 獲取信號量
     *
     * @param name
     * @return
     */
publicstaticRSemaphore getSemaphore(String name){
return distributedLocker.getSemaphore(name);
}
}

底層封裝

/**
 * @author gourd
 */
publicinterfaceDistributedLocker{
RLocklock(String lockKey);
RLocklock(String lockKey, int timeout);
RLocklock(String lockKey, TimeUnit unit, int timeout);
boolean tryLock(String lockKey, TimeUnit unit, int waitTime, int leaseTime);
void unlock(String lockKey);
void unlock(RLocklock);
}
/**
 * @author gourd
 */
@Component
publicclassRedisDistributedLockerimplementsDistributedLocker{
@Autowired
privateRedissonClient redissonClient;
@Override
publicRLocklock(String lockKey) {
RLocklock= redissonClient.getLock(lockKey);
lock.lock();
returnlock;
}
@Override
publicRLocklock(String lockKey, int leaseTime) {
RLocklock= redissonClient.getLock(lockKey);
lock.lock(leaseTime, TimeUnit.SECONDS);
returnlock;
}
@Override
publicRLocklock(String lockKey, TimeUnit unit ,int timeout) {
RLocklock= redissonClient.getLock(lockKey);
lock.lock(timeout, unit);
returnlock;
}
@Override
publicboolean tryLock(String lockKey, TimeUnit unit, int waitTime, int leaseTime) {
RLocklock= redissonClient.getLock(lockKey);
try{
returnlock.tryLock(waitTime, leaseTime, unit);
} catch(InterruptedException e) {
returnfalse;
}
}
@Override
publicvoid unlock(String lockKey) {
RLocklock= redissonClient.getLock(lockKey);
lock.unlock();
}
@Override
publicvoid unlock(RLocklock) {
lock.unlock();
}
}

測試

模擬併發測試

/**
 * redis分佈式鎖控制器
 * @author gourd
 * @since 2019-07-30
 */
@RestController
@Api(tags = "redisson", description = "redis分佈式鎖控制器")
@RequestMapping("/redisson")
@Slf4j
publicclassRedissonLockController{
/**
     * 鎖測試共享變量
     */
privateInteger lockCount = 10;
/**
     * 無鎖測試共享變量
     */
privateInteger count = 10;
/**
     * 模擬線程數
     */
privatestaticint threadNum = 10;
/**
     * 模擬併發測試加鎖和不加鎖
     * @return
     */
@GetMapping("/test")
@ApiOperation(value = "模擬併發測試加鎖和不加鎖")
publicvoidlock(){
// 計數器
finalCountDownLatch countDownLatch = newCountDownLatch(1);
for(int i = 0; i < threadNum; i ++) {
MyRunnable myRunnable = newMyRunnable(countDownLatch);
Thread myThread = newThread(myRunnable);
            myThread.start();
}
// 釋放所有線程
        countDownLatch.countDown();
}
/**
     * 加鎖測試
     */
privatevoid testLockCount() {
String lockKey = "lock-test";
try{
// 加鎖,設置超時時間2s
RedisLockUtil.lock(lockKey,2, TimeUnit.SECONDS);
            lockCount--;
            log.info("lockCount值:"+lockCount);
}catch(Exception e){
            log.error(e.getMessage(),e);
}finally{
// 釋放鎖
RedisLockUtil.unlock(lockKey);
}
}
/**
     * 無鎖測試
     */
privatevoid testCount() {
        count--;
        log.info("count值:"+count);
}
publicclassMyRunnableimplementsRunnable{
/**
         * 計數器
         */
finalCountDownLatch countDownLatch;
publicMyRunnable(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
publicvoid run() {
try{
// 阻塞當前線程,直到計時器的值爲0
                countDownLatch.await();
} catch(InterruptedException e) {
                log.error(e.getMessage(),e);
}
// 無鎖操作
            testCount();
// 加鎖操作
            testLockCount();
}
}
}

調用接口後打印值:

測試結果

根據打印結果可以明顯看到,未加鎖的 count-- 後值是亂序的,而加鎖後的結果和我們預期的一樣。

本文只是實戰項目中複製出來的部分代碼,實際使用中封裝成了 jar,內部系統只需要引用即可。最後,希望對大家有幫助。如有錯誤之處,歡迎指正!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章