// 下述代码是逐步完善的
@RestController
public class GoodController{
private static final String REDIS_LOCK = "redisLock"; // 锁的名称
@Autowired
private StringRedisTemplate stringRedisTemplate;
@GetMapping("/buy")
public String buy_goods(){
try{
// 作为客户端的唯一固定标识
String value = UUID.randomUUID().toString()+Thread.currentThread().getName();
//setIfAbsent 相当于redis中setNX,不存在,就建锁
// 将加锁操作和设置过期时间操作合并在一起,避免问题2的出现
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK,value,10L,TimeUnit.SECONDS);
// 添加过期时间,这里是10秒过后,如果没有释放锁,则会自动删除,避免问题1的出现
// 在执行完上一行setIfAbsent操作后再添加过期时间,无法执行原子性,因此得修改
// stringRedisTemplate.expire(REDIS_LOCK,10L,TimeUnit.SECONDS);
// 建锁失败
if(!flag){
return "抢锁失败";
}
// 建锁成功
String result = stringRedisTemplate.opsForValue().get("goods:001"); // 从redis中取值
int goodNumber = result == null ? 0:Integer.parseInt(result);
int realNumer = goodNumber -1;
stringRedisTemplate.opsForValue().set("goods:001",String.valueOf(realNumer)); // 修改redis中的值
}finally{
// 用完后要解锁
// 由于在上述代码中可能会出现异常,导致无法走到释放锁这步,也就无法释放锁,因此必须在代码层面添加finally释放锁
// 需要判断当前锁是不是自己的,避免问题3的出现
// 判断加锁与解锁是不是同一个客户端
/*
// 判断当前客户端的唯一标识与redis分布式锁中持有的客户端标识是否相同,相同才能解锁
if (stringRedisTemplate.opsForValue().get(key).equals(value)){
// 如果在此时,这把锁突然不是这个客户顿的,则会无解锁
stringRedisTemplate.delete(REDIS_LOCK);
}
*/
// 经两行代码换成下述代码,避免问题4的出现
while(true){
// 监控锁,如果在这个过程中有其他线程掺和进来了,则进行第二次,直到删除
// watch命令就是标记一个键,如果标记了一个键,在提交事务前如果该键被别人修改过,那么事务就会失败,这种情况下通常可以在程序中重新再尝试一次
stringRedisTemplate.watch(REDIS_LOCK);
if(stringRedisTemplate.opsForValue().get(REDIS_LOCK).equalsIgnoreCase(value)){
stringRedisTemplate.setEnableTransactionSupport(true); // 是否支持事务
stringRedisTemplate.multi(); // 开启事务
stringRedisTemplate.delete(REDIS_LOCK); // 删除锁
List<Object> list = stringRedisTemplate.exec(); // 执行,返回队列
// 为空说明有其他线程掺和,需要再次进行
if (list == null){
continue;
}
// 解锁,退出监控
stringRedisTemplate.unwatch();
break;
}
}
}
}
}
如果出现以下问题1:部署了微服务jar包的机器挂了,代码层面根本没有走到finally这块
解决办法:没有办法保证解锁,这 个key没有被删除,需要加入一个过期时间限定key
stringRedisTemplate.expire(REDIS_LOCK,10L,TimeUnit.SECONDS);
在问题1的基础上存在问题2:单独设置的过期时间不具有原子性
解决办法:将建锁设置key操作和设置过期时间合并成同一行
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK,value,10L,TimeUnit.SECONDS);
在问题2的基础上存在问题3:删除了其他线程的锁
这是由于当前线程A执行的时间超过了设置的过期时间,导致redis删除了线程A的锁,此时线程A并没有结束,但由于没有了锁,因此其他线程B就可以进入程序,重新加锁,当A继续执行,走到finally去释放锁时,释放的却是线程B的锁
解决办法:线程只能删除自己的锁,添加判断,判断当前锁是不是自己的
if (stringRedisTemplate.opsForValue().get(key).equals(values)){
stringRedisTemplate.delete(REDIS_LOCK);
}
在问题3的基础上存在问题4:finally块的判断和删除操作不是原子性的(如果不用lua脚本,还有其他办法吗)
解决办法1:使用redis事务
while(true){
// 监控锁,如果在这个过程中有其他线程掺和进来了,则进行第二次,直到删除
stringRedisTemplate.watch(REDIS_LOCK);
if(stringRedisTemplate.opsForValue().get(REDIS_LOCK).equalsIgnoreCase(values)){
stringRedisTemplate.setEnableTransactionSupport(true); // 是否支持事务
stringRedisTemplate.multi(); // 开启事务
stringRedisTemplate.delete(REDIS_LOCK); // 删除锁
List<Object> list = stringRedisTemplate.exec(); // 执行,返回队列
// 为空说明有其他线程掺和,需要再次进行
if (list == null){
continue;
}
// 解锁,退出监控
stringRedisTemplate.unwatch();
break;
}
}
解决办法2:使用Lua脚本(替换上述的while循环)
// RedisUtils类如上图所示
Jedis jedis = RedisUtils.getJedis();
// Lua脚本
String script = "if redis.call('get',KEYS[1]) == ARGV(1)" +
"then " +
"return redis.call('del',KEYS[1]) " +
"else " +
" return 0 " +
"end";
try{
Object o = jedis.eval(script,Collections.singletonList(REDIS_LOCK),Collections.singletonList(value));
if ("1".equals(o.toString()))
System.out.println("删除成功");
else
System.out.println("删除失败");
}finally{
if (jedis != null){
jedis.close();
}
}
在问题4的基础上存在问题5:确保redisLock过期时间大于业务执行时间的问题,即Redis分布式锁如何续期?
Redisson提供的分布式锁是支持锁自动续期的,也就是说,如果线程仍旧没有执行完,那么redisson会自动给redis中的目标key延长超时时间,这在Redisson中称之为 Watch Dog 机制。
CAP:一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)
Redis:属于AP,主机OK了,马上返回,不管从节点。在redis集群中,可能会发生:redis异步复制造成的锁丢失,比如主节点没来得及把刚刚set进来的这条数据给从节点,就挂了
zookeeper:属于CP,等从节点全部OK了,主机才返回
在redis集群环境下,使用RedLock之Redisson落地实现
使用redission,可以解决以上的所有问题
// 先配置RedisConfig.java
@Bean
public Redisson redisson(){
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.111.147:6379").setDatabase(0);
return (Redisson)Redisson.create(Config);
}
@RestController
public class GoodController{
private static final String REDIS_LOCK = "redisLock"; // 锁的名称
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private Redisson redisson; // 使用reidsson
@GetMapping("/buy")
public String buy_goods(){
try{
// 作为唯一固定标识
String value = UUID.randomUUID().toString()+Thread.currentThread().getName();
RLock redissonLock = redisson.getLock(REDIS_LOCK);
redissonLok.lock();
String result = stringRedisTemplate.opsForValue().get("goods:001"); // 从redis中取值
int goodNumber = result == null ? 0:Integer.parseInt(result);
int realNumer = goodNumber -1;
stringRedisTemplate.opsForValue().set("goods:001",String.valueOf(realNumer)); // 修改redis中的值
}finally{
// 判断还是锁定状态
if (redissonLock.isLocked()){
// 判断锁是被当前线程持有
if (redissonLock.isHeldByCurrentThread()){
redissonLok.unlock(); // 尽量不要直接使用unlock(),会出现当前线程和解锁线程不是同一个的错误
}
}
}
}
}