Redis 分布式锁
概述
单机架构下,一个进程中的多个线程竞争同一共享资源时,通常使用 JVM 级别的锁即可保证互斥,以对商品下单并扣库存为例:
public String deductStock() {
synchronized (this){
// 获取库存值
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "")
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
}
return "end";
}
然而,当使用分布式架构时,这种方式就不管用了,因为 JVM 锁只能控制自家应用,其他机器的应用时管不了的,这时候分布式锁就派上用场了,它能保证分布式系统下不同进程对共享资源访问的互斥性
案例分析
下面对使用 Redis 实现分布式锁的案例进行分析:
1. Case1
使用 Redis 中的 setnx()
设计一个入门级别的分布式锁
public String deductStock1() {
String localKey = "lock:product:0001";
Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(localKey, "true");
if (!aBoolean){
return "当前系统繁忙";
}
try {
// 获取库存值
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
} finally {
// 即使中间的任何一处逻辑抛出异常,也能保证锁释放
stringRedisTemplate.delete(localKey);
}
return "end";
}
存在的问题:锁没有释放,机器却宕机了,这时候其他机器将无法获取锁
2. Case2
设置一个过期时间,解决 Case1 中存在的宕机没有释放锁的问题
public String deductStock2() {
String localKey = "lock:product:0001";
Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(localKey, "true");
stringRedisTemplate.expire(localKey,10,TimeUnit.SECONDS);
if (!aBoolean){
return "当前系统繁忙";
}
try {
// 获取库存值
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
} finally {
stringRedisTemplate.delete(localKey);
}
return "end";
}
存在的问题:有可能还没有执行到 expire()
就宕机了,没有保证原子性
3. Case3
在加锁时就设置超时时间,保证加锁和设置超时时间是原子操作
public String deductStock3() {
String localKey = "lock:product:0001";
// 这条命令能够保证原子性
Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(localKey, "true",10,TimeUnit.SECONDS);
if (!aBoolean){
return "当前系统繁忙";
}
try {
// 获取库存值
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
} finally {
stringRedisTemplate.delete(localKey);
}
return "end";
}
存在问题:如果系统并发量不是特别的大,那么问题不大,但如果并发量很大,就会出现严重的并发问题:
- 假设线程 A 的时间超过了超时时间,锁失效了,此时该线程 A 还没有执行 delete 方法
- 线程 B 这时候加锁成功了,与此同时线程 A 执行了 delete 方法,但是这时候线程 A 释放的锁是线程 B 的
- 于是极端情况下就会出现:线程 A 释放线程 B 的锁,B 释放 C 的,C 释放 D 的 ......
4. Case4
Case3 存在的问题的根本原因就是在执行 delete 方法的时候,自己的锁被其他的线程释放了,所以解决办法就是给每个线程生成一个唯一 ID,在最后释放锁的时候判断是否是自己的锁,如果是自己的才释放
public String deductStock4() {
String localKey = "lock:product:0001";
String uuid = UUID.randomUUID().toString();
// 这条命令能够保证原子性
Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(localKey, uuid,10,TimeUnit.SECONDS);
if (!aBoolean){
return "当前系统繁忙";
}
try {
// 获取库存值
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
} finally {
if (uuid.equals(stringRedisTemplate.opsForValue().get(localKey))){
stringRedisTemplate.delete(localKey);
}
}
return "end";
}
存在问题:存在原子性问题,问题代码如下:
if (uuid.equals(stringRedisTemplate.opsForValue().get(localKey))){
stringRedisTemplate.delete(localKey);
}
有可能出现当前线程执行完 if 判断却还没执行 delete 操作的时候当前锁过期了,于是又会出现当前线程释放了其他线程的锁的情况
5. Case5
对于 Case4 的问题,本质是 「判断是不是当前线程加的锁」和「释放锁」不是一个原子操作,可以用 Lua 脚本代替,Redis 会将整个脚本作为一个整体执行
String redisScript = "
if redis.call('get',KEYS[1]) == ARGV[1] then
return redis.call('del',KEYS[1])
else
return 0
end;"
public String deductStock5() {
String localKey = "lock:product:0001";
String uuid = UUID.randomUUID().toString();
// 这条命令能够保证原子性
Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(localKey, uuid,10,TimeUnit.SECONDS);
if (!aBoolean){
return "当前系统繁忙";
}
try {
// 获取库存值
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
} finally {
redisTemplate.execute(redisScript, Arrays.asList(localKey), uuid);
}
return "end";
}
也可以使用锁续命的方式解决,即创建一个守护线程,每过一段时间,判断业务的主线程有没有结束(是否还加着锁),如果还加着锁,将锁的超时时间重新设置
public String deductStock5() {
String localKey = "lock:product:0001";
String uuid = UUID.randomUUID().toString();
// 这条命令能够保证原子性
Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(localKey, uuid,10,TimeUnit.SECONDS);
if (!aBoolean){
return "当前系统繁忙";
} else {
// 续命
Thread demo = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
Boolean expire = redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
// 有可能已经主动删除key,不需要在续命
if(!expire){
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
demo.setDaemon(true);
demo.start();
}
try {
// 获取库存值
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
} finally {
if (uuid.equals(stringRedisTemplate.opsForValue().get(localKey))){
stringRedisTemplate.delete(localKey);
}
}
return "end";
}
Redisson 实现分布式锁
Redisson 提供了使用 Redis 的简单便捷的方法,意在促进使用者从对 Redis 的关注分离,将精力集中在业务逻辑
通常我们会为 Redis 分布式锁设置过期时间,如果持锁线程在锁过期时还没完成业务执行,则有可能引发问题。Redisson 提供了看门狗机制,作用是在持锁线程被关闭前,不断延长锁的有效期,确保锁不会因为超时而被释放。默认情况下,看门狗每隔 10s 为锁续期 30s。Redisson 通过内部调度器实现看门狗机制,每当成功获取一个锁,就会创建一个定时任务,定期延长锁的过期时间
SpringBoot 集成 Redisson 步骤如下:
引入 Maven 依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.15.5</version>
</dependency>
自定义配置类
@Configuration
public class MyRedissonConfig {
@Bean(destroyMethod="shutdown") // 服务停止后调用 shutdown 方法。
public RedissonClient redisson() throws IOException {
// 1.创建配置
Config config = new Config();
// 集群模式
// config.useClusterServers().addNodeAddress("127.0.0.1:7004", "127.0.0.1:7001");
// 2.根据 Config 创建出 RedissonClient 示例。
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
return Redisson.create(config);
}
}
代码实现
public void test() throws Exception{
RLock lock = redissonClient.getLock("guodong");
lock.lock();
// 尝试拿锁 10s 后停止重试,返回false,具有Watch Dog 自动延期机制,默认续30s
boolean res1 = lock.tryLock(10, TimeUnit.SECONDS);
// 没有 Watch Dog ,10s后自动释放
lock.lock(10, TimeUnit.SECONDS);
// 尝试拿锁 100s 后停止重试,返回false 没有Watch Dog ,10s后自动释放
boolean res2 = lock.tryLock(100, 10, TimeUnit.SECONDS);
Thread.sleep(40000L);
lock.unlock();
}
Red Lock
Redis 一般以集群模式部署,从而保证高可用。集群模式下使用分布式锁可能会出现问题,比如客户端 A 从 master 获得锁,但 master 在向 slave 同步之前挂掉了,slave 晋升为 master,客户端 B 便可以再次获得锁
RedLock 是对集群的每个节点进行加锁,如果大多数节点,才会认为加锁成功。RedLock 算法的工作流程大致如下:
- 客户端向多个独立的 Redis 实例尝试获取锁
- 如果客户端能在大部分节点上成功获取锁,并且所花费的时间小于锁的过期时间的一半,那么认为客户端成功获取到了分布式锁
使用 Redisson 可以很方便的实现 RedLock
public class RedLockDemo {
public static void main(String[] args) {
// 创建 Redisson 客户端配置
Config config = new Config();
// 有三个 Redis 节点
config.useClusterServers()
.addNodeAddress("redis://127.0.0.1:6379",
"redis://127.0.0.1:6380",
"redis://127.0.0.1:6381");
// 创建 Redisson 客户端实例
RedissonClient redissonClient = Redisson.create(config);
// 创建 RedLock 对象
RedissonRedLock redLock = redissonClient.getRedLock("resource");
try {
// 尝试获取分布式锁,最多尝试 5 秒获取锁,并且锁的有效期为 5000 毫秒
boolean lockAcquired = redLock.tryLock(5, 5000, TimeUnit.MILLISECONDS);
if (lockAcquired) {
// 加锁成功,执行业务代码...
} else {
System.out.println("Failed to acquire the lock!");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println("Interrupted while acquiring the lock");
} finally {
// 无论是否成功获取到锁,在业务逻辑结束后都要释放锁
if (redLock.isLocked()) {
redLock.unlock();
}
// 关闭 Redisson 客户端连接
redissonClient.shutdown();
}
}
}
Redisson 中的 RedLock 是基于 RedissonMultiLock(联锁)实现的。RedissonMultiLock 是 Redisson 提供的一种分布式锁类型,它可以同时操作多个锁,以达到对多个锁进行统一管理的目的。联锁的操作是原子性的,即要么全部锁住,要么全部解锁。RedLock 实际上是使用 RedissonMultiLock 对集群的每个节点进行加锁
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战