Redis分布式锁的实现和分析

关于 synchronized

项目单独部署时,使用 synchronized 可以实现并发安全,但如果项目搭建集群时,有多个线程同时对某项目中的数据修改时,可能会出现并发问题。

synchronized 关键字底层是 JVM 中的 monitor(管程,监视器)管理的。一个JVM对应一个 monitor 。所以在分布式项目中,不同的环境下,synchronized 对应的 monitor 可能不同。

关于 synchronized 底层原理的理解见 JUC并发编程——对于synchronized关键字的理解

分布式锁的实现

使用 setnx(SET if Not eXist) 可以实现分布式锁

SETNX :如果 key 不存在,设置 value。如果 key,设置 value 失败。

但是只使用 setnx 命令,存在线程安全问题。

解决方法:SET key value NX EX seconds

存在的问题:超时释放锁导致误删其他锁。

如果线程1先获取到了锁,由于一些原因线程1阻塞,一段时间后Redis 超时释放了锁,
线程2获取锁成功,还未到过期时间,此时线程1恢复运行,删除了线程2的锁,
线程2非自然释放后,线程3获取锁成功,执行业务。
在这里插入图片描述

解决方法:在原有的代码中获取锁时,存入线程标识,在释放锁前判断标识是否是自己

存在的问题:无法保证判断锁标识和释放锁的原子性

线程1在判断锁标识成功后,由于一些原因进入阻塞(如jvm 的 full gc 会stw),在阻塞这段时间内,释放了线程1的锁,线程2获取锁,线程1醒来后已经判断过是否是自己锁,所以会直接释放线程2的锁。

在这里插入图片描述

解决方法:Redis 调用 lua 脚本

-- 比较线程标识与锁中的标识是否一致
if (redis.call('get',KEYS[1]) == ARGV[1]) then
    -- 一致 释放锁
    return redis.call('del',KEYS[1])
end
-- 不一致 直接返回
return 0	
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;

   /**
    * 让 lua 脚本在类加载时加载
    */
   static {
       UNLOCK_SCRIPT = new DefaultRedisScript<>();
       UNLOCK_SCRIPT.setLocation(new ClassPathResource("FreeLock.lua"));
       UNLOCK_SCRIPT.setResultType(Long.class);
   }
   /**
    * Redis 支持lua 脚本功能,在一个脚本中编写多条Redis命令,确保命令执行时的原子性
    */
   @Override
   public void unlock() {
       // 调用 lua 脚本
       stringRedisTemplate.execute(
               UNLOCK_SCRIPT,
               Collections.singletonList(KEY_PREFIX + name),
               ID_PREFIX + Thread.currentThread().getId());
   }

存在的问题:不可重入

解决方法:使用 Redisson 实现分布式锁

  1. 引入 Redisson 依赖
    <dependency>
    	<groupId>org.redisson</groupId>
    	<artifactId>redisson</artifactId>
    	<version>3.13.6</version>
    </dependency>
    
  2. 配置 Redisson 客户端
    @Configuration
    public class RedissonConfig {
        @Autowired
        private RedisConfigProperties redisConfigProperties;
    
        @Bean
        public RedissonClient redissonClient() {
            // 配置
            Config config = new Config();
            // 设置单节点的redis
            config.useSingleServer()
                    .setAddress("redis://" + redisConfigProperties.getHost() + ":" + redisConfigProperties.getPort())
                    .setPassword(redisConfigProperties.getPassword());
            // 创建 Redisson 对象
            return org.redisson.Redisson.create(config);
        }
    }
    
  3. 在业务中使用 Redisson 分布式锁
    @Autowired
    private RedissonClient redissonClient;
    
    public void testRedisson throw Exception {    
        RLock lock = redissonClient.getLock("lock:order:" + userId);
        /**
         * 通过 Redisson 获取分布式锁
         */
        boolean isLock = lock.tryLock();
        if (!isLock){
    		log.debug("执行业务");
        }
        try {
    		log.debug("执行业务");
        } finally {
            lock.unlock();
        }
    }    
    

存在的问题:可能出现主从不一致

在 Redis 集群中,如果Redis 主机宕机,在进行故障转移时,Redis 中主从同步是异步的,所以可能在 Redis 主机中还没有将 实现分布式锁的 key 传给副本,Redis 主机就宕机了,这时另一台客户端访问 Redis 时就访问到了主机实现分布式锁中的资源。所以这是不安全的😱。
在这里插入图片描述

解决方法:使用 Redisson 获取 Redis 集群中的所有锁,进行联锁。

为了实现主从一致性,获取Redis 集群中的所有锁,进行联合。删除锁和获取锁都会同时在 Redis 集群中创建,且进行联锁的 Redis 服务,不存在主从关系(可以都认为是主节点),且支持重入,超时释放,可重试,主从一致。

Redisson 可重入锁原理

与 juc 中的 ReentrantLock 相比,
ReentrantLock 底层实现可重入的原理是是实现了 AbstractQueuedSynchronizer(AQS),通过修改锁的状态 state 控制锁的释放,保证了可重入。获取锁 state++,释放锁 state–,当state == 0时,就真正释放锁。

Redisson 底层使用 Redis 的哈希结构存储,state 作为 hash 字段的 value,从而进行自增或自减。
在这里插入图片描述
设置锁的有效期时,如果没有指定有效期,默认 leaseTime == -1,开启 watchDog 机制,重复更新锁的有效期(默认是30秒)。指定有效期则不使用 watchDog 机制。

  • 28
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值