使用redis实现分布式锁

// 下述代码是逐步完善的

@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(),会出现当前线程和解锁线程不是同一个的错误
                }
            }
            
        }
                             
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值