redis分布式锁

分布式锁

为什么使用分布式锁:

  • 加锁的目的是为了防止代码的重复执行,在单机情况下,可以使用 jvm的锁:lock和synchronized进行加锁
  • 但是在分布式系统下,每个jvm是相互隔离的,JVM锁没有互斥性,所以需要引入第三方进行加锁

常用的分布式锁实现方案:

  • mysql

    • 利用mysql 表的主键或者唯一索引不能重复,插入数据成功代表加锁成功,插入失败代表获取锁失败,执行完成删除表中数据就是释放锁
    • 但因为是磁盘IO,代价比较大,不适合大型项目
  • Redis

  • zookeeper

Redis分布式锁

redis分布式锁的要求

  • 锁需要有独占性,任何时刻有且只能有一个线程持有
  • 并且要保证高可用,redis集群情况下,不能因为某个节点挂了,就出现获取锁和释放锁失败的情况
  • 要有超时机制或者撤销操作,防止死锁
  • 防止锁被其他线程误删
  • 锁需要保证重入性

Redis分布式锁的实现

  • 简单来说Redis实现锁机制其实就是在Redis中设置一个key-value,当key存在时,即上锁,删除key即解锁。
  • 使用 setnx + del ,先加锁用完后释放锁,这种情况如果执行过程中出现异常,可能导致del指令没有被调用,锁会永远得不到释放,就会产生死锁
    • 所以需要给锁增加过期时间,保证锁可以自动释放,但是setnx 和expire 是两条指令,它们不是原子的,如果在设置过期时间时,出现异常,同样会导致锁得不到释放
    • 所以redis 在2.8 版本增加了,set的扩展命令(set ex px nx),使得setnx 和expire 可以一起执行,如果redis版本较低,可以使用lua脚本保证两个指令的原子性
  • 如果没有获取到锁,锁的重试需要暂停一段时间,以减少不必要的空转,但是等待时间需要根据时间业务进行评估,等待时间过长容易造成业务阻塞,时间过短,可能会造成大量的空转,浪费系统资源
  • 此外还有可能会遇到超时问题,也就是锁过期释放了,但业务还没执行完,此时就会有其他线程获取到新的锁
    • 这时需要增加一个看门狗,定期检查业务线程有没有执行完,如果没有就要续锁,防止业务没有执行完,锁被释放掉了
    • 但是如果在需要续锁的时候,jvm进行垃圾回收了,触发STW机制,导致续锁不及时,key仍然有过期风险,可以通过 zookeeper 解决
  • 也有可能原本的程序执行完了,去删除锁时,自身的锁过期了,从而误删除了其他线程加的锁
    • 所以设置一个唯一的value值,在删除锁时,需要先判断value值是不是自己的,如果不是就不能删除,防止删除了其他线程的锁
    • 但是匹配value和删除key又不是一个指令,这里就需要使用lua脚本了
  • 此外还要保证锁的重入性,但是并不推荐redis使用可重入锁,它加重了客户端的复杂性,调整业务结构完全可以避免可重入锁

锁的重入性

  • 一个线程获取到锁后,再进入该线程的内部方法如果还需要获得相同的锁,就会自动获得锁,不会因为之前获得过还没有释放而阻塞,可重入锁能再一定程度上避免死锁
  • redis分布式锁想要保证可重入性,就需要使用hash结构,使用String无法保证可重入性,加锁解锁操作都需要使用lua脚本保证原子性

看门狗

  • 锁过期释放了,但业务还没执行完,就需要锁的续期,防止任务没有执行完,锁被释放掉了,同样需要lua脚本

如果redis客户端加锁请求失败:

  • 可以直接抛出异常,这种适合由用户发起的操作,用户看到错误后,可以点击重试
  • 或者让线程休眠一会儿,然后在重试,但是这种会阻塞消息处理线程,如果队列里的消息很多,sleep并不合适
  • 或者将请求转移到延时队列,过一会儿在试

