java如何保证redis设置过期时间的原子性_redis专题系列22 -- 如何优雅的基于redis实现分布式锁

几个概念

线程锁:主要用来给方法、代码块加锁。当某个方法或代码使用锁,在同一时刻仅有一个线程执行该方法或该代码段。线程锁只在同一JVM中有效果,因为线程锁的实现在根本上是依靠线程之间共享内存实现的,比如synchronized是共享对象头,显示锁Lock是共享某个变量(state)。

进程锁:为了控制同一操作系统中多个进程访问某个共享资源,因为进程具有独立性,各个进程无法访问其他进程的资源,因此无法通过synchronized等线程锁实现进程锁。

分布式锁:当多个进程不在同一个系统中,用分布式锁控制多个进程对资源的访问。

合理的分布式锁应该具有哪些特性

在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行高可用的获取锁与释放锁高性能的获取锁与释放锁具备可重入特性(可理解为重新进入,由多于一个任务并发使用,而不必担心数据错误)具备锁失效机制,防止死锁具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败

目前可供选择的分布式锁技术方案

1.利用数据库本身的乐观锁(新增字段version,每次更新版本号+1,参考mysql的MVCC机制)或者排他锁来实现(for update)

2.利用 Zookeeper 的顺序临时节点,来实现分布式锁和等待队列。Zookeeper 设计的初衷,就是为了实现分布式锁服务的。

3.利用redis的SET my_key my_value NX PX milliseconds命令实现或者内嵌的lua脚本,鉴于redis的特性,他们都具有原子性

4.利用 Memcached 的 add 命令。此命令是原子性操作,只有在 key 不存在的情况下,才能 add 成功,也就意味着线程得到了锁。

5.Chubby:Google 公司实现的粗粒度分布式锁服务,底层利用了 Paxos 一致性算法。

6.基于 Consul 做分布式锁,主要利用 Consul 的 Key / Value 存储 API 中的 acquire 和 release 操作来实现

谈谈如何优雅的通过redis实现分布式锁

在设计redis分布式锁,我们要考虑的点:

1.命令具有原子性,不允许多个客户端可以同时执行同一条指令,包含加锁和释放锁的命令

2.锁超时,针对锁应该设置超时时间防止单点客户端宕机后锁得不到释放造成其他等待锁的线程无限等待

3.锁续约,简单来说,假设我们给锁设置的失效时间为2s,但业务执行完毕需要3s,导致其他线程过早拿到锁,可能对临界区资源造成数据安全问题,同时,执行unlock的时候,释放掉了其他进程持有的锁。

大概思维导图如下:

33e93df09aeb410099dfa4024576cddc

下面拆分:

1.加锁

加锁实际上就是在redis中,给Key键设置一个值,为避免死锁,并给定一个过期时间

SET lock_key random_value NX PX 5000

值得注意的是:
random_value 是客户端生成的唯一的字符串(可以参考分布式id算法,像UUID,Snowflake等等)。
NX 代表只在键不存在时,才对键进行设置操作。
PX 5000 设置键的过期时间为5000毫秒。

这样,如果上面的命令执行成功,则证明客户端获取到了锁。

2.解锁

解锁的过程就是将Key键删除。但也不能乱删,不能说客户端1的请求将客户端2的锁给删除掉。这时候random_value的作用就体现出来。

为了保证解锁操作的原子性,我们用LUA脚本完成这一操作。先判断当前锁的字符串是否与传入的值相等,是的话就删除Key,解锁成功。

3.关键代码

5953dbee3e0d4aa8a5a0eed239abd74c

加锁

40d278d68cc44bb496a3a58330e31879

解锁


至于守护线程,可以参考JDK的ScheduleService,根据key的TTL创建定时任务去监测当前的业务的执行时间从而判断是否决定锁续约,当然还有其他很多方法,这只是一个参考。

总结:基于上面的实现,我们基本上实现了单机的redis分布式锁,但它依然有个明显的缺点,即不可重入,如果同一个进程进行多次加锁,则显得有点捉襟见肘了。其实,参看JDK的重入锁,我们也可以轻松的设计出redis分布式锁,下面我们来了解一下Redisson.

Redisson分布式锁

Redisson为我们提供了更好的实现,几门满足了分布式锁的所有特性。我们来了解一下

Redisson是架设在Redis基础上的一个Java驻内存数据网格(In-Memory Data Grid)。充分的利用了Redis键值数据库提供的一系列优势,基于Java实用工具包中常用接口,为使用者提供了一系列具有分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力,大大降低了设计和研发大规模分布式系统的难度。同时结合各富特色的分布式服务,更进一步简化了分布式环境中程序相互之间的协作。

Redisson可谓是强大到不可思议,几乎包含了我们想要的关于分布式锁想要的一切特性,拆箱即用,非常方便,而且锁的种类繁多,像可重入锁,联锁,红锁,公平锁,信号量,可过期信号量,闭锁等等。本文就结合源码大概讲解一下redisson基于JDK重入锁实现的分布式锁,希望你能get到它的思想并运用到项目中。

先给一下Maven配置:

org.redisson   redisson   3.11.6

代码简单示例:

7631decd5e014930b6e413a2d73f9a58

源码解析:

1.加锁

RLock lock = client.getLock("lock1"); 这句代码就是为了获取锁的实例,然后我们可以看到它返回的是一个RedissonLock对象。加锁的代码都是lockInterruptibly 方法提供支持的,我们只分析这个方法

ec83fb55d4e2446d8da19d1456267158

如上代码,就是加锁的全过程。先调用tryAcquire来获取锁,如果返回值ttl为空,则证明加锁成功,返回;如果不为空,则证明加锁失败。这时候,它会订阅这个锁的Channel,等待锁释放的消息,然后重新尝试获取锁。流程如下:

8e1f1ff1bcf04c36b37bc6d5dffee05b

接下来就要看tryAcquire方法

a9255b6d2db14b4cbe5281ec82e826a7

在这里,它有两种处理方式,一种是带有过期时间的锁,一种是不带过期时间的锁。接着往下看,tryLockInnerAsync方法是真正执行获取锁的逻辑,它是一段LUA脚本代码。在这里,它使用的是hash数据结构。同时注意scheduleExpirationRenewal,redisson就是通过它进行所续约的。

1dae2ec3ed8b464586fd89f5ef644c2d

这段LUA代码看起来并不复杂,有三个判断:

  • 通过exists判断,如果锁不存在,则设置值和过期时间,加锁成功
  • 通过hexists判断,如果锁已存在,并且锁的是当前线程,则证明是重入锁,加锁成功
  • 如果锁已存在,但锁的不是当前线程,则证明有其他线程持有锁。返回当前锁的过期时间,加锁失败
01689d6a3fce4053b2dbb570771b993c

加锁成功后,在redis的内存数据中,就有一条hash结构的数据。Key为锁的名称;field为随机字符串+线程ID;值为1。如果同一线程多次调用lock方法,值递增1。这正好吻合了JDK重入锁的设计思想。

2.解锁

通过调用unlock方法来解锁

b780591ec0f4445e9eaa1a83955640b4

然后我们再看unlockInnerAsync方法。这里也是一段LUA脚本代码。

f39f602d44ea42418b33d84a6a045d13

如上代码,就是释放锁的逻辑。同样的,它也是有三个判断:

  • 如果锁已经不存在,通过publish发布锁释放的消息,解锁成功
  • 如果解锁的线程和当前锁的线程不是同一个,解锁失败,抛出异常
  • 通过hincrby递减1,先释放一次锁。若剩余次数还大于0,则证明当前锁是重入锁,刷新过期时间;若剩余次数小于0,删除key并发布锁释放的消息,解锁成功
047bd896895544808db708b680a5b357

这样,关于redisson重入锁的加锁解锁都已经分析完了。注意,redisson的锁续约是采用了watchdog机制,其实它就是一个定时调度任务,如果你没有设置锁的过期时间,redisson会给一个默认值30s,这样看门狗就会每10s进行一次续约,保证锁一致持有在手中,当然它依然有自己的熔断机制,假设持有锁的线程进入了死循环,在几次续约后便不再续约。同样,在解锁时,取消了锁续约机制。

总结

1.上面介绍的均为单机模式下的分布式锁实现方式,如果强行使用到集群模式下存在一定的风险,我们知道redis集群模式下使用的主从复制模式(异步),如果客户端在加锁成功的一刹那,master节点故障,导致slave节点没有同步到对应的key值,可能存在多个客户端获取通一把锁。至于集群模式下分布式锁如何实现和原理细节,请参考http://redis.cn/topics/distlock.html。

2.同时对于redisson,可重入锁只是其冰山一角,如果感兴趣的话可以去官网了解其架构和底层原理,本文不宜过多讲解。github地址:https://github.com/redisson/redisson。

附上redisson架构图:

62464fef821349f3a3715019c42c04b2
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值