多个进程如果需要修改 MySQL 中的同一行记录时,为了避免操作乱序导致数据错误,此时,我们就需要引入「分布式锁」来解决这个问题了。
想要实现分布式锁,必须借助一个外部系统,所有进程都去这个系统上申请「加锁」。
而这个外部系统,必须要实现「互斥」的能力,即两个请求同时进来,只会给一个进程返回成功,另一个返回失败(或等待)。
这个外部系统,可以是 MySQL,也可以是 Redis 或 Zookeeper。但为了追求更好的性能,我们通常会选择使用 Redis 或 Zookeeper 来做。
想要实现分布式锁,必须要求 Redis 有「互斥」的能力,我们可以使用 SETNX 命令,这个命令表示SET if Not eXists,即如果 key 不存在,才会设置它的值,否则什么也不做。
两个客户端进程可以执行这个命令,达到互斥,就可以实现一个分布式锁。
jdk没有提供分布式锁,只能基于现有技术自己实现或者使用第三方框架的实现
1.基于mysql关系型数据库实现
2.基于redis非关系型数据库
3.基于zookeeper实现
性能:redis > zk > mysql
可靠性:zk > redis == mysql
简易:mysql > redis > zk
追求极致性能:redis
追求极致可靠性:zk
mysql:唯一键索引
kz:znode节点也是唯一性
基于redis实现分布式锁:
redis实现分布式锁特征:
-
独占排他 setnx
//方法之前获取锁 setIfAbsent 如果不存在 设置锁
Boolean lock = this.stringRedisTemplate.opsForValue().setIfAbsent("lock", "111");
if(!lock){
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.testLock();
}else {
//查询redis中的num值
String num = this.stringRedisTemplate.opsForValue().get("num");
if(StringUtils.isBlank(num)){
return;
}
//有就转化int +1
int i = Integer.parseInt(num);
this.stringRedisTemplate.opsForValue().set("num",String.valueOf(++i));
//解锁
this.stringRedisTemplate.delete("lock");
2.死锁问题:
我拿到锁之后,redis客户端蹦了,或者程序异常 就死锁了
解决:给锁添加过期时间 expire 但是拿到锁之后,还没有设置过期时间 服务就崩掉了 依然有死锁问题,所以要保持拿锁和设置过期时间的原子性。
3.原子性:加锁和设置时间: redis指令 set k v ex 3 nx
判断和删除之间也要判断原子性
Boolean lock = this.stringRedisTemplate.opsForValue().setIfAbsent("lock", "111",3,TimeUnit.SECONDS); 可以直接设置过期时间
4.防误删 先判断再删除 这个锁的过期时间,我是不确定的 一旦我程序还没有执行完,我的锁就到期了,这时候我就有可能把别人的锁给删除了 给每个锁保存一个uuid唯一标识 删除的时候判断一下 redis和我的uuid是否一致 ,一致就删除
if(uuid.equals(this.stringRedisTemplate.opsForValue().get("lock"))){
//解锁
this.stringRedisTemplate.delete("lock");
}
但是此时有一种极端情况,我刚判断完是自己的锁,然后这时候lock过期了,别的线程又获取到了锁,我再释放锁,就会出现把别人锁释放掉的情况。所以,判断和删除之间也要判断原子性 lua脚本操作
5.可重入锁 要是a拿到锁了 但是在后续的操作中有需要获取锁来操作,此时如果不支持重入锁,就会一直等啊等,导致死锁。
使用redis里的hset hexist hincrby
6.自动续期:定时任务 + lua脚本 spring定时任务:@Scheduled
juc定时任务线程池
timer定时器
判断自己的锁是否存在,存在则自动续期
if redis.call('hexists', KEYS[1], ARGV[1]) == 1 then return redis.call('expire', KEYS[1], ARGV[2]) else return 0 end
if redis.call('hexists', KEYS[1], ARGV[1]) == 1
then
return redis.call('expire', KEYS[1], ARGV[2])
else
return 0
end
key: lock
arg: uuid 30
lua脚本:redis对lua脚本提供了支持。 lua脚本本身不是原子性,但是redis是单线程,底层io才是多线程
,一次只能一个操作,而一个操作里面包含了判断和删除。
可重入锁:
1.判断是否存在(exist lock) 是就后去 hset 设置过期时间
2.判断是否是自己的 (hexist) 返回值为1 说明是自己的 然后重入(加1)hincrby 并重置过期时间(expire)
可重入锁-加锁
1.判断锁是否存在(exists lock),如果返回值为0,则说明锁不存在 直接获取锁hset 并设置过期时间(expire)
2.判断是否自己的锁(hexists),如果返回值为1,说明是自己的锁 则重入(hincrby)并重置过期时间(expire)
3.否则获取锁失败,返回0
if redis.call('exists', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 1 then redis.call('hincrby', KEYS[1], ARGV[1], 1) redis.call('expire', KEYS[1], ARGV[2]) return 1 else return 0 end
if redis.call('exists', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 1
then
redis.call('hincrby', KEYS[1], ARGV[1], 1)
redis.call('expire', KEYS[1], ARGV[2])
return 1
else
return 0
end
key: lock
arg: uuid 30
可重入锁-解锁
1.判断自己的锁是否存在,不存在则说明恶意释放锁,直接返回nil
2.直接对锁的值进行减1(hincrby -1),如果减1后的值为0,则直接释放锁(del),返回1
3.直接返回0
if redis.call('hexists', KEYS[1], ARGV[1]) == 0 then return nil elseif redis.call('hincrby', KEYS[1], ARGV[1], -1) == 0 then return redis.call('del', KEYS[1]) else return 0 end
if redis.call('hexists', KEYS[1], ARGV[1]) == 0
then
return nil
elseif redis.call('hincrby', KEYS[1], ARGV[1], -1) == 0
then
return redis.call('del', KEYS[1])
else
return 0
end
key: lock
arg: uuid
锁操作:
加锁:方法执行之前
解锁: 方法执行完
重试: 没有获取到锁 递归 递归的时候要注意 方法结束完是否还操作 !
Redlock
至少五个独立redis服务器
Redlock 具体如何使用呢?
整体的流程是这样的,一共分为 5 步:
-
客户端先获取「当前时间戳T1」
-
客户端依次向这 5 个 Redis 实例发起加锁请求(用前面讲到的 SET 命令),且每个请求会设置超时时间(毫秒级,要远小于锁的有效时间),如果某一个实例加锁失败(包括网络超时、锁被其它人持有等各种异常情况),就立即向下一个 Redis 实例申请加锁
-
如果客户端从 >=3 个(大多数)以上 Redis 实例加锁成功,则再次获取「当前时间戳T2」,如果 T2 - T1 < 锁的过期时间,此时,认为客户端加锁成功,否则认为加锁失败
-
加锁成功,去操作共享资源(例如修改 MySQL 某一行,或发起一个 API 请求)
-
加锁失败,向「全部节点」发起释放锁请求(前面讲到的 Lua 脚本释放锁)
还是老实用分布式单机就够了
主要三个方面
拿锁:用set考虑到死锁问题,给锁加过期时间,还会继续出行释放时锁到期问题,会把别人锁释放,所以给value加uuid,判断的时候根据uuid判断。 但是但 我程序内部如果继续想要我这个锁,我还是死锁,所以要可重入,使用redis里的双map结构hset,利用 exist判断锁存不存在,并设置过期时间,用hexist判断是不是自己的锁,是就重入,加一操作,并且重新设置过期时间。 但是这个过期时间吧,我还是不太能确定,所以就要弄个一个定时任务,timer定时器,每次到了我设置过期时间的3分之一啊,多少的时候,我再一次判断当前锁是不是自己的,是就重新设置过期时间。
判断:不存在递归
释放:根据uuid判断是否是自己的锁,再释放锁,但是会有原子性问题,所以,使用lua脚本,发送一个指令,redis单线程,内部也是支持lua脚本,所以就具有原子性。
解锁的时候,判断自己的锁释放存在,存在就减1,如果减1后等于0,就释放锁。
不存在就返回nil。就是null。
Redisson:
内置自己手写的一切分布式锁。。。java独有
1.引依赖
2.配置redis单机模式,配置地址 初始化配置
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient(){
// 1. Create config object
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.231.100:6379");
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
3.使用直接 获取锁对象后 lock,unlock
redisson:读写锁 所有读写锁都是一样,写写不能并发,读写也不能并发,读读可以并发。
和重入锁差不多 都是双map结构
redisson:闭锁 countDownLatch
RcountdownLatch latch=redissonClient.getcountdownlatch("latch");
latch.trySetCount(6);设置数量
latch.await();//等着 只要我设置的6被别人线程减完了 我才可以继续下一步
RcountdownLatch latch=redissonClient.getcountdownlatch("latch");
latch.countdown;//一次减一