分布式锁-redis

多个进程如果需要修改 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实现分布式锁特征

  1. 独占排他 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 步:

  1. 客户端先获取「当前时间戳T1」

  2. 客户端依次向这 5 个 Redis 实例发起加锁请求(用前面讲到的 SET 命令),且每个请求会设置超时时间(毫秒级,要远小于锁的有效时间),如果某一个实例加锁失败(包括网络超时、锁被其它人持有等各种异常情况),就立即向下一个 Redis 实例申请加锁

  3. 如果客户端从 >=3 个(大多数)以上 Redis 实例加锁成功,则再次获取「当前时间戳T2」,如果 T2 - T1 < 锁的过期时间,此时,认为客户端加锁成功,否则认为加锁失败

  4. 加锁成功,去操作共享资源(例如修改 MySQL 某一行,或发起一个 API 请求)

  5. 加锁失败,向「全部节点」发起释放锁请求(前面讲到的 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;//一次减一
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值