Redis之分布式锁的使用
一、分布式锁
分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁。本篇博客将介绍第二种方式,基于Redis实现分布式锁。
二、分布式锁的演进
业务:电商网站卖东西需要去减库存,本篇文章假设下的订单数量都为1;
第1版的代码:
@Service
public class RedisLockDemo {
@Autowired
private StringRedisTemplate redisTemplate;
public String deduceStock() {
ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
//获取redis中的库存
int stock = Integer.valueOf(valueOperations.get("stock"));
if(stock > 0) {
int newStock = stock - 1;
valueOperations.set("stock", newStock + "");
System.out.println("扣减库存成功, 剩余库存:" + newStock);
}
else {
System.out.println("库存已经为0,不能继续扣减");
}
return "success";
}
}
以上代码在高并发的场景下会产生超卖的问题,所以我们修改一下代码(增加synchronized);
第2版代码:
@Service
public class RedisLockDemo {
@Autowired
private StringRedisTemplate redisTemplate;
public String deduceStock() {
ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
synchronized (this) {
//获取redis中的库存
int stock = Integer.valueOf(valueOperations.get("stock"));
if (stock > 0) {
int newStock = stock - 1;
//减库存
valueOperations.set("stock", newStock + "");
System.out.println("扣减库存成功, 剩余库存:" + newStock);
} else {
System.out.println("库存已经为0,不能继续扣减");
}
}
return "success";
}
}
以上代码在服务为多实例的情况下,还是会出现超卖的问题,这个时候就要引入分布式锁来解决了。
第3版代码:
@Service
public class RedisLockDemo {
@Autowired
private StringRedisTemplate redisTemplate;
public String deduceStock() {
String lockKey = "lockKey";
//加锁: setnx
Boolean isSuccess = redisTemplate.opsForValue().setIfAbsent(lockKey, "1");
if(null == isSuccess || isSuccess) {
System.out.println("服务器繁忙, 请稍后重试");
return "error";
}
//------ 执行业务逻辑 ----start------
// 问题: 代码出现异常,则会造成死锁
int stock = Integer.valueOf(redisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int newStock = stock - 1;
//执行业务操作减库存
redisTemplate.opsForValue().set("stock", newStock + "");
System.out.println("扣减库存成功, 剩余库存:" + newStock);
} else {
System.out.println("库存已经为0,不能继续扣减");
}
//------ 执行业务逻辑 ----end------
//释放锁
redisTemplate.delete(lockKey);
return "success";
}
}
以上代码的问题:
(1)若在执行业务逻辑的过程中出现了异常,则会造成锁不会被释放,使其他有关的线程全部阻塞住(死锁);我们可以把锁释放操作放入到 finally 语句中来解决;
(2)若在执行业务逻辑的过程中服务给挂掉了,仍然会造成锁不会被释放,使其他有关的线程全部阻塞住(死锁);我们可以给 redis 的 key 增加一个超时时间(超过指定的时间则会删除key及其对应的数据),虽然在超时时间到达之前其他有关的线程会一直阻塞住,但是这个时间比较小,且可以解决死锁的问题,所以这个解决方案也是可以接受的。代码如下:
第4版代码:
@Service
public class RedisLockDemo {
@Autowired
private StringRedisTemplate redisTemplate;
public String deduceStock() {
String lockKey = "lockKey";
try {
//加锁: setnx,expire(10秒超时)
Boolean isSuccess = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if(null == isSuccess || isSuccess) {
System.out.println("服务器繁忙, 请稍后重试");
return "error";
}
//------ 执行业务逻辑 ----start------
// 问题:代码出现异常不会造成死锁,但是若锁的过期时间已经到了,但是业务逻辑还没有执行完,会导致锁失效
int stock = Integer.valueOf(redisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int newStock = stock - 1;
//执行业务操作减库存
redisTemplate.opsForValue().set("stock", newStock + "");
System.out.println("扣减库存成功, 剩余库存:" + newStock);
} else {
System.out.println("库存已经为0,不能继续扣减");
}
//------ 执行业务逻辑 ----end------
} finally {
//释放锁
redisTemplate.delete(lockKey);
}
return "success";
}
}
以上代码还是会出现问题:
当线程1的业务执行到一半的时候,设置的锁超时时间到了,则锁的key会被删除;线程2就加锁成功了,线程2还在执行的时候,线程1的业务执行完了,线程1接着执行删除锁的操作,但是线程1删除的锁实际上是线程2加的锁,导致锁失效的问题。
方法一:可以使用 “只要自己加锁,只能自己去释放” 来解决这个问题(第5版代码);
第5版代码:
@Service
public class RedisLockDemo {
@Autowired
private StringRedisTemplate redisTemplate;
public String deduceStock() {
String lockKey = "lockKey";
String clientId = UUID.randomUUID().toString();
try {
//加锁: setnx,expire
Boolean isSuccess = redisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 10, TimeUnit.SECONDS);
if(null == isSuccess || isSuccess) {
System.out.println("服务器繁忙, 请稍后重试");
return "error";
}
//------ 执行业务逻辑 ----start------
int stock = Integer.valueOf(redisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int newStock = stock - 1;
//执行业务操作减库存
redisTemplate.opsForValue().set("stock", newStock + "");
System.out.println("扣减库存成功, 剩余库存:" + newStock);
} else {
System.out.println("库存已经为0,不能继续扣减");
}
//------ 执行业务逻辑 ----end------
} finally {
if(clientId.equals(redisTemplate.opsForValue().get(lockKey))) {
//释放锁
redisTemplate.delete(lockKey);
}
}
return "success";
}
}
以上代码虽然解决了锁被其他线程释放的问题,但是还是会出现问题;当前线程的业务还没有执行完,锁的超时时间到了,这样其他线程就可以去加锁并执行业务逻辑了,这样就有两个线程都在执行了,有可能导致bug。
方法二:可以给锁进行续命,每次锁快超时的时候就给锁重新在设置一个时间(引入另一个redis的java客户端 Redisson)
三、分布式锁的Redisson实现
Redisson 的分布式锁
Jedis和Redisson的比较
Jedis提供了比Redisson更丰富的操作;
Redisson底层多使用 lua 脚本实现,对原子性的操作封装较好,尤其是在分布式锁上的封装;
Redis实现的分布式锁还会出现一点问题:
线程1加了锁去执行业务了,此时Redis的 master 挂掉了,还没有将数据同步到 slave 上。因为集群会选举一个新的 master 出来,但是新的 master 上并没有这个锁;线程2可以在新选举产生的 master 上去加锁,然后处理业务。
(1)针对以上问题,我们可以使用 zookeeper 去实现分布式锁,因为它是强一致性的。但是zookeeper的性能是低于Redis,使用Redis是完全够了。
(2)当然,对于以上的问题,我们也可以使用 RedLock 去解决Redis上的那个问题,RedLock 实现的原理:给多个Redis节点发送加锁的消息,只有超过一半以上的节点加锁成功才算加锁成功。
但是不推荐使用RedLock,当前的 RedLock 是有bug的,它的实现原理和 zookeeper 是差不多的。
高并发的高性能的Redis
怎么在高并发的场景去实现一个高性能的分布式锁呢?
电商网站在大促的时候并发量很大:
(1)若抢购不是同一个商品,则可以增加Redis集群的cluster来实现,因为不是同一个商品,所以通过计算 key 的hash会落到不同的 cluster上;
(2)若抢购的是同一个商品,则计算key的hash值会落同一个cluster上,所以加机器也是么有用的。
我们可以使用库存分段锁的方式去实现。
分段锁
假如产品1有200个库存,我们可以将这200个库存分为10个段存储(每段20个),每段存储到一个cluster上;将key使用hash计算,使这些key最后落在不同的cluster上。
每个下单请求锁了一个库存分段,然后在业务逻辑里面,就对数据库或者是Redis中的那个分段库存进行操作即可,包括查库存 -> 判断库存是否充足 -> 扣减库存。
可以参照 ConcurrentHashMap 的源码去实现,它使用的就是分段锁。
高性能分布式锁参考链接:https://blog.csdn.net/eluanshi12/article/details/84616173