一、前言
作爲後臺開發,相信大家都對 Redis 並不陌生了。Redis
有三個客戶端 Jedis
、Redisson
、Lettuce
。也就是提供基本的驅動來連接操作 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 的幾行代碼就可以實現我們前面所擔心的那些問題了。