Redis分布式锁
1.分布式锁的作用
1.1 什么是分布式锁
分布式锁是控制分布式系统或不同系统之间共同访问共享资源的一种锁实现,如果不同的系统或同一个系统的不同主机之间共享了某个资源时,往往需要互斥来防止彼此干扰保证一致性。 一个相对安全的分布式锁,一般需要具备以下特征: 互斥性。 互斥是锁的基本特征,同一时刻锁只能被一个线程持有,执行临界区操作。
以上来自维基百科
1.2 分布式锁的作用
以我平时写代码的感受来讲,分布式锁的作用就是在高并发场景下对数据库或缓存的操作,保证一个请求不会受到另一请求影响,确保同一请求中数据的一致性,避免出现并发安全问题。
1.2.1 并发安全问题示例
下面这段代码就存在严重的并发安全问题。如果第一个请求执行较慢只查询到了库存但还未扣减库存,且此时第二个请求开始执行并且执行成功扣减库存后第一个请求才开始扣减库存,则第一个请求设置的库存值没考虑到第二个请求已经执行完成,就相当于第二个请求的商品已经卖出但并未扣减库存,而造成超卖问题。
/**
* 扣减库存
* @return end
*/
@GetMapping("deduct_stock")
public String deductStock() {
String lockKey = "lock:product_101";
// 查询库存
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0) {
// 库存减1,并重新设置库存数
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
// 库存不足
System.out.println("扣减失败,库存不足");
}
return "end";
}
2. 如何解决并发安全问题
解决并发安全问题的方式有很多,如Jdk
中的synchronized
关键字,原生Redis
中的SETNX
命令和分布式锁框架Redisson
。
2.1 初始化
2.1.1 导包
<!--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.17.5</version>
</dependency>
2.1.2 配置
@Bean
public Redisson redisson(){
// 单机模式
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379").setDatabase(0);
// 设置分布式锁watch dog超时时间
// config.setLockWatchdogTimeout(10000);
return (Redisson) Redisson.create(config);
}
2.1.3 注入
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private Redisson redisson;
2.2 synchronized
synchronized (this){
if (stock > 0) {
// 库存减1,并重新设置库存数
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
// 库存不足
System.out.println("扣减失败,库存不足");
}
}
还是上面的案例,只是在减库存的代码部分加上了同步代码块,这样就能保证不同线程执行到减库存的逻辑时能够串行执行。
不足:
- 只对单
Jvm
进程有效,实际生活中往往是集群部署,当请求在不同Jvm
进程或者不同服务器时就会失效。- …
2.3 SETNX
Redis Setnx(SET if Not eXists) 命令在指定的 key 不存在时,为 key 设置指定的值。
SETNX KEY_NAME VALUE
2.3.1 代码演示
注意:
- 只使用
SETNX
命令,如服务器意外宕机的特殊情况下,有可能会造成锁无法被释放,所以需要使用EXPIRE
命令设置过期时间。为保证SETNX
、EXPIRE
两条命令的原子性,建议使用setIfAbsent
方法。- 为保证锁的释放,建议使用
try/finally
代码块。- 为防止删除其他请求的锁,建议使用把
SETNX
命令的value
设置为全局唯一的UUID
,并且在删除时判断是否为本线程生成的,保证只能删除自己加的锁。 因为可能某一个请求执行时间较长,没有删除锁而是过期释放的,而此时其他请求进入然后进行加锁,此时前一个请求刚好执行到删除锁的逻辑,把新请求加的锁给删除了。
/**
* 扣减库存
* @return end
*/
@GetMapping("deduct_stock")
public String deductStock() {
String lockKey = "lock:product_101";
// 防止删除其他请求的锁
// 可能某一个请求执行时间较长,没有删除锁而是过期释放的,
// 而此时其他请求进入加锁,此时前一个请求刚好执行到删除锁的逻辑,把新请求加的锁给删除了
// 解决方案:锁续命。可用redisson框架
// 标识这把锁是谁加的
String clientId = UUID.randomUUID().toString();
// 设置超时时间,防止无法释放锁
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 10, TimeUnit.SECONDS);
if (Boolean.FALSE.equals(lock)) {
return "error_code";
}
// try/finally 保证锁的释放
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 (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
// 需要判断这把锁是谁加的,只能删除自己加的锁
stringRedisTemplate.delete(lockKey);
}
}
return "end";
}
不足:
- 如果一个请求执行时间较长,在锁释放时还未执行完成,此时下个请求也能执行,存在隐患(解决方式:锁续命)。
- …
2.4 Redisson
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。
使用Redisson
能轻松实现分布式锁
/**
* 扣减库存
* @return end
*/
@GetMapping("deduct_stock")
public String deductStock() {
String lockKey = "lock:product_101";
RLock redissonLock = redisson.getLock(lockKey);
redissonLock.lock();
// try/finally 保证锁的释放
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 {
redissonLock.unlock();
}
return "end";
}
注意:
- 2.3的
SETNX
方式的思想就是仿照的Redisson
的加锁和释放锁,同时Redisson
已经实现了锁续命,解决了2.3的不足。不足:
- …
3. 总结
- 我们以后在编写分布式锁代码的过程中直接使用
Redisson
即可,简洁高效。 - 此篇为单节点
Redis
的设计。在Redis
的主从架构中,当主节点宕机时还会有锁丢失的可能,解决方式:多节点集群模式设置红锁等。性能优化:设置分段锁,如将key设计为lock:product_101_section_1、lock:product_101_section_2…。