高并发下Redis实现分布式锁的坑你是否踩过

目录

一、锁,你了解多少?

二、设计分布式锁应该考虑的东西

三、可重入锁你知道吗?

四、分布式锁的选型实现

五、Redis实现分布式锁的坑你发现了吗 


一、锁,你了解多少?

有本地锁:synchronize、lock等,锁在当前进程内,分布式集群部署下依旧存在锁失效问题
还有分布式锁:redis、zookeeper等实现,虽然还是锁,但是多个进程共用的锁标记,可以用Redis、Zookeeper、Mysql等都可以

二、设计分布式锁应该考虑的东西

排他性
    在分布式应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行
容错性
    分布式锁一定能得到释放,比如客户端奔溃或者网络中断
满足可重入、高性能、高可用
注意分布式锁的开销、锁粒度 

三、可重入锁你知道吗?

单节点可重入锁
    可重入锁: JDK指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的,synchronized 和 ReentrantLock 都是可重入锁
分布式下的可重入锁
    进程单位,当一个线程获取对象锁之后,其他节点的同个业务线程可以再次获取本对象上的锁

四、分布式锁的选型实现

 实现分布式锁 可以用 Redis、Zookeeper、Mysql数据库这几种 , 性能最好的是Redis且是最容易理解

分布式锁离不开 key - value 设置

key 是锁的唯一标识,一般按业务来决定命名,比如想要给一种商品的秒杀活动加锁,key 命名为 “seckill_商品ID” 。value就可以使用固定值,比如设置成1。
短链码可以:short_link:code:xxxx

基于redis实现分布式锁,文档:http://www.redis.cn/commands.html#string

加锁 SETNX key value
    setnx 的含义就是 SET if Not Exists,有两个参数 setnx(key, value),该方法是原子性操作
    如果 key 不存在,则设置当前 key 成功,返回 1;
    如果当前 key 已经存在,则设置当前 key 失败,返回 0
    
解锁 del (key)
得到锁的线程执行完任务,需要释放锁,以便其他线程可以进入,调用 del(key)

配置锁超时 expire (key,30s)
客户端奔溃或者网络中断,资源将会永远被锁住,即死锁,因此需要给key配置过期时间,以保证即使没有被显式释放,这把锁也要在一定时间后自动释放 

综合伪代码

 methodA(){
  String key = "short_link:code:abcdef"
  
  if(setnx(key,1) == 1){
      expire(key,30,TimeUnit.MILLISECONDS)
      try {
          //做对应的业务逻辑
      } finally {
          del(key)
      }
  }else{
    //睡眠100毫秒,然后自旋调用本方法
    methodA()
  }
}

五、Redis实现分布式锁的坑你发现了吗 

上述伪代码最大的问题:多个命令之间不是原子性操作,如setnx和expire之间,如果setnx成功,但是expire失败,且宕机了,则这个资源就是死锁

可以使用原子命令解决:设置和配置过期时间  setnx / setex
如: set key 1 ex 30 nx
java代码里面 

String key = "short_link:code:abcdef"
redisTemplate.opsForValue().setIfAbsent(key,1,30,TimeUnit.MILLISECONDS)

 但是实际业务应用情况还有更多问题:
业务超时,如何避免其他线程勿删
业务执行时间过长,如何实现锁的自动续期

分布式锁的核心:

是保证多个指令原子性,加锁使用setnx setex 可以保证原子性,那解锁使用判断和设置等怎么保证原子性

多个命令的原子性:采用 lua脚本+redis, 由于【判断和删除】是lua脚本执行,所以要么全成功,要么全失败

流程:
* 先判断是否有,如没这个key,则设置key-value,配置过期时间,加锁成功
* 如果有这个key,判断value是否是同个账号,是同个账号则返回加锁成功
* 如果不是同个账号则加锁失败

代码实现:

//key1是短链码,ARGV[1]是accountNo,ARGV[2]是过期时间
String script = "if redis.call('EXISTS',KEYS[1])==0 then redis.call('set',KEYS[1],ARGV[1]); redis.call('expire',KEYS[1],ARGV[2]); return 1;" +
                " elseif redis.call('get',KEYS[1]) == ARGV[1] then return 2;" +
                " else return 0; end;";


Long result = redisTemplate.execute(new
                DefaultRedisScript<>(script, Long.class), Arrays.asList(code), value,100);

演示效果:同一个code下,不同账号进行加锁。同一个账号则不加锁。

code=aabbcc进入方法,账号是123,加锁成功,返回1 

 code=aabbcc进入方法,账号是123,加锁成功,返回2

 这样就实现了同一个code,同一个账号的可重入锁

code=aabbcc进入方法,账号是567,加锁失败

 实际业务代码中,可result 是否大于0来判断加锁失败和成功,然后走对应的业务逻辑即可。

  • 19
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 24
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值