目录
2.2.1:使用redisTemplate.opsForValue().setIfAbsent()
1:回顾以前
在我们的单机服务情况下,高并发的时候,我们采用synchronized和ReentrantLock来保证多线程下的锁竞争。
代码案例如下:模拟10个线程抢5张票。使用了synchronized来保证超卖和线程安全
/**
* 在redis之前 同一个进程我们可以使用synchronized和ReentrantLock加锁
* 但是在分布式环境下 这种情况就不行了
*/
public class Test1 {
//一共10张票
public static int num = 5;
// Lock lock = new ReentrantLock();
public static void main(String[] args) {
Test1 t1 = new Test1();
for (int i = 0; i < 10; i++) {
//10个线程抢票
new Thread(new Runnable() {
@Override
public void run() {
try {
t1.sale();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}).start();
}
}
//方法非static加锁,属于单个对象锁
public synchronized void sale() throws InterruptedException {
if (num > 0) {
System.out.println(Thread.currentThread().getName() + "抢票成功:" + num);
num--;
}else {
System.out.println(Thread.currentThread().getName() + "抢票失败!"+num);
}
}
}
2:redis分布式锁
2.1:Redis分布锁原理依赖
在redis中我们依赖redis的string数据类型中的命令,展示如下
当key不存在的时候设置key的vaule
//命令1:当key不存在的时候设置key的值是value 并且设置key的过期时间
//命令1的缺点,这是两条命令 设置key的值和设置过期时间不是原子性的,需要依赖lua
setnx key value
expire key time
//命令2:在redis的2.6.12版本开始后 redis支持了同时设置值和过期时间
//当key不存在的时候设置key的值是value,并且设置过期时间
//EX seconds 秒
//PX milliseconds 毫秒
//NX:只在键不存在时设置键的值。如果键已经存在,则不做任何操作。
//XX:只在键已经存在时设置键的值。如果键不存在,则不做任何操作。
set key value [EX seconds] [PX milliseconds] [NX|XX]
我们在redis中验证setnx key value如下:
我们在redis中验证set key value [EX seconds] [PX milliseconds] [NX|XX]如下:
2.2:redis的分布式锁SpringBoot实现
2.2.1:使用redisTemplate.opsForValue().setIfAbsent()
/**
* redis的分布式锁实现 伪代码
*
*/
@GetMapping(value = "redisLock1")
public String getLock1(int num) throws InterruptedException {
//设置redisLock1和过期时间100秒
if (Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent("redisLock1", "锁1", 100, TimeUnit.SECONDS))) {
//模拟抢到锁的线程执行任务耗时
Thread.sleep(10000);
System.out.println("并发执行下的 "+num+" :做了一些事情!");
//做了一些事情后删除锁,让分布式线程进来获取锁
redisTemplate.delete("redisLock1");
return num+"抢到了锁!";
}else {
System.out.println(num+"没有抢到锁!");
return num+"没有抢到锁!";
}
}
缺点分析:
缺点1:锁过期时间和线程执行时间不一致导致的问题
我们想一下,如果如果线程1进来,设置锁过期时间是3秒,但是任务执行了10秒,10秒后才能删除锁,那么锁就会提前过期,放他的线程2进来了。线程2设置了锁由于key相等,线程1在10秒结束后就会删除线程2设置的锁。我们该怎么办?😁
2.2.2:谁上的锁只能谁删除
实现方法是:当前线程判断value的值是不是当前前程的value
/**
* redis的分布式锁实现 伪代码
*
*/
@GetMapping(value = "redisLock2")
public String getLock2(int num) throws InterruptedException {
//设置redisLock1和过期时间100秒
//解决谁上的锁 谁来解锁 我们设置锁的value是UUID
String key="redisLock2";
String value = UUID.randomUUID().toString().replace("-", "");
try {
if (Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(key, value, 5, TimeUnit.SECONDS))) {
//模拟抢到锁的线程执行任务耗时 10秒
Thread.sleep(10000);
System.out.println("并发执行下的 "+num+" :做了一些事情!");
return num+"抢到了锁!";
}else {
System.out.println(num+"没有抢到锁!");
return num+"没有抢到锁!";
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
//谁上锁,谁删除 当前线程判断value的值是不是当前前程的value
val o = redisTemplate.opsForValue().get(key);
if (o!=null&&o.toString().equals(value)) {
System.out.println("删除锁:"+redisTemplate.opsForValue().get(key));
redisTemplate.delete(key);
}
}
}
2.2.2的缺点:
1:在2.2.2中还是解决不了锁的过期时间和当前任务的执行时间的差,当锁提前过期的时候,任务没有执行完毕时候,会放进来其他的线程。
2:在2.2.2中的谁上锁谁删除的逻辑中,需要判断当前锁的值然后删除,在finally中的这两个操作也不是原子操作。并发的时候也会出问题,并发嘛,就是要保证数据的一致性,保证数据的一致性,最好要保证对数据的操作具有原子性。
2.2.3:谁上锁谁删除原子操作
使用了lua保证原子性
/**
* redis的分布式锁实现 伪代码
* 谁上锁 谁删除 原子操作使用了lua
*/
@GetMapping(value = "redisLock3")
public String getLock3(int num) throws InterruptedException {
//设置redisLock1和过期时间100秒
//解决谁上的锁 谁来解锁 我们设置锁的value是UUID
String key="redisLock3";
String value = UUID.randomUUID().toString().replace("-", "");
try {
if (Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(key, value, 10, TimeUnit.SECONDS))) {
//模拟抢到锁的线程执行任务耗时 10秒
Thread.sleep(8000);
System.out.println("并发执行下的 "+num+" :做了一些事情!");
return num+"抢到了锁!";
}else {
System.out.println(num+"没有抢到锁!");
return num+"没有抢到锁!";
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
//谁上锁,谁删除 当前线程判断value的值是不是当前前程的value
//优化为原子操作 利用了lua
String lua="if redis.call('get',KEYS[1]) == ARGV[1] then " +
" return redis.call('del',KEYS[1]) " +
"else " +
" return 0 " +
"end; ";
DefaultRedisScript<Long> script = new DefaultRedisScript<>(lua, Long.class);
Object eval = redisTemplate.execute(script, Collections.singletonList(key), value);
if("1".equals(eval.toString())){
System.out.println("锁成功删除");
}else{
System.out.println("锁删除失败");
}
}
}
总结:
为什么要设置随机值?
主要是为了防止锁被其他客户端删除。有这么一种情况:
-
客户端 A 获得了锁,还没有执行结束,但是锁超时自动释放了;
-
客户端 B 此时过来,是可以获得锁的,加锁成功;
-
此时,客户端 A 执行结束了,要去释放锁,如果不对比随机值,就会把客户端 B 的锁给释放了。
在2.2.3中还是解决不了由于任务耗时比较长,但是锁的过期时间短,锁提前删除的问题。
我们可以把锁的过期时间设置的长点,防止万一提前过期,但是又有一个问题,当程序死机了,finally的代码一致执行不了,到时锁迟迟无法删除。
3:redisson分布式锁
为啥推荐使用Redission
redis的分布式锁10大缺点:Redis分布式锁的10个坑-腾讯云开发者社区-腾讯云
1:客户端1加锁的锁key默认生存时间才30秒,如果超过了30秒,客户端1没有执行完毕,还想一直持有这把锁,怎么办呢?(锁过期释放,业务没执行完)
2:锁不可重入,所谓的不可重入,就是当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,会阻塞,不可以再次获得锁。同一个人拿一个锁 ,只能拿一次不能同时拿2
次。
3:在redis集群中,主节点上有锁,但还没有考虑缓存续命,以及Redis集群部署下,异步复制造成的锁丢失:主节点没来得及把刚刚set进来这条数据给从节点,就挂了。从节点开始工作,但是从节点上没有锁,导致锁丢失
为了解决这些缺点,所以使用redisson,使用 Redisson 实现的分布式锁相对于直接使用 Redis 的分布式锁,具有一些显著的优势,尤其是在功能完善性、开发便捷性以及可扩展性方面。以下是 Redisson 实现的分布式锁相对于手动实现 Redis 分布式锁的几个主要优势:
3.1:redisson的优点
1:看门狗机制(watch dog)
加锁后,如果超时了,Redis
会自动释放清除锁,这样有可能业务还没处理完,锁就提前释放了。怎么办呢?
有些小伙伴认为,稍微把锁过期时间设置长一些就可以啦。其实我们设想一下,是否可以给获得锁的线程,开启一个定时守护线程,每隔一段时间检查锁是否还存在,存在则对锁的过期时间延长,防止锁过期提前释放。
客户端1加锁的锁key默认生存时间是30秒,如果超过了30秒,客户端1还想一直持有这把锁,怎么办呢?
简单!只要客户端1一旦加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端1还持有锁key,那么就会不断的延长锁key的生存时间。
redission,采用的就是这种方案, 此方案不会入侵业务代码。
当前开源框架Redisson解决了这个问题。我们一起来看下Redisson
底层原理图吧
只要线程一
加锁成功,就会启动一个watch dog
看门狗,它是一个后台线程,会每隔10
秒检查一下,如果线程一还持有锁,那么就会不断的延长锁key
的生存时间。因此,Redisson
就是使用Redisson
解决了锁过期释放,业务没执行完问题。
3.2:redisson使用
重学SpringBoot3-集成Redis(四)之Redisson-腾讯云开发者社区-腾讯云
1:导入jar
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.23.4</version>
</dependency>
2:创建bean
@Autowired
private RedissonProperties redissonProperties;
@Bean
public RedissonClient redissonClient() throws Exception{
//1:读取配置文件中的redissonProperties
// Config config = Config.fromYAML(redissonProperties.getConfig());
// Redisson.create(config);
// System.out.println("Redisson 已启动8080");
// return Redisson.create(config);
//2:创建配置对象
Config config = new Config();
// 指定使用单节点部署方式
config.useSingleServer().setAddress("redis://localhost:6379").setDatabase(0);
config.useSingleServer().setPassword("123456");
// 创建Redisson客户端实例
return Redisson.create(config);
}
3:springboot代码使用
@RestController
public class RedissonController1 {
@Autowired
RedissonClient redissonClient;
/**
* Redisson的分布式锁 伪代码
* 缺点:一个线程抢到了锁 其他的线程会进行排队
*/
@GetMapping(value = "Rlock1")
public String getLock1(int num) throws InterruptedException {
//解决谁上的锁 谁来解锁 我们设置锁的value是UUID
String key = "Rlock1";
RLock lock = redissonClient.getLock(key);
try {
lock.lock();//加锁
//模拟抢到锁的线程执行任务耗时 10秒
Thread.sleep(10000);
System.out.println("并发执行下的 " + num + " :做了一些事情!");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
System.out.println("解锁");
lock.unlock();//解锁
}
return num + "抢到了锁!";
}
/**
* 此方法更好
* Redisson的分布式锁 伪代码
* tryLock尝试获取锁 超时返回
*/
@GetMapping(value = "Rlock2")
public String getLock2(int num) throws InterruptedException {
//解决谁上的锁 谁来解锁 我们设置锁的value是UUID
String key = "Rlock2";
RLock lock = redissonClient.getLock(key);
// 尝试获取锁,等待时间 2ms,锁定时间 10秒
if (lock.tryLock(0, 10, TimeUnit.SECONDS)) {
try {
Thread.sleep(8000);
System.out.println("并发执行下的 " + num + " :做了一些事情!");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
System.out.println(num+"解锁");
lock.unlock();//解锁
}
} else {
System.out.println(num+"没有抢到锁");
return num+"没有抢到锁!";
} //模拟抢到锁的线程执行任务耗时 10秒
return num + "抢到了锁!";
}
}
总结:lock.lock()方法的源码中我们可以可以看到lockWatchdogTimeout是30秒,也就是默认加锁啥时间是30秒,锁的value是uuid+线程id,底层也是使用了lua脚本同时设置值和过期时间,但是重点在设置完redis之后,开启了线程一个延迟执行(lockWatchdogTimeout/3)10秒的任务,再次去redis中把延迟时间加到30秒,递归调用,只在主线程结束删除key。
private long lockWatchdogTimeout = 30 * 1000;
redisson还有很多其他功能详情见官网。这里只介绍分布式锁的功能
3.3:RedLock(红锁)
使用了redisson感觉好像完美了,自动续期,使用简单,但是还有一个问题,当集群的redis中锁落在的主节点1上,锁没有及时同步到从节点1,主节点1挂了,从节点1上线,从节点1没有了锁。那该怎么办?
Redisson中红锁(RedLock)的实现_redisson红锁-CSDN博客
红锁怎使用?
/**
* Redisson的redLock分布式锁 伪代码
* tryLock尝试获取锁 获取锁的过程中,会多次加锁
* redlock是一种基于多节点redis实现分布式锁的算法,
* 可以有效解决redis单点故障的问题。
* 官方建议搭建五台 redis服务器对redlock算法进行实现。
* 这里使用了3台
*/
@GetMapping(value = "RedLock")
public String getRedLock3(int num) throws InterruptedException {
//解决谁上的锁 谁来解锁 我们设置锁的value是UUID
String key = "RedLock";
//创建多个锁
//锁1
Config config1 = new Config();
config1.useSingleServer().setAddress("redis://127.0.0.1:7000").setDatabase(0);
RedissonClient redissonClient1 = Redisson.create(config1);
RLock lock1= redissonClient1.getLock(key);
//锁2
Config config2 = new Config();
config2.useSingleServer().setAddress("redis://127.0.0.1:7001").setDatabase(0);
RedissonClient redissonClient2 = Redisson.create(config2);
RLock lock2= redissonClient2.getLock(key);
//锁3
Config config3 = new Config();
config3.useSingleServer().setAddress("redis://127.0.0.1:7002").setDatabase(0);
RedissonClient redissonClient3 = Redisson.create(config3);
RLock lock3= redissonClient3.getLock(key);
RedissonRedLock lock= new RedissonRedLock(lock1,lock2,lock3);
// 尝试获取锁,等待时间 2ms,锁定时间 10秒
// 加锁的过程中迭代这三个锁,逐个加锁
//locks.size()/2 + 1 在这里3个所,必须3/2+1=2,2个以上的锁加锁成功才可以
if (lock.tryLock(0, 10, TimeUnit.SECONDS)) {
try {
Thread.sleep(8000);
System.out.println("并发执行下的 " + num + " :做了一些事情!");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
System.out.println(num+"解锁");
lock.unlock();//解锁
}
} else {
System.out.println(num+"没有抢到锁");
return num+"没有抢到锁!";
} //模拟抢到锁的线程执行任务耗时 10秒
return num + "抢到了锁!";
}
redlock的原理
3:基于数据库的分布式锁
基于数据库的分布式锁实现原理:
利用数据库唯一索引或SELECT ... FOR UPDATE排他锁实现互斥性。插入唯一业务ID记录成功视为加锁,删除记录视为解锁。
优化方案:通过版本号实现乐观锁(CAS机制)。
缺点:
性能低,频繁操作易引发锁表风险;
需额外处理锁超时和可重入性问题
4:基于Zookeeper的分布式锁
利用了zookeeper的强一致性,通过Watcher机制监听节点变化实现锁释放通知。
(这个需要写一篇新的博客)