lua

  • 轻量级的脚本语言,C语言编写,嵌入应用程序中,为程序提供灵活的扩展和定制功能

  • redis调用lua脚本,通过eval命令保证redis命令的原子性,用return返回脚本执行后的结果,例如:

    -- eval后面跟的是脚本, 一个redis.call()代表一个redis命令
    eval " redis.call('set','test','1234')  redis.call('expire','test','60')  return redis.call('get','test') " 0
    -- 上面的脚本是写死的,用动态参数代替后为:
    eval " redis.call('set',KEYS[1],ARGV[1])  redis.call('expire',KEYS[1],'60')  return redis.call('get',KEYS[1]) " 1  test  1234
    
  • lua脚本参数说明

    eval "return redis.call('mset','k1','v1','k2','v2') " 0
    -- 上面的脚本是写死的,用动态参数代替后为: 
    -- KEYS代表key, ARGV代表value,,下标都是从1开始
    -- 2代表参数的个数,后面的key和value要一一对应
    eval "return redis.call('mset',KEYS[1],ARGV[1],KEYS[2],ARGV[2]) " 2  k1  k2 v1 v2
    
    -- if语法说明 ,有if一定有end,除了最后一个else都有then
    if (布尔条件) then
        业务代码
    elseif (布尔条件) then
    	业务代码
    else 
        业务代码
    end
    
  • redis官网的lua脚本如下:

    -- 获取key的value值,和输入的value进行比较,相同就删除key并返回删除结果,否则返回0
    if redis.call("get",KEYS[1]) == ARGV[1] then
        return redis.call("del",KEYS[1])
    else
        return 0
    end
    

    改写后为:注意单双引号

    eval " if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else  return 0 end" 1 testLock 1234
    

    hash类型的分布式锁 lua脚本

  • 加锁

    • 先判断key是否存在,不存在就加锁,存在就判断锁是否是自己的
    • hash做分布式锁, >
    • 返回1说明加锁成功,返回0加锁失败
-- 第一个条件是锁存在,第二条件是锁的value存在
if redis.call('exists',KEYS[1])==0 or redis.call('hexists',KEYS[1],ARGV[1])==1 then
   -- hincrby包含了hset, key2是uuid ,1是增长的步长值
	redis.call('hincrby',KEYS[1],ARGV[1],1)
   --设置过期时间
    redis.call('expire',KEYS[1],ARGV[2])
    return 1  
else
    return 0   
end

-- 缩进后:
eval "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" 1 testhashLock 1111 60

  • 解锁
    • 先判断有没有锁,没有锁返回null
    • 有锁,就减少锁的加锁次数,返回0
    • 如果加锁的次数为0,就删除锁 ,删除锁后返回1
-- 锁不存在
if  redis.call('hexists',KEYS[1],ARGV[1])==0 then
    return nil
--减少锁的加锁次数,如果加锁的次数为0,就删除锁  
elseif redis.call('hincrby',KEYS[1],ARGV[1],-1)==0 then    
    return  redis.call('del',KEYS[1])
else    
    return 0   
end


eval "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" 1 testhashLock 1111

自动续期 lua 脚本

-- 过了一段时间后,发现锁还在,就要续期
if  redis.call('hexists',KEYS[1],ARGV[1])==1 then
    return  redis.call('expire',KEYS[1],ARGV[2])
else    
    return 0   
end

--测试,先加一个key,设置过期时间再续期

hset  testhashLock 1111  1
expire  testhashLock  60
ttl testhashLock

eval "if  redis.call('hexists',KEYS[1],ARGV[1])==1 then  return  redis.call('expire',KEYS[1],ARGV[2]) else  return 0   end" 1 testhashLock 1111 60

Redlock

