redis之分布式锁(四)

发布时间:2024年01月02日

一.为什么需要分布式锁

在分布式的情况下,比如:会员服务,可能会有多个,然而单个服务的加锁行为,只能锁住一个服务,这样就会出现问题。
在这里插入图片描述

二.分布式锁的基本原理

在这里插入图片描述

三.分布式锁的实现形式

分布式锁重点:保证 加锁解锁 的原子性。

1.使用 redis的set命令

set key value [EX seconds][PX milliseconds][NX|XX]
// EX:过期时间单位是秒。
// PX:过期时间单位是毫秒。
// NX:key不存在,才会放。
// XX:key存在,才会放。
实现逻辑1
使用 
1.获取锁(set lock ikun nx)
2.设置过期时间(Expire lock 603.处理业务
4.释放锁
问题:如果在获取锁之后,如果断网了,那么就不会设置过期时间,就会造成死锁。
实现逻辑2
解决实现逻辑1的问题
1.获取锁和设置过期时间同时(原子性)set lock ikun ex 20 nx
2.处理业务
3.释放锁
问题:如果业务执行的时间大于设置的过期时间,然后锁已经释放,可能被别的线程获取到了,然后咱们再去执行删锁的时候,就会删除别人的锁。
实现逻辑3
解决实现逻辑2的问题
1.获取锁和设置过期时间同时(原子性)set lock uuid ex 20 nx
2.处理业务
3.判断是否是自己的锁(使用UUID4.释放锁
缺点:判断是自己的锁,然后准备去释放锁,然后锁过期自动释放,在3)完成,4)还没执行的时候,有线程又重新设置值,然后我们再去删锁,那我们就删了别人的锁。
实现逻辑4(使用redis和Lua脚本实现)
解决实现逻辑3的问题
1.获取锁和设置过期时间同时(原子性)set lock uuid ex 20 nx
2.处理业务
3.Lua脚本释放锁(原子性)

## java代码中使用Lua脚本
	//使用get命令取到key然后和传进来的key作对比,相等就删掉返回1,否则就返回0。(要么全成功,要么全失败)
	String lua = "if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end";
	//执行脚本  Arrays.asList("key")相当于KEYS[1],uuid相当于ARGV[1]
	Integer result = redisTemplate.execute(new DefaultRedisScript<Integer>(lua,Integer.class),Arrays.asList("key"),uuid);

四.使用Redisson

1.添加pom文件

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.25.2</version>
</dependency>

2.配置redisson

@Configuration
public class RedissonConfig {
	@Bean(destroyMethod="shutdown")
	public RedissonClient redisson() throws IOException {
		//1.创建配置
		Config config = new Config();
		//2.配置单机的redisson连接  redis://必须加,如果启用SSL(安全连接),必须加rediss://
		config.useSingleServer()
				.setAddress("redis://redis地址:端口号")
				.setPassword("redis密码")
                .setDatabase(0);//使用redis的第几个库
       //3.返回RedissonClient对象
       RedissonClient redissonClient = Redisson.create(config);
       return redissonClient; 
	}
}

五.Redisson-lock(重入锁)

1.lock的两大特点

//Lock锁实例
RLock lock = redisson.getLock("ikunLock");
//最常见用法(会自动续期)
lock.lock();//手动设置过期时间(不会自动续期)
lock.lock(22,TimeUnit.SECONDS)

//尝试加锁,最多等待100S,上锁以后10S自动解锁
boolean flag = lock.tryLock(100,10,TimeUnit.SECONDS)

(1)看门狗-锁在运行期间,自动进行续期。
(2)为了防止死锁,默认是30S的过期时间。

2.看门狗的原理

看门狗的原理: 只要占锁成功,会开启一个定时任务,锁超时就会重新给默认的过期时间30S,续期时间 = 默认的过期时间/3 = 10S。默认过期时间也就是看门狗时间。

六.Redisson-lock(读写锁)

分布式可重入读写锁允许有多个读锁和一个写锁:写锁没释放,读就必须等待。可以包装拿到的数据一定是最新的值。
RReadWriteLock rwLock = redisson.getReadWriteLock("ikunLock");
//读锁
rwLock.readLock().lock();
//写锁
rwLock.writeLock().lock();
//也可以设置自动时间和最大尝试时间

1、读 + 读: 相当于无所。
2、写 + 读: 等待写锁释放。
3、读 + 写 : 写需要等待。
4、写 + 写 : 等待上一个写锁释放。
只要有写锁,都需要等待。

七.闭锁

举一个例子:坤哥开演唱会,只有听演唱会的小黑子走了,保安才能关门。
public String guanmen(){
	RCountDownLatch latch = redisson.getCountDownLatch("ikun");
	//一共两个人
	latch.trySetCount(2);
	//等待
	latch.await();
	return "小黑子走了,开心";
}

public String sanhui(){
	RCountDownLatch latch = redisson.getCountDownLatch("ikun");
	//走出演唱会
	latch.countDown(1);
	return "见不到哥哥了,难受";
}

执行guanmen()方法,然后必须执行两遍sanhui()方法,guanmen()方法才会执行完毕,不然会一直等待。

八.redisson信号量

举一个例子:去看坤哥开演唱会,找地方停车,停车场有2.5*2个位置。(可以进行限流)
public String stop(){
	RSemaphore stop = redisson.getSemaphore("ikun");
	//尝试获取车位
	boolean flag = stop.tryAcquire();
	if(flag){
	//有车位,停车
	}else{
		return "没有车位";
	}
	return "停车成功,开心";
}

public String out(){
	RSemaphore out = redisson.getSemaphore("ikun");
	//开出车库,释放车位
	out.release();
	return "开出停车场";
}

九.保证数据一致性

1.先写缓存,在写数据库
缺点: 如果写缓存成功,数据库失败。那么数据就是脏数据。

2.先写数据库,在写缓存
缺点: 数据库成功,缓存失败。缓存数据就是旧数据。

3.先删缓存,再写数据库
缺点: 在删除缓存后,网络卡顿还没来的急去写数据库,那么再次取缓存,那还是旧数据。
解决: 缓存双删,网络卡顿之后,写入数据库,然后间隔一段时间再去删除缓存。

4.先写数据库,再删缓存
缺点: 数据库写入之后,还没来的急删除缓存,就被读取到旧值。(但是正常情况下读操作也比写操作更快,所以比较推荐此操作,相对其他方案,问题出现的概率要小)

删除缓存失败:
①使用定时任务,存放进数据库,进行重试。重试到一定次数记录失败,等待后续处理,任意重试成功就返回成功。实时性没那么高。

②使用mq,存放到mq消息里面,进行处理,重试到一定次数,加入到死信队列。实时性比较高。

③使用canal(阿里开源的中间件,可以当做是数据的从服务器),数据库改变,canal就会记录什么数据改变,然后再去更新缓存。还可以解决数据异构问题:就是淘宝每个人推荐的东西都不一样,因为canal记录了你的浏览记录然后结合商品表去做计算,然后给你推送你需要的东西。

更多配置请看redisson文档

文章来源:https://blog.csdn.net/twotwo22222/article/details/134080734
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。