基于Redis的分布式锁解决方案:Redisson

基于Redis的分布式锁解决方案:Redisson

通常来说,秒杀系统在活动期间都需要极高的性能,为了防止超买或超卖,此时需要使用分布式锁解决数据的一致性问题。

本章介绍基于Redis的分布式锁解决方案:Redisson。

分布式锁与Redisson原理

1. 分布式锁

分布式锁即把JVM内部的synchroinzed放置在Redis中,多台服务器可共同去Redis中请求这把锁。

在分布式场景下,为了保证数据一致性,在进行通信时可以共享存储,使各个应用系统拥有更好的自治性。例如,在秒杀系统中,分布式锁可以保证数据一致性,防止超买和超卖。

除 了 基 于 Redis 的 分 布 式 锁 解 决 方 案 , 还 可 以 使 用 ZooKeeper 、Memcached、Chubby等实现方案。在实际使用分布式锁时,可以将分布式锁设计成悲观锁和乐观锁等模式。

当通过编程设计分布式锁时,需要考虑如下特性:

• 互斥:在分布式高并发条件下,同一时刻只有一个线程可以获得锁。

• 防止死锁:在分布式高并发条件下,比如有个线程在获得锁之后,还没有来得及释放锁,就因为系统故障或者其他原因使它无法执行释放锁的命令,导致其他线程无法获得锁,进而造成死锁。所以有必要设置锁的有效时间,确保在系统出现故障后,在一定时间内能够主动释放锁,避免造成死锁。

• 性能:对于访问量大的共享资源,需要减少锁等待的时间,避免大量线程阻塞。为了保证性能,锁的范围要尽可能小,如果锁住两行代码能解决问题,就不要锁住十行代码。另外,锁的颗粒度要小,如果锁住商品ID可以解决问题,就不要锁住商品的整个对象或整行数据。

• 重入:同一个线程可以重复拿到同一个资源的锁。重入锁非常有利于资源的高效利用。

2. Redisson原理

Redisson是架设在Redis上的一个Java驻内存数据网格(In-Memory DataGrid),是Redis官方推荐的框架。

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

Redisson兼容Redis 2.6以上版本和JDK 1.6以上版本,使用ApacheLicense 2.0授权协议,其特点如下:

• 支持Redis集群模式,如自动发现主节点变化、自动发现主从节点、自动更新状态、监听数值变化等。

• 支持Redis哨兵模式,如自动发现主节点、从节点和哨兵节点,自动更新状态。

• 支持Redis主从模式。

• 支持Redis单节模式。

• 多节点模式均支持读写分离,如主读主写、从读主写和主从混读主写。

• 所有对象和接口均支持异步操作。

• 自行管理的弹性异步连接池。

• 所有操作线程安全。

• 支持Lua脚本。

• 支持采用多种方式自动序列化和反序列化(如Jackson JSON、Avro、Smile、CBOR、MsgPack、Kryo、FST、LZ4、Snappy和JDK)。

• 提供分布式锁和同步器,如可重入锁(Reentrant Lock)、公平锁( Fair Lock ) 、 联 锁 ( MultiLock ) 、 红 锁 ( RedLock ) 、 读 写 锁( ReadWriteLock ) 、 信 号 量 ( Semaphore ) 、 可 过 期 性 信 号 量(PermitExpirableSemaphore)和闭锁(CountDownLatch)等。

• 提供分布式对象,如通用对象(Object Bucket)、二进制流(BinaryStream)、地理空间对象桶(Geospatial Bucket)、原子类(AtomicLong或AtomicDouble)、订阅发布、布隆过滤器(Bloom Filter)和基数估计算法(HyperLogLog)等。

• 提供分布式锁和同步器。

• 提供分布式集合,如映射(Map)、多值映射(Multimap)、集( Set ) 、 列 表 ( List ) 、 有 序 集 ( SortedSet ) 、 计 分 排 序 集(ScoredSortedSet)、字典排序集(LexSortedSet)、列队(Queue)、双端队列(Deque)、阻塞队列(Blocking Queue)、有界阻塞列队(BoundedBlocking Queue)、阻塞双端列队(Blocking Deque)、阻塞公平列队( Blocking Fair Queue ) 、 延 迟 列 队 ( Delayed Queue ) 、 优 先 队 列(Priority Queue)和优先双端队列(Priority Deque)。

• 提供分布式服务,如分布式远程服务(Remote Service, RPC)、分布式实时对象(Live Object)服务、分布式执行服务Executor Service、分布式调度任务服务Scheduler Service和分布式映射归纳服务MapReduce)。

• 提供多种集成方式。

3. Redisson相关机制

(1)加锁机制:在Java线程成功请求锁之后,会执行Redisson的内置LUA脚本,并保存数据到Redis中。在Java线程请求锁失败之后,会通过while无限循环进行阻塞,不断请求锁,直到获取成功。

(2)看门狗机制:看门狗也被称为watch dog或watchdog timer,属于一种定时器机制。在某一服务开启了开门狗机制之后,在运行过程中,需要不断向看门狗发出一个信号,这个行为被称之为“喂狗”(feed dog)。在不断喂狗的情况下才能正常进行操作。若在一定内没有“喂狗”,则看门狗会中断当前操作,并重启计时器。Redisson采用了看门狗自动机制,假如一台Java服务器请求到了锁,但该服务器突然宕机,无法释放锁,则在一定时间之后(默认30秒),Redisson会强制释放锁。

(3)Reisson与Jedis最大的不同之处在于,Redisson底层使用了各种封装的LUA脚本,这种方案的好处是LUA脚本在单线程的Redis中可以保证更好的原子性。

单机版超买或超卖问题描述及解决方案

单机版超买或超卖问题描述

多线程在并发执行时,可能会发生重复删除数据或未删除数据等情况,例如:

日志如下所示:

从日志中可以看出,在i+0线程与i+1线程处,结果都为48,即48被连续减了两次,说明已经发生了数据一致性问题。

解决方案

在单机情况下,如果想解决多线程共享数据产生的数据一致性问题,则只需在多线程处加锁(synchronized)即可,代码如下所示:


日志如下所示:

从日志中可以看出,其数据是有序递减的。

分布式版超买或超卖问题描述及解决方案

分布式版超买或超卖问题描述

单机版超买或超卖问题可以通过加锁解决,但是在集群化部署中,对于两台服务器的锁应如何统一处理?

解决方案

当有两台服务器时,把count值放置在Redis中,即使给本地run函数增加synchronized或给count增加synchronized,也无法改变另一台服务器中的相关请求,所以此时需要将整个synchronized移交到Redisson中,做分布式锁的架构即可解决该问题。

Redisson中的代码如下所示:

在上面的代码中,可以通过config配置相关属性,在addNodeAddress处增加Redis集群,在doSomething处增加业务代码。即便出现了“死锁”,只需30秒后Redis即可自动释放锁。

为了防止分布式系统中多个进程之间相互干扰,需要用一种分布式协调技术对这些进程进行调度。而分布式协调技术的核心就是分布式锁。简单来说,当多个程序或应用在操作某一数据时,应先请求一个外部服务器的锁,如果请求到,则行业务操作;如果没有请求到,则需进行阻塞等待。

基于Redis的分布式锁指将Redis的某一值作为锁而存在。假设有两台Java服务器,其中一台在请求到Redis这一值的锁之后,即可进行数据库操作;而另一台Java服务器需要等待第一台服务器执行释放掉Redis的锁之后才能再次进行请求。

Redisson的功能十分强大,包括队列、定时任务、订阅发布通信、对Redis增删改查等不同。

如果系统需要分布式、高并发,那么必须设计分布式锁重。

多线程死锁问题描述及解决方案

多线程死锁问题描述

Java线程死锁是一个经典的多线程问题,因为不同的线程都在等待根本不可能被释放的锁,从而导致所有的任务都无法完成。在多线程技术中,“死锁”是必须避免的,因为这可能会造成线程的“假死”,代码如下所示:


输出结果如图12-1所示。

图12-1

值得注意的是,此时线程处于未关闭状态,Terminate按钮仍是可以使用的。

解决方案

当Terminate按钮仍可用时,先不要关闭线程,而是通过jps和jstack两个工具查询Java死锁。它们的地址在$JAVA_HOME/bin目录下。

• jps是Java提供的一个显示当前所有Java进程的工具,适合在Linux或UNIX平台上查看当前Java进程的一些简单情况。它的作用是显示当前系统的Java进程,通过它可以查看到底启动了多少个Java进程(每个Java程序都会独占一个Java虚拟机实例),并且可以通过opt命令查看这些进程的详细启动参数。

• jstack是JDK自带的线程堆栈分析工具,使用该工具可以查看或导出Java应用程序中的线程堆栈信息。

在$JAVA_HOME/bin目录下输入jps命令,结果如图12-2所示。

图12-2

从图12-2中可以看出,当前一共有3个Java进程,其中,test20测试类进程的PID为12000,它就是未终止的死锁进程。此时可以在jstack命令后输入PID,即可查看当前PID是否包含死锁进程,命令如下所示:

执行结果如下所示:

从执行结果中可以看出,目前仍在执行的只有两个线程,即“Thread-1”与“Thread-0”,此时它们都在等待锁。而目前已找到一处死锁(Found1 deadlock)。找到死锁位置即可解决多线程死锁的问题。

在分布式锁的情况下,Redisson有看门狗机制可以自动释放锁。

Redisson实战--Redisson的可重入锁

如果一个线程已经获得了锁,并且其内部还可以多次申请该锁,那么该锁即为可重入锁。

可以把可重入锁看成锁的一个标识,该标识具有计数器功能。标识的初始值为0,表示当前锁没有被任何线程持有。每当有线程获得可重入锁时,该锁的计数器就加1。每当有线程释放该锁时,该锁的计数器减1。前提是:当前线程已经获得了该锁。JDK的Lock接口释义如下所示:

• lockInterruptibly():表面加锁时,当前拥有锁的线程可以被中断。

• tryLock():尝试获取锁,能获取则返回true,否则返回false。

• tryLock(long time, TimeUnit unit):与tryLock()类似,只是会尝试一段时间。

• unlock():拥有锁的线程释放锁。

• newCondition():返回一个新的与当前实例绑定的Condition。

JDK自带ReentrantLock实现类,以实现Lock接口进行重入锁功能,代码如下所示:


Redisson的公平锁

CPU在调度线程时会从等待队列里随机挑选一个线程,由于是随机的,所以无法保证线程先到先得,同时有些线程(优先级较低的线程)可能永远无法获取CPU的执行权,此时就需要用公平锁进行处理。公平锁可以保证线程按照时间的先后顺序执行,但公平锁的效率较低,因为它需要维护一个有序队列,代码如下所示:

创建公平锁的应用代码如下所示:

输出结果如下所示:

若使用不公平锁,则输出结果如下所示:

基 于 Redis 的 Redisson 分 布 式 可 重 入 公 平 锁 是 实 现java.util.concurrent.locks.Lock接口的一个RLock对象,同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。它保证了当多个Redisson客户端线程同时请求加锁时,优先分配给先发出请求的线程,所有请求线程会在一个队列中排队。当某个线程宕机时,Redisson会等待5秒后继续执行下一个线程。也就是说,如果前面有5个线程都处于等待状态,那么后面的线程需至少等待25秒,代码如下所示:

如果负责储存这个分布式锁的Redis节点宕机了,而这个锁刚好处于锁住的状态,那么这个锁会锁死。为了避免这种情况的发生,Redisson内部提供了一个监控锁的“看门狗”,它的作用是在Redisson实例被关闭前,不断地延长锁的有效期。在默认情况下,“看门狗”检查锁的超时时间是30秒,也可 以 通 过 修 改 Config.lockWatchdogTimeout 参 数 来 另 行 指 定 。 另 外 ,Redisson还可以通过加锁的方式提供leaseTime参数来指定加锁的时间,即超过这个时间后锁便自动解开,代码如下所示:

Redisson还为分布式锁提供了异步执行的相关方法,代码如下所示:

RLock对象完全符合Java的Lock规范。也就是说,只有拥有锁的进程才能解锁,如果其他进程解锁,则会抛出IllegalMonitorStateException错误。

Redisson的联锁

联锁指在程序设计中同时使用多个锁。Redisson的MultiLock对象可以将多个RLock对象关联为一个联锁,每个RLock对象可以来自不同的Redisson实例,代码如下:

Redisson的红锁

红锁(Redis Distributed Lock,RedLock)是一种算法,可以实现多节点Redis的分布式锁,它的特性如下:

• 互斥访问:即永远只有一个客户端能拿到锁。

• 避免死锁:不会出现死锁的情况,即使锁定资源的服务崩溃或者分区,仍然能释放锁。

• 容错性:只要大部分Redis节点存活(一半以上),就可以正常提供服务。

RedLock的原理如下所示:

• 获取当前UNIX时间,以毫秒为单位。

• 依次尝试从N个实例,使用相同的key和随机值获取锁。当为Redis设置锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如,锁的自动失效时间为10秒,则超时时间应该设置在5~50毫秒之间。这样可以避免在服务器端Redis已经“挂掉”的情况下,客户端还在等待响应结果。如果服务器端没有在规定的时间内响应,则客户端应该尽快尝试另一个Redis实例。

• 客户端使用当前时间减去开始获取锁时间即可得到获取锁可使用的时间。当大多数(这里是3个节点)的Redis节点都取到了锁,并且锁可使用的时间小于锁失效时间时,才算获取成功。

• 如果取到了锁,则key的真正有效时间等于有效时间减去获取锁所使用的时间。

• 如果获取锁失败,则客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本没有加锁)。

RedissonRedLock对象实现了RedLock介绍的加锁算法,该对象可以将多个RLock对象关联为一个红锁,每个RLock对象实例可以来自不同的Redisson实例:

Redisson的读写锁

ReadWriteLock 是 JDK 中 的 读 写 锁 接 口 , ReentrantReadWriteLock 是ReadWriteLock的一种实现。读写锁非常适合读多写少的场景。读写锁和互斥锁的一个重要区别是读写锁允许多个线程同时共享读变量,这也是读写锁在读多写少的情况下性能较高的原因。读写锁的特点如下所示:

• 多个线程可同时共享读变量。

• 只允许一个线程共享写变量。

• 写线程正在执行写操作,禁止其他线程读写共享变量。

读写锁的代码如下所:

读写锁的执行结果如下所示:

基于Redisson的分布式可重入读写锁RReadWriteLock,Java对象实现了java.util.concurrent.locks.ReadWriteLock接口。其中,读锁和写锁都继承了RLock接口。分布式可重入读写锁允许同时有多个读锁和一个写锁处于加锁状态,代码如下所示:

Redisson的信号量

信号量(Semaphore)在多线程环境下被广泛使用。在Java并发库中,它可以控制某个资源当前被访问的次数,即通过acquire方法获取一个许可,如果没有就等待;然后通过release方法释放一个许可。

Semaphore不仅维护了当前访问的个次数,还提供了同步机制,即可以控制同时访问的次数。在数据结构中,链表可以保存“无限”个节点,而使用Semaphore可以实现有限大小的链表。另外,可重入锁也可以实现该功能,但在实现上要复杂一些。Samaphore的示例代码如下所:

执行结果如下所示:

基 于 Redisson 的 分 布 式 信 号 量 , Java 对 象 RSemaphore 采 用 了 与java.util.concurrent.Semaphore相似的接口和用法,同时提供了异步、反射式和RxJava2标准的接口。

基于Redisson的可过期信号量,RPermitExpirableSemaphore.acquire为每个信号增加了一个过期时间。每个信号都有独立的ID,并且只能通过提交这个ID才能被释放。可过期信息号量提供了异步、反射式和RxJava2标准的接口,代码如下所示:

Redisson的分布式闭锁

当一个用户请求服务器时,服务器为了响应,需要进行一系列的操作。

例如,有的需要去调用多个接口,等待各个接口的返回结果。而各个接口之间相互独立,为了提高效率,一般会使用异步的方式调用。在所有异步线程都执行完毕后,再通知主线程,由主线程执行下一步的逻辑,最终把响应结果返回给用户。闭锁的存在是为了让主线程知道所有的线程都已执行结束,若多线程未执行完,则主线程阻塞;若多线程已执行完,则主线程执行下一步的逻辑。在JUC中提供了CountDownLatch,它可以简化闭锁的操作。代码如下所示:

执行结果如下所示:

基 于 Redisson 的 分 布 式 闭 锁 , Java 对 象 RCountDownLatch 采 用 了 与java.util.concurrent. CountDownLatch相似的接口和用法,代码如下所示:

本文给大家讲解的内容是基于Redis的分布式锁解决方案:Redisson

展开阅读全文

页面更新:2024-02-21

标签:死锁   分布式   节点   线程   加锁   进程   接口   对象   解决方案   代码   时间

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top