redis的红锁方案(Redlock):

  • redis分布式锁,在主从情况下是不可靠的,因为redis的复制是异步的,所以即使是在集群模式下运行,如果在加锁时 redis 主节点故障,从节点还未来得及同步锁就升级为主节点,此时其他线程就可以再次加锁,这样就会导致多个线程都持有锁,可以通过红锁解决

  • 红锁本质上就是使用多个Redis做锁,官网的示例采用的是5个节点,这些节点完全互相独立,没有主从关系,和很多分布式算法一样,红锁也采用大多数机制

  • 在奇数台redis上加锁,一次锁的获取,会对每个请求都获取一遍,如果获取锁成功的数量超过一半,则获取锁成功,反之失败;

  • 红锁的问题:

    • 如果有节点宕机了,需要等待红锁的key过期或者释放掉才能重启,否则也有可能加锁失败代码重复执行
    • 会增加系统复杂度,而且会降低效率

redisson的git地址:

https://github.com/redisson/redisson

红锁的设计理念

  • 多个节点完全互相独立,没有主从关系,也不使用复制和其他隐式协调系统
  • 获取客户端锁时,首先获取当前时间,
  • 依此获取5个实例,使用相同的key和随机值获取锁,客户端设置一个超时时间,超时时间应该小于锁的失效时间,可以防止客户端与一个宕机的redis节点长时间处于阻塞状态,如果一个redis服务器不可用,就要去请求其他redis获取锁
  • 客户端通过当前时间减去最开始记录的时间,来计算获取锁使用的时间,只有大多数的redis节点(N/2 +1)都获取到锁,并且获取锁的时间小于锁的失效时间,才算锁获取成功
  • 所以锁的有效时间是,初始有效时间减去获取锁使用的时间
  • 如果没能获得锁,需要在所有的redis实例上进行解锁,因为可能部分加锁成功,部分加锁失败

红锁节点个数:N = 2 X +1

  • N是最终部署的机器数
  • X是容错机器数,也就是宕机多少台红锁依然可用
  • 因为要保证过半的节点可用,所以奇数台是经历的方法

redisson

  • redis官方给的红锁实现,封装了分布式锁的实现,底层使用的是hash结构保证了锁的可重入性,还使用了lua脚本保证原子性,锁的超时时间默认是30s
  • redisson支持MultLock机制,可以将多个锁合并为一个大锁,然后禁止统一管理,加锁和解锁
  • 加锁流程:
    • 通过exists判断,锁如果不存在,就设置值和过期时间,加锁成功
    • 如果锁已存在,通过hexists判断,锁的是当前线程,就是可重入锁,加锁成功
    • 如果锁已存在,通过hexists判断,锁的不是当前线程,加锁失败,返回锁的过期时间
  • 解锁流程:
    • 通过hexists判断,释放锁的线程和持有锁的线程是不是同一个,不是就返回null,
    • 是就减少锁的加锁次数,并刷新锁的过期时间
    • 如果加锁的次数为0,就删除锁 ,并发布解锁的消息后
  • 还会额外启动一个线程,也就是看门狗,它的作用是在redisson实例关闭前,来定期检查线程是否还持有锁,如果持有锁,就刷新锁的过期时间(30s),看门狗刷新的频率是锁过期时间的1/3,也就是10s刷新一次
  • 此外redisson还提供了参数,指定加锁时间,超过这个时间后锁会自动解开,即使不释放锁也会解开,防止了死锁
  • 多机情况下,ReadLock已被弃用,推荐使用 MultLock,MultLock符合juc的lock规范

示例:

    @Resource
    private Redisson redisson;

	public BaseResultModel reduce() {
        RLock lock = redisson.getLock(COMMODITY_KEY_LOCK_REDISSON);
        //加redis分布式锁
        lock.lock();
        
        try {

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //判断锁是否是当前线程的
            if (lock.isLocked()&&lock.isHeldByCurrentThread()){
                //释放redis分布式锁
                lock.unlock();
            }
        }
        return BaseResultModel.success("执行成功“);
    }

你可能感兴趣的:(redis,redis,分布式,数据库)