分佈式架構下基於Redisson實現Redis分佈式鎖

一、前言

作爲後臺開發,相信大家都對 Redis 並不陌生了。Redis 有三個客戶端 JedisRedissonLettuce。也就是提供基本的驅動來連接操作 Redis 數據庫的。我們先簡單介紹下這幾個客戶端的異同。

  • Jedis:是Redis 的 Java 實現客戶端,提供了比較全面的 Redis 命令的支持。
  • Redisson:實現了分佈式和可擴展的 Java 數據結構。
  • Lettuce:高級 Redis 客戶端,用於線程安全同步,異步和響應使用,支持集羣,Sentinel,管道和編碼器。

優點:

  • Jedis:比較全面的提供了 Redis 的操作特性
  • Redisson:促使使用者對 Redis 的關注分離,提供很多分佈式相關操作服務,例如,分佈式鎖分佈式集合,可通過 Redis 支持延遲隊列。
  • Lettuce:主要在一些分佈式緩存框架上使用比較多。

對比後不難發現,Redisson 實現 Redis 分佈式鎖是比較好的。那爲啥不是其他兩個呢?不着急,我們下面一一道來。小夥伴們應該接觸 Jedis 會比較多吧,因爲它出現的時間比較長了。

但是隨着現代系統的多核和異步,爲了不斷提高的吞吐量,異步非阻塞線程模型大行其道,這裏面非常熱門的框架就是 Netty,Netty 因其設計優秀,應用面廣,實際使用的場景廣泛,很多大型框架比如 Hadoop、Dubbo 等許多的底層都是通過Netty來實現的通信。所以我們就專門針對異步的且基於 Netty 的 Redis 驅動來分析,Redisson 和 Lettuce 都是基於 Netty 的也就是說他倆都是異步非阻塞的,但是他們有什麼區別呢?其實在使用語法上面有一些區別,Redisson對結果做了一層包裝,通過包裝類來進行一些額外的操作來達到異步操作,並且 Redisson 提供了額外的分佈式鎖功能,那我們接下來就來說說 Redisson 的使用方式吧。

二、代碼實現

在大廠中,經常有高併發場景。我就以下單減庫存場景爲例來模擬下怎麼實現 Redis 分佈式鎖。

@Controller
public class RedisLockController {

    private static final Logger LOGGER = LoggerFactory.getLogger(RedisLockController.class);

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @RequestMapping("/deduct_stock")
    @Transactional
    public ApiResult deductStock() {
        String lockKey = "lockKey";
        String clientId = UUID.randomUUID().toString();
        try {
            Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS);
            if (!flag) return new ApiResult(ReturnEnum.FAILED, "Not the same lock");
            int stockNum = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            if (stockNum > 0) {
                int realStockNum = stockNum - 1;
                stringRedisTemplate.opsForValue().set("stock", realStockNum + "");
                LOGGER.info("扣減成功,剩餘庫存:{}", realStockNum);
            } else {
                LOGGER.info("扣減失敗,庫存不足");
            }
        } finally {
            if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
                stringRedisTemplate.delete(lockKey);
            }
        }
        return new ApiResult();
    }
}

我這裏只是簡單寫了一個 RedisLockController 來模擬高併發場景實現Redis分佈式鎖,實際中的項目業務遠遠複雜的多哈。

不要小看以上幾行代碼,包含了很多場景在裏面。

1、synchronized

我們通常都喜歡用 synchronized 來加鎖,這是 jvm 在單機上才能這樣操作,分佈式中行不通,高併發場景依舊會出現兩個線程共搶到這把鎖。所以要用我上面實現的 Redis分佈式鎖。

2、finally 語句塊的代碼

if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
    stringRedisTemplate.delete(lockKey);
}

這個是爲了防止不是同一線程操作刪除的,不這樣加個 clientId 唯一標識那條線程的話,那個鎖永遠不會生效。

3、Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS);

其實這行代碼包含了以下兩行代碼:

Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "riemann");
stringRedisTemplate.expire(lockKey, 30, TimeUnit.SECONDS);

還有這裏爲什麼要設置 30s 的超時呢?

因爲怕整個應用宕機了,像有的時候運維直接 kill -9 或者應用沒有做容災突然斷電了,導致應用掛掉。這裏設置了 30s 後鎖直接過期,這樣可以解決應用掛掉導致沒有這個鎖,後面的其他請求都直接是這個鎖導致程序出Bug。

所以對於高併發的業務一定要考慮到各種場景,不然一不小心就會出Bug。

下面我來介紹下基於 Redisson 實現 Redis 分佈式鎖。Redisson 底層封裝了 Luna 腳本來實現的分佈式鎖,直接拿來用,放心,很多大廠用了那麼多年了,Redisson 有的 Bug 基本修復的基本沒有了。這樣一來,也讓我們開發人員省心了很多,也不至於一不小心寫了有 Bug 的分佈式鎖業務。一旦涉及到了客戶的投訴、對公司造成了影響,輕則 KPI 沒有了,重則直接叫你走人。

三、基於Redisson實現Redis分佈式鎖

Talk is cheap, show me the code.

1、Maven 依賴

<!--redis分佈式鎖-->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.12.3</version>
</dependency>
<!--redis-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2、RedissonConfig.java

@Configuration
public class RedissonConfig {
    @Bean(destroyMethod = "shutdown")
    public RedissonClient redisson() throws IOException {
        Config config = new Config();
        // 此爲單機模式
        config.useSingleServer().setAddress("redis://localhost:6379");
        return Redisson.create(config);
    }
}

3、RedisLockController.java

@Controller
public class RedisLockController {

    private static final Logger LOGGER = LoggerFactory.getLogger(RedisLockController.class);

    @Resource
    private RedissonClient redissonClient;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @RequestMapping("/deduct_stock_2")
    @Transactional
    public ApiResult deductStock() {
        String lockKey = "lockKey";
        RLock rLock = redissonClient.getLock(lockKey);
        try {
            rLock.lock();
            int stockNum = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            if (stockNum > 0) {
                int realStockNum = stockNum - 1;
                stringRedisTemplate.opsForValue().set("stock", realStockNum + "");
                LOGGER.info("扣減成功,剩餘庫存:{}", realStockNum);
            } else {
                LOGGER.info("扣減失敗,庫存不足");
            }
        } finally {
            rLock.unlock();
        }
        return new ApiResult();
    }
}

這樣我們用的 Redisson 的幾行代碼就可以實現我們前面所擔心的那些問題了。

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