基于Redis实现分布式锁-Redisson使用及源码分析

本文探讨了在分布式环境中实现最终一致性的一种解决方案——分布式锁,重点分析了基于Redis的分布式锁实现。通过Redisson这一Java分布式锁开源框架,解释了如何设计和使用分布式锁,包括Redis命令、锁设计、加锁与解锁流程,并提供了Redisson的使用示例和源码解析。文章强调了设计分布式锁时需要考虑的问题,如死锁、性能效率和可重入性,并给出了具体的解决方案。
摘要由CSDN通过智能技术生成

在分布式场景下,有很多种情况都需要实现最终一致性。在设计远程上下文的领域事件的时候,为了保证最终一致性,在通过领域事件进行通讯的方式中,可以共享存储(领域模型和消息的持久化数据源),或者做全局XA事务(两阶段提交,数据源可分开),也可以借助消息中间件(消费者处理需要能幂等)。通过Observer模式来发布领域事件可以提供很好的高并发性能,并且事件存储也能追溯更小粒度的事件数据,使各个应用系统拥有更好的自治性。
本文主要探讨另外一种实现分布式最终一致性的解决方案——采用分布式锁。基于分布式锁的解决方案,比如zookeeper,redis都是相较于持久化(如利用InnoDB行锁,或事务,或version乐观锁)方案提供了高可用性,并且支持丰富化的使用场景。 本文通过Java版本的redis分布式锁开源框架——Redisson来解析一下实现分布式锁的思路。

分布式锁的使用场景

如果是不跨限界上下文的情况,跟本地领域服务相关的数据一致性,尽量还是用事务来保证。但也有些无法用事务或者乐观锁来处理的情况,这些情况大多是对于一个共享型的数据源,有并发写操作的场景,但又不是对于单一领域的操作。
举个例子,还是用租书来比喻,A和B两个人都来租书,在查看图书的时候,发现自己想要看的书《大设计》库存仅剩一本。书店系统中,书作为一种商品,是在商品系统中,以Item表示出租商品的领域模型,同时每一笔交易都会产生一个订单,Order是在订单系统(交易限界上下文)中的领域模型。这里假设先不考虑跨系统通信的问题(感兴趣的可以参考下领域服务、领域事件),也暂时不考虑支付环节,但是我们需要保证A,B两个人不会都对于《大设计》产生订单就可以,也就是其中一个人是可以成功下单,另外一个人只要提示库存已没即可。此时,书的库存就是一种共享的分布式资源,下订单,减库存就是一个需要保证一致性的写操作。但又因为两个操作不能在同一个本地事务,或者说,不共享持久化的数据源的情况,这时候就可以考虑用分布式锁来实现。本例子中,就需要对于共享资源——书的库存进行加锁,至于锁的key可以结合领域模型的唯一标识,如itemId,以及操作类型(如操作类型是RENT的)设计一个待加锁的资源标识。当然,这里还有一个并发性能的问题,如果是个库存很多的秒杀类型的业务,那么就不能单纯在itemId 加类型加锁,还需要设计排队队列以及合理的调度算法,防止超卖等等,那些就是题外话了。本文只是将这个场景作为一个切入点,具体怎么设计锁,什么场景用还要结合业务。

需要解决的问题

分布式的思路和线程同步锁ReentrantLock的思路是一样的。我们也要考虑如以下几个问题:

  • 死锁的情况。复杂的网络环境下,当加锁成功,后续操作正在处理时,获得锁的节点忽然宕机,无法释放锁的情况。如A在Node1 节点申请到了锁资源,但是Node1宕机,锁一直无法释放,订单没有生成,但是其他用户将无法申请到锁资源。
  • 锁的性能效率。分布式锁不能成为性能瓶颈或者单点故障不能导致业务异常。
  • 如果关键业务,可能需要重入场景,是否设计成可重入锁。这个可以参考下在多线程的情况下,比如ReentrantLock就是一种可重入锁,其内部又提供了公平锁和非公平锁两种实现和应用,本文不继续探讨。带着以上问题,和场景,沿着下文,来一一找到解决方案。

基于Redis实现

Redis 命令

在Redisson介绍前,回顾下Redis的命令,以及不通过任何开源框架,可以基于redis怎么设计一个分布式锁。基于不同应用系统实现的语言,也可以通过其他一些如Jedis,或者Spring的RedisOperations 等,来执行Reids命令Redis command list
分布式锁主要需要以下redis命令,这里列举一下。在实现部分可以继续参照命令的操作含义。

  1. SETNX key value (SET if Not eXists):当且仅当 key 不存在,将 key 的值设为 value ,并返回1;若给定的 key 已经存在,则 SETNX 不做任何动作,并返回0。详见:SETNX commond
  2. GETSET key value:将给定 key 的值设为 value ,并返回 key 的旧值 (old value),当 key 存在但不是字符串类型时,返回一个错误,当key不存在时,返回nil。详见:GETSET commond
  3. GET key:返回 key 所关联的字符串值,如果 key 不存在那么返回 nil 。详见:GET Commond
  4. DEL key [KEY …]:删除给定的一个或多个 key ,不存在的 key 会被忽略,返回实际删除的key的个数(integer)。详见:DEL Commond
  5. HSET key field value:给一个key 设置一个{field=value}的组合值,如果key没有就直接赋值并返回1,如果field已有,那么就更新value的值,并返回0.详见:HSET Commond
  6. HEXISTS key field:当key 中存储着field的时候返回1,如果key或者field至少有一个不存在返回0。详见HEXISTS Commond
  7. HINCRBY key field increment:将存储在 key 中的哈希(Hash)对象中的指定字段 field 的值加上增量 increment。如果键 key 不存在,一个保存了哈希对象的新建将被创建。如果字段 field 不存在,在进行当前操作前,其将被创建,且对应的值被置为 0。返回值是增量之后的值。详见:HINCRBY Commond
  8. PEXPIRE key milliseconds:设置存活时间,单位是毫秒。expire操作单位是秒。详见:PEXPIRE Commond
  9. PUBLISH channel message:向channel post一个message内容的消息,返回接收消息的客户端数。详见PUBLISH Commond

