六、Redis事物
(一)事物简介
事物定义
-
Redis执行指令过程中,多条连续执行的指令被干扰,打断,插队
-
Redis是事物就是一个命令执行的队列,将一系列预定义命令包装成一个整体(一个队列),当执行时,一次性按照添加顺序一次执行,中间不会被打断或者被干扰。
-
一个队列中,一次性,顺序性,排它性的执行一系列命令。
事物的边界
(二)事物基本操作
Redis事物三大特性
- 单独的隔离操作
- 事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
- 没有隔离级别的概念
- 队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行
- 不保证原子性
- 事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚
开启事物
1、命令
mult
i
2、作用
设置事物的开启位置,此指令执行后,后续的所有指令均加入到事物中。
执行事物
1、命令
exec
2、作用
设定事物的结束位置,同时执行事物,与multi成对出现,成对使用。
注意事项
- 加入事物的key暂时进入到任务队列中,并没有立即执行,只有执行exec命令才开始执行。
取消事物
1、命令:
discard
2、作用:
终止当前事物的定义,发生在multi之后,exec之前。
事物的工作流程
事物的注意事项
1、语法错误
-
语法错误:
- 指令命令书写格式错误
-
处理结果:
- 如果定义的事物中所包含的命令存在语法错误,整体事务中所有命令都不会执行,包括哪些语法正确的命令,相当于取消整个事物。
2、命令执行出现错误
- 运行错误
- 指命令格式正确,但是无法正确的执行,例如对list进行incr操作
- 处理结果
- 能够正确运行的命令会执行,运行错误的命令不会被执行。
- 注意:
- 已经执行完毕的命令对应的数据不会自动回滚,需要程序员自己在代码中实现回滚。
手动进行事物回滚
- 记录操作过程中影响数据之前的状态
- 单数据:string
- 多数据:hash、list、set、zset
- 设置指令恢复所有的被修改的向
- 单数据:直接set(注意周边属性,例如时效)
- 多数据:修改对应值或整体克隆复制。
(三)锁
基于特定条件的事物执行-监视锁(乐观锁)
1、业务场景
- 天猫双11热卖过程中,对已经售罄的货物追加补货,4个业务员都有权限进行补货。补货的操作可能是一系列的操作,牵扯到多个连续操作,如何保障不会重复操作?
2、业务分析
- 多个客户端有可能同时操作同一组数据,并且该数据一旦被操作修改后,将不适用于继续操作
- 在操作之前锁定要操的数据,一旦发生变化,终止当前操作
3、解决方案-监视锁
- 对key添加监视锁,添加事物之前执行,再执行exec前如果key发生了变化,终止事物执行
watch key1 key2 key3.....
- 取消对所有key的监视
unwatch
4、场景:Redis应用于基于状态控制的批量任务执行。
基于特定条件的事物执行-分布式锁
1、业务场景-【超卖问题】
天猫双11热卖过程中,对已经售垒的货物追加补货,且补货完成。客户购买热情高涨,3秒内将所有商品购
买完毕。本次补货已经将库存全部清空,如何避免最后一件商品不被多人同时购买?【超卖问题】
2、业务分析
- 使用watch监控一个key有没有改变已经不能解决问题,此处要监控的是具体数据
- 虽然rdis是单线程的,但是多个客户端对同一数据同时进行操作时,如何避免不被同时修改?
3、解决方案
- 使用setnx设置一个公共锁
setnx lock-key value
- lock-key就是个普通key,只是利用了setnx的存在该key就会set失败的特性
- 利用setnx命令的返回值特征,有值则返回设置失败,无值则返回设置成功。
- 对应返回设置成功的,拥有控制权,进行下一步的具体业务操作。
- 对于返回设置失败的,不具有控制权,排队或等待
- 操作完毕通过del key 操作释放锁。
- 注意:上述解决方案是一种设计概念,依赖规范保障,具有风险性。
基于特定条件的事物执行-死锁
1、业务场景
依赖分布式锁的机制,某个用户操时对应客户端宕机,且此时已经获取到锁。如何解决?
2、业务分析
- 由于锁操作由用户控制助加锁解锁,必定会存在加锁后未解锁的风险。
- 需要解锁操作不能仅依赖用户控制,系统级别要给出对应的保底处理方案。
3、解决方案
使用expire为锁添加时间限定,到时如不释放,放弃锁
expire lock-key second
pexpire lock-key milliseconds
由于操作通常都是微秒或毫秒级,因此该锁定时间不宜设置过大。具体时间需要业务测试后确认。
- 例如:持有锁的操作最长执行时间127s,最短执行时间7ms。
- 测试百万次最长执行时间对应命令的最大耗时,测试百万次网络延迟平均耗时
- 锁时间设定推荐:最大耗时120%+平均网络延迟110%
- 如果业务最大耗时<<网络平均迟,通常为2个数量级,取其中单个耗时较长即可
二、分布式锁
(一)概括
锁的种类
- 单机版同一个JVM虚拟机内,synchronized或者Lock接口
- 分布式多个不同JVM虚拟机,单机的线程锁机制不再起作用,资源类在不同的服务
分布式锁需要具备的条件和刚需
1、独占性
- OnlyOne,任何时刻只能有且仅有一个线程持有
2、高可用
- 若redis集群环境下,不能因为某一个节点挂了而出现获取锁和释放锁失败的情况
- 高并发请求,依旧性能好
3、防死锁
- 杜绝死锁,必须有超时控制机制或者撤销操作,有个兜底终止跳出方案
4、不乱抢
- 防止张冠李戴,不能私下unlock别人的锁,只能自己加锁自己释放,自己约的锁含着泪也要自己解
5、重入性
- 同一个节点的同一个线程如果获得锁之后,它也可以再次获取这个锁。
synchronized中锁在分布式项目中的缺陷
- 在单机环境下,可以使用synchronized或Lock来实现。
- 但是在分布式系统中,因为竞争的线程可能不在同一个节点上(同一jvm中),
- 所以需要一个让所有进程都能访问到的锁来实现(比如redis或者zookeeper来构建)
- 不同进程jvm层面的锁就不管用了,那么可以利用第三方的一个组件,来获取锁,未获取到锁,则阻塞当前想要运行的线程
(三)分布式锁
**差评,sexnx+expire不安全,两条命令非原子性。Lua脚本 **
注意:中小厂可以用,大厂不可以用
使用expire为锁添加时间限定,到时如不释放,放弃锁
expire lock-key second
pexpire lock-key milliseconds
由于操作通常都是微秒或毫秒级,因此该锁定时间不宜设置过大。具体时间需要业务测试后确认。
- 例如:持有锁的操作最长执行时间127s,最短执行时间7ms。
- 测试百万次最长执行时间对应命令的最大耗时,测试百万次网络延迟平均耗时
- 锁时间设定推荐:最大耗时120%+平均网络延迟110%
- 如果业务最大耗时<<网络平均迟,通常为2个数量级,取其中单个耗时较长即可
在代码中加锁和过期时间必须同一行,保证原子性。
stringRedisTemplate.opsForValue().setIfAbsent(key,uuidValue,30L,TimeUnit.SECONDS)
只能自己删除自己的锁,不可以别人删除自己的锁
- 让一样的key不一样即可,比如追加UUID
(三)Redission 看门狗
Reidssion简介
Redisson是一个实现的Java操作Redis的工具包,它不仅提供了一系列常用的操作Redis的API,还提供了许多分布式服务。其中包括(BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service) Redisson提供了使用Redis的最简单和最便捷的方法,Redisson的宗旨是促进使用者对Redis的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上。
整合步骤
1.导入Redission依赖
<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
2.配置一个单机Redis
@Configuration
public class RedissonConfig {
//创建客户端
@Bean
public RedissonClient redissonClient(){
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");//.setPassword("123456");
return Redisson.create(config);
}
}
注:锁的粒度要小,先加锁要判断。
官方定义分布式锁:
如果负责储存这个分布式锁的Redisson节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。
另外Redisson还通过加锁的方法提供了leaseTime的参数来指定加锁的时间。超过这个时间后锁便自动解开了。
总结就是:
- Redisson加锁自动有过期时间30s,监控锁的看门狗发现业务没执行完,会自动进行锁的续期(重回30s),这样做的好处是防止在程序执行期间锁自动过期被删除问题
- 当业务执行完成不再给锁续期,即使没有手动释放锁,锁的过期时间到了也会自动释放锁
可重入锁
基于Redis的Redisson分布式可重入锁RLock Java对象实现了java.util.concurrent.locks.Lock接口。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。
@Autowired
private RedissonClient redissonClient;
@Test
public void testLock1(){
RLock rLock = redissonClient.getLock("lock_stock");
rLock.lock(); //阻塞式等待,过期时间30s
try{
System.out.println("加锁成功....");
System.out.println("执行业务....");
}finally {
rLock.unlock();
System.out.println("释放锁....");
}
}
实现原理:
-
如果没有设置过期时间,Redisson以 30s 作为锁的默认过期时间,获取锁成功后(底层也用到了Lua脚本保证原子性)会开启一个定时任务定时进行锁过期时间续约,即每次都把过期时间设置成 30s,定时任务 10s执行一次(看门狗)
-
如果设置了过期时间,直接把设定的过期时间作为锁的过期时间,然后使用Lua脚本获取锁,没获取到锁的线程会while自旋重入不停地尝试获取锁
-
rLock.lock(10, TimeUnit.SECONDS)指定了解锁时间,Redisson就不会再自动续期,那么如果在线程A业务还没执行完就自动解锁了,这时候线程B获取到锁,继续执行业务,那么等线程A业务执行完释放锁就可能会把线程B的锁删除,当然这种情况Redisson会报异常,但是这种情况是没有把所有线程都锁住的,所以如果要手动设定过期时间需要让过期时间比业务逻辑执行的时间长。
@Test
public void testLock3() {
RLock rLock = redissonClient.getLock("lock_stock");
try{
//rLock.lockAsync();
//10秒自动释放锁
//rLock.lockAsync(10, TimeUnit.SECONDS);
//尝试加锁等待2秒,上锁以后10秒自动释放锁
Future<Boolean> res = rLock.tryLockAsync(2, 10, TimeUnit.SECONDS);
if(res.get()){
System.out.println("加锁成功....");
System.out.println("执行业务....");
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}finally {
rLock.unlock();
System.out.println("释放锁....");
}
}
RLock对象完全符合Java的Lock规范。也就是说只有拥有锁的进程才能解锁,其他进程解锁则会抛出IllegalMonitorStateException错误。但是如果遇到需要其他进程也能解锁的情况,请使用分布式信号量Semaphore 对象。
公平锁(Fair Lock)
定义:
基于Redis的Redisson分布式可重入公平锁也是实现了java.util.concurrent.locks.Lock接口的一种RLock对象。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。它保证了当多个Redisson客户端线程同时请求加锁时,优先分配给先发出请求的线程。所有请求线程会在一个队列中排队,当某个线程出现宕机时,Redisson会等待5秒后继续下一个线程,也就是说如果前面有5个线程都处于等待状态,那么后面的线程会等待至少25秒(总结:排队拿锁)
公平锁代码:
@Test
public void testLock5() {
RLock fairLock= redissonClient.getFairLock("anyLock");
try{
// 最常见的使用方法
fairLock.lock();
}finally {
fairLock.unlock();
System.out.println("释放锁....");
}
}
超时解锁:
// 10秒钟以后自动解锁
// 无需调用unlock方法手动解锁
fairLock.lock(10, TimeUnit.SECONDS);
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = fairLock.tryLock(100, 10, TimeUnit.SECONDS);
...
fairLock.unlock();
Redisson同时还为分布式可重入公平锁提供了异步执行的相关方法:
RLock fairLock = redisson.getFairLock("anyLock");
fairLock.lockAsync();
fairLock.lockAsync(10, TimeUnit.SECONDS);
Future<Boolean> res = fairLock.tryLockAsync(100, 10, TimeUnit.SECONDS);
联锁(MultiLock)
定义
基于Redis的Redisson分布式联锁RedissonMultiLock对象可以将多个RLock对象关联为一个联锁,每个RLock对象实例可以来自于不同的Redisson实例。
代码
RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance2.getLock("lock2");
RLock lock3 = redissonInstance3.getLock("lock3");
RedissonMultiLock lock = new RedissonMultiLock(lock1, lock2, lock3);
// 同时加锁:lock1 lock2 lock3
// 所有的锁都上锁成功才算成功。
lock.lock();
...
lock.unlock();
超时解锁:
Redisson还通过加锁的方法提供了leaseTime的参数来指定加锁的时间。超过这个时间后锁便自动解开了。
RedissonMultiLock lock = new RedissonMultiLock(lock1, lock2, lock3);
// 给lock1,lock2,lock3加锁,如果没有手动解开的话,10秒钟后将会自动解开
lock.lock(10, TimeUnit.SECONDS);
// 为加锁等待100秒时间,并在加锁成功10秒钟后自动解开
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
...
lock.unlock();
红锁(RedLock)
Redis常用的方式有单节点、主从模式、哨兵模式、集群模式,在后三种模式中可能会出现 ,异步数据丢失,脑裂问题,Redis官方提供了解决方案:RedLock,RedLock是基于redis实现的分布式
锁,它能够保证以下特性:
- 容错性:只要多数节点的redis实例正常运行就能够对外提供服务,加锁释放锁
- 互斥性:只能有一个客户端能获取锁,即使发生了网络分区或者客户端宕机,也不会发生死锁
基于Redis的Redisson红锁RedissonRedLock对象实现了Redlock介绍的加锁算法。该对象也可以用来将多个RLock对象关联为一个红锁,每个RLock对象实例可以来自于不同的Redisson实例
RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance2.getLock("lock2");
RLock lock3 = redissonInstance3.getLock("lock3");
RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
// 同时加锁:lock1 lock2 lock3
// 红锁在大部分节点上加锁成功就算成功。
lock.lock();
...
lock.unlock();
超时机制
Redisson还通过加锁的方法提供了leaseTime的参数来指定加锁的时间。超过这个时间后锁便自动解开了。
RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
// 给lock1,lock2,lock3加锁,如果没有手动解开的话,10秒钟后将会自动解开
lock.lock(10, TimeUnit.SECONDS);
// 为加锁等待100秒时间,并在加锁成功10秒钟后自动解开
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
...
lock.unlock();
读写锁(ReadWriteLock)
基于Redis的Redisson分布式可重入读写锁RReadWriteLock Java对象实现了java.util.concurrent.locks.ReadWriteLock接口。其中读锁和写锁都继承了RLock接口。
分布式可重入读写锁允许同时有多个读锁和一个写锁处于加锁状态,即:使用同一个RReadWriteLock加写锁和读锁,多个读锁是需要等待写释放锁才能加锁成功,如下:
@Test
public void testWriteLock() {
//获取读写锁
RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("ReadWriteLock");
//获取写锁
RLock rLock = readWriteLock.writeLock();
try{
//加上写锁,读会等待
rLock.lock();
System.out.println("写锁加锁成功");
Thread.sleep(200000);
System.out.println("处理写业务...");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
rLock.unlock();
System.out.println("释放写锁....");
}
}
@Test
public void testReadLock() {
//获取读写锁
RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("ReadWriteLock");
//获取读锁
RLock rLock = readWriteLock.readLock();
try{
//加上读锁,如果写锁没释放会等待
rLock.lock();
System.out.println("读锁加锁成功");
System.out.println("处理读业务...");
}finally {
rLock.unlock();
System.out.println("释放读锁....");
}
}
如果 testWriteLock 写方法先自行,先加上写锁 ,那么 testReadLock读方法中的加锁代码会等待,直到写锁释放。
当然如果多个线程全是读锁没有写锁那相当于是没有加锁,不会等待,其他情况只要有写锁参与,后执行加锁的线程都要等先执行加锁的线程释放锁,不管是先读还是先写,又或者是写和写。这种锁能够保证读锁能读到的数据始终是写完之后的数据。
Redisson还通过加锁的方法提供了leaseTime的参数来指定加锁的时间。超过这个时间后锁便自动解开了:
// 10秒钟以后自动解锁
// 无需调用unlock方法手动解锁
rwlock.readLock().lock(10, TimeUnit.SECONDS);
// 或
rwlock.writeLock().lock(10, TimeUnit.SECONDS);
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = rwlock.readLock().tryLock(100, 10, TimeUnit.SECONDS);
// 或
boolean res = rwlock.writeLock().tryLock(100, 10, TimeUnit.SECONDS);
...
lock.unlock();
信号量(Semaphore)
基于Redis的Redisson的分布式信号量(Semaphore)Java对象RSemaphore采用了与java.util.concurrent.Semaphore相似的接口和用法。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。
信号量可以看做是在Redis中保存了一个数字,然后可以实现原子性的加或者减,比如说有一商品需要拿100个做秒杀,我们就可以把这个库存数量做成信号量,然后实现原子性加减操作:
@Test
public void testReadLock5() throws InterruptedException {
//获得到一个信号量
RSemaphore semaphore = redissonClient.getSemaphore("semaphore");
//设置信号量的值
boolean setPermits = semaphore.trySetPermits(1000);
System.out.println(setPermits);
System.out.println("可用数量:"+semaphore.availablePermits());
}
@Test
public void testReadLock6() throws InterruptedException {
//获得到一个信号量
RSemaphore semaphore = redissonClient.getSemaphore("semaphore");
//获取 2 个信号量 , 值会减去 2 , 如果获取不到,方法会阻塞
semaphore.acquire(2);
System.out.println("可用数量:"+semaphore.availablePermits());
//尝试获取 2 个信号量 , 值会减去 2 , 如果获取不到,方法不会
boolean tryAccquireSuccess = semaphore.tryAcquire(2);
System.out.println(tryAccquireSuccess);
System.out.println("可用数量:"+semaphore.availablePermits());
}
@Test
public void testReadLock7() throws InterruptedException {
//获得到一个信号量
RSemaphore semaphore = redissonClient.getSemaphore("semaphore");
//释放2个值,数量会加回去
semaphore.release(2);
System.out.println("可用数量:"+semaphore.availablePermits());
}
可过期性信号量(PermitExpirableSemaphore)
基于Redis的Redisson可过期性信号量(PermitExpirableSemaphore)是在RSemaphore对象的基础上,为每个信号增加了一个过期时间。每个信号可以通过独立的ID来辨识,释放时只能通过提交这个ID才能释放。它提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。
RPermitExpirableSemaphore semaphore = redisson.getPermitExpirableSemaphore("mySemaphore");
String permitId = semaphore.acquire();
// 获取一个信号,有效期只有2秒钟。
String permitId = semaphore.acquire(2, TimeUnit.SECONDS);
// ...
semaphore.release(permitId);
闭锁(CountDownLatch)
基于Redisson的Redisson分布式闭锁(CountDownLatch)Java对象RCountDownLatch采用了与java.util.concurrent.CountDownLatch相似的接口和用法。
闭锁可以实现多个线程都执行完才是完成的效果,否则闭锁会等待
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
//设置2个数量
latch.trySetCount(2);
//await方法会等待,等待其他线程 countDown 完成所有的trySetCount(2)次就结束闭锁
latch.await();
// 在其他线程或其他JVM里
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
//完成第1个
latch.countDown();
// 在其他线程或其他JVM里
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
//完成第2个 , 闭锁完成
latch.countDown();
untDownLatch)
基于Redisson的Redisson分布式闭锁(CountDownLatch)Java对象RCountDownLatch采用了与java.util.concurrent.CountDownLatch相似的接口和用法。
闭锁可以实现多个线程都执行完才是完成的效果,否则闭锁会等待
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
//设置2个数量
latch.trySetCount(2);
//await方法会等待,等待其他线程 countDown 完成所有的trySetCount(2)次就结束闭锁
latch.await();
// 在其他线程或其他JVM里
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
//完成第1个
latch.countDown();
// 在其他线程或其他JVM里
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
//完成第2个 , 闭锁完成
latch.countDown();