操作小记:进入github的redisson后,选择如下图,可以进入Redisson的项目介绍(中文文档)
一、分布式锁的概念
分布式锁的目的是:在分布式环境下,为了某一操作(从数据库中,插入一条数据)只执行一次,避免数据操作的重复导致的一系列问题,使用了Redis中的setnx命令和lua脚本,来实现分布式锁,解决以上问题。
二、使用setnx命令的几种情况
2.1、如果业务代码出现异常,执行释放锁(删除锁)的命令不能执行,导致死锁(没有释放锁),该怎么办?
解决办法是:可以用try{}finally{}来解决,但是如果执行到业务代码,机器突然断电了,该怎么办?
最终解决办法是:设置锁的过期时间,即使没有删除锁,到期锁自动就会删除
2.2、设置锁和设置过期时间之间进行闪断(宕机),也就依然成为了死锁,该怎么解决?
解决办法是:用Redis的setnx ex命令,该命令是设置锁+设置过期时间的合并,也是一个原子操作。
2.3、删除锁的时候,由于业务代码超时,导致锁提早过期,删除了别人的锁,该怎么办?
2.4、如果业务超时的话,也会导致其他线程拿到该锁,进入该业务操作,导致业务重复操作
解决以上问题:占锁的时候,将值设为uuid,删除的时候,进行比对
2.5、由于比对uuid的值(要从Redis中获取值),并且删除该值,不是原子操作,在获取uuid值时,可能费时,导致最后删除的别的线程的锁,该怎么办?
解决办法:比对uuid值+比对成功后删除值=原子操作,可以通过Redis的官方文档知道,可以使用lua脚本
2.6、最后形态如下:加锁和解锁的时候,保证原子性
最终代码:
@Test
public void testRedisHadoop(){
String uuid = UUID.randomUUID().toString();
// 占锁(设置值),并且设置过期时间。这是一个Redis的原子命令
// NX和EX
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
if(lock){
// 加锁成功,执行业务
try{
System.out.println("业务执行。。。。");
}finally {
// 获取值对比+对比成功处理=原子操作------lua脚本
// 通过uuid来删除锁,以防删除别人的锁或者
// String lockValue = (String) redisTemplate.opsForValue().get("lock");
// if(uuid.equals(lockValue)){
// redisTemplate.delete("lock");
// }
//lua脚本
String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
"then\n" +
" return redis.call(\"del\",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";
//删除锁
Object lock1 = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
}
}else {
System.out.println("获取锁失败,继续尝试");
testRedisHadoop();//自旋锁
}
}
三、Redisson的分布式锁--------可重入锁
如果a方法内调用了b方法,a加了一把标志为1的锁,b也加了一把标志为1的锁。
如果是**可重入锁,那么执行a方法时,里面的b方法不用等a释放锁,直接拿来a方法的锁使用。
如果是不可重入锁,**那么执行a方法时,里面的b方法需要等a释放1锁后,才去拿1锁,然后才执行。
3.1、可重入锁的两个加锁重载方法(具体如下图)
四、redis的读写锁
作用是:获得最新的数据
写锁是一个排它锁(互斥锁,独享锁),读锁是一个共享锁。
写锁在操作时,读锁只能等待。如果没有写锁在操作,读锁就是共享的,和没加锁是一样的。
4.2、读写锁补充说明
读+写:有读锁,写需要等待。=======当读锁正在进行时,写锁需要等待读锁释放(执行完),才能执行。
读+读:相当于无锁状态。所有的当前的读锁,他们都会同时加锁成功。
写+读:读锁等待写锁释放
写+写:阻塞方式(必须拿到为止)
总结:只要有写锁,都必须等待。