Redis 实现分布式锁

假设我们现在要给itemId 1234 和下单操作 OP_ORDER 加锁,key是OP_ORDER_1234,结合上面的redis命令,似乎加锁的时候只要一个SETNX OP_ORDER_1234 currentTimestamp ,如果返回1代表加锁成功,返回0 表示锁被占用着。然后再用DEL OP_ORDER_1234解锁,返回1表示解锁成功,0表示已经被解锁过。然而却还存在着很多问题:SETNX会存在锁竞争,如果在执行过程中客户端宕机,也会引起死锁问题,即锁资源无法释放。并且当一个资源解锁的时候,释放锁之后,其他之前等待的锁没有办法再次自动重试申请锁(除非重新申请锁)。解决死锁的问题其实可以可以向Mysql的死锁检测学习,设置一个失效时间,通过key的时间戳来判断是否需要强制解锁。但是强制解锁也存在问题,一个就是时间差问题,不同的机器的本地时间可能也存在时间差,在很小事务粒度的高并发场景下还是会存在问题,比如删除锁的时候,在判断时间戳已经超过时效,有可能删除了其他已经获取锁的客户端的锁。另外,如果设置了一个超时时间,但是确实执行时间超过了超时时间,那么锁会被自动释放,原来持锁的客户端再次解锁的时候会出现问题,而且最为严重的还是一致性没有得到保障。
所以设计的时候需要考虑以下几点:

  1. 锁的时效设置。避免单点故障造成死锁,影响其他客户端获取锁。但是也要保证一旦一个客户端持锁,在客户端可用时不会被其他客户端解锁。(网上很多解决方案都是其他客户端等待队列长度判断是否强制解锁,但其实在偶发情况下就不能保证一致性,也就失去了分布式锁的意义)。
  2. 持锁期间的check,尽量在关键节点检查锁的状态,所以要设计成可重入锁,但在客户端使用时要做好吞吐量的权衡。
  3. 减少获取锁的操作,尽量减少redis压力。所以需要让客户端的申请锁有一个等待时间,而不是所有申请锁的请求要循环申请锁。
  4. 加锁的事务或者操作尽量粒度小,减少其他客户端申请锁的等待时间,提高处理效率和并发性。
  5. 持锁的客户端解锁后,要能通知到其他等待锁的节点,否则其他节点只能一直等待一个预计的时间再触发申请锁。类似线程的notifyAll,要能同步锁状态给其他客户端,并且是分布式消息。
  6. 考虑任何执行句柄中可能出现的异常,状态的正确流转和处理。比如,不能因为一个节点解锁失败,或者锁查询失败(redis 超时或者其他运行时异常),影响整个等待的任务队列,或者任务池。

锁设计

由于时间戳的设计有很多问题,以及上述几个问题,所以再换一种思路。先回顾几个关于锁的概念和经典java API。通过一些java.util.concurrent的API来处理一些本地队列的同步以及等待信号量的处理。

  • Semaphore :Semaphore可以控制某个资源可被同时访问的个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。其内部维护了一个int 类型的permits。有一个关于厕所的比喻很贴切,10个人在厕所外面排队,厕所有5个坑,只能最多进去五个人,那么就是初始化一个 permits=5的Semaphore。当一个人出来,会release一个坑位,其他等坑的人会被唤醒然后开始要有人进坑。Semap
Redisson是基于Redis实现Java驻留内存数据网格的开源框架,提供了丰富的分布式锁实现方式。下面介绍基于Redisson的最优实现。 1. 初始化Redisson客户端 ```java Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379"); RedissonClient redisson = Redisson.create(config); ``` 2. 获取分布式锁 ```java RLock lock = redisson.getLock("myLock"); lock.lock(); try { // 业务逻辑 } finally { lock.unlock(); } ``` 3. 设置加锁超时时间和释放锁时限制 ```java RLock lock = redisson.getLock("myLock"); boolean locked = lock.tryLock(10, 60, TimeUnit.SECONDS); try { if (locked) { // 业务逻辑 } else { // 获取锁失败 } } finally { if (locked) { lock.unlock(); } } ``` 4. 实现可重入锁 ```java RLock lock = redisson.getLock("myLock"); lock.lock(); try { lock.lock(); try { // 业务逻辑 } finally { lock.unlock(); } } finally { lock.unlock(); } ``` 5. 实现公平锁 ```java RLock lock = redisson.getFairLock("myFairLock"); lock.lock(); try { // 业务逻辑 } finally { lock.unlock(); } ``` 6. 实现读写锁 ```java RReadWriteLock rwlock = redisson.getReadWriteLock("myReadWriteLock"); rwlock.readLock().lock(); try { // 读操作业务逻辑 } finally { rwlock.readLock().unlock(); } rwlock.writeLock().lock(); try { // 写操作业务逻辑 } finally { rwlock.writeLock().unlock(); } ``` 以上就是基于Redisson实现Redis分布式锁的最优实现Redisson提供了丰富的分布式锁实现,可以根据业务需求选择合适的锁类型。同时,Redisson还提供了许多其他功能,如分布式对象、分布式限流等,可以方便地实现分布式应用程序。
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值