RedisTemplate → RedisLockRegistry → Redisson → RedLock 进化史

原博客链接:Notion – The all-in-one workspace for your notes, tasks, wikis, and databases.

graph LR
   A["Jendis"] --> D["Spring Data Redis"]
	 B["Lettuce"] --> D
	 D --> E["RedisTemplate"]
	 D --> F["RedisLockRegistry"]
	 E --> G["Redisson"]
	 F --> H["RedLock"]

<aside> ⚠️ Redis分布式锁有普及的有两种实现RedisLockRegistryRedisson. RedisLockRegistry对锁的续期没有处理,而Redisson提供了watchDog机制,RedLock等,

</aside>

jendis和lettuce都是java链接redis的驱动包,跟mysql的jdbc一样。RedisTemplate对redis的基础客户端进行封装 Jendis → Lettuce → Spring Data Redis(RedisTemplate/RedisLockRegistry)→ (Redisson/RedLock)

RedisLockRegistry 源码解析

开头先进行源码的解析,透一透。 这个jar包的可读性太高了,模块十分清晰。

  • RedisConnectionFactory底层加载过程 (RedisConnectionFactory 从何而来?)

    <aside> 💡 加载LettuceConnectionConfiguration / JedisConnectionConfiguration 配置类, 获取LettuceConnectionFactory / JedisConnectionConfiguration 相关Bean

    </aside>

    父类是没有通过@Bean进行注入的,仅仅为子类初始化时提供了调用。

    • 首先你在maven中导入了spring-boot-starter-data-redis的坐标,所以在项目加载的时候会通过配置类进行基本的初始化。
    • RedisAutoConfiguration类开始自动配置,先初始化并加载RedisProperties,读取配置文件初始化相关属性,存入IOC容器配置文件
    • 再通过@Import({ LettuceConnectionConfiguration.class**,** JedisConnectionConfiguration.class })导入其他两个客户端配置类。
      • 触发@Import配置类加载,得到RedisConnectionFactory (Lettuce/JedisConnectionFactory)的Bean对象
        • 加载时获取上层bean加载到的RedisProperties配置文件进行初始化,从而得到super入参需要的类。
          • 因为LettuceConnectionConfiguration或者Jedis…..都是继承了 RedisConnectionConfiguration。 触发配置类的构造方法后,会先调用父类的super构造方法进行初始化,同时为了简化冗余代码,调用super进行公共父类初始化加载,为Jedis与Lettuce提供一些公共属性与方法等。
        • 继续进行初始化,在@Bean的redisConnectionFactory方法中,就调用父类中的公共方法(获取一些配置文件的属性等)进行 加载,进行自定义的redis客户端的加载了。
        • 最后将LettuceConnectionFactory装入IOC容器中(这里可以Jedis或者Lettuce,只不过换了个RedisConnectionFactory接口的实现类)
    • 通过@Bean得到redisTemplate/stringRedisTemplate后也会注入IOC容器。 你可以直接在spring中使用这两个类,但都是默认配置,我们一般自己创建后使用。
  • 自己创建配置类

    @Configuration
    public class RedisTemplateConfig {
        @Bean
        public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory factory) {
            RedisTemplate<String, Object> template = new RedisTemplate<>();
            template.setConnectionFactory(factory);
    
            Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
            ObjectMapper om = new ObjectMapper();
            om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
            om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
           
            om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
            //如果java.time包下Json报错,添加如下两行代码
            om.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
            om.registerModule(new JavaTimeModule());
            om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
            jackson2JsonRedisSerializer.setObjectMapper(om);
    
            StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
            
            // key、hash的key ,采用String的序列化方式
            template.setKeySerializer(stringRedisSerializer);
            template.setHashKeySerializer(stringRedisSerializer);
            // value、hash的value,采用jackson2序列化方式
            template.setValueSerializer(jackson2JsonRedisSerializer);
            template.setHashValueSerializer(jackson2JsonRedisSerializer);
            
    		
            template.afterPropertiesSet();
    
            return template;
        }
    
    
    @Configuration
    public class RedisLockConfig {
        private static final String DISTRIBUTED_LOCK = "DISTRIBUTED_LOCK";
    
        @Bean
        public RedisLockRegistry redisLockRegistry(RedisConnectionFactory redisConnectionFactory) {
            return new RedisLockRegistry(redisConnectionFactory, DISTRIBUTED_LOCK);
        }
    }
    
  • RedisLockRegistry() 构造方法

    <aside> ⚠️ RedisLockRegistry的构造方法,初始化一些操作redis的API(StringRedisTemplate),初始化lua脚本 ,自定义分布式锁的名字,过期时间 如果你注入了自己的RedisTemplate的配置类,则可以自动按照你的配置类规则(序列化KV等),底层自动注入

    </aside>

      public RedisLockRegistry(RedisConnectionFactory connectionFactory, String registryKey) {
    		this(connectionFactory, registryKey, DEFAULT_EXPIRE_AFTER);
    	}
    
    	
    	public RedisLockRegistry(RedisConnectionFactory connectionFactory, String registryKey, long expireAfter) {
    		Assert.notNull(connectionFactory, "'connectionFactory' cannot be null");
    		Assert.notNull(registryKey, "'registryKey' cannot be null");
    		this.redisTemplate = new StringRedisTemplate(connectionFactory);
    		this.obtainLockScript = new DefaultRedisScript<>(OBTAIN_LOCK_SCRIPT, Boolean.class);
    		this.registryKey = registryKey;
    		this.expireAfter = expireAfter;
    	}
    
  • obtainLock( )

    		private boolean obtainLock() {
    			Boolean success =
    					RedisLockRegistry.this.redisTemplate.execute(RedisLockRegistry.this.obtainLockScript,
    							Collections.singletonList(this.lockKey), RedisLockRegistry.this.clientId,
    							String.valueOf(RedisLockRegistry.this.expireAfter));
    
    			boolean result = Boolean.TRUE.equals(success);
    
    			if (result) {
    				this.lockedAt = System.currentTimeMillis();
    			}
    			return result;
    		}obtainLock
    
  • tryLock( )

    <aside> ⚠️ tryLock进行加锁的方法,虽然可以重入的方式进行续期,但是不能自动续期(单次执行检测超时续期)

    </aside>

    	 	
        @Override
    		public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
    			long now = System.currentTimeMillis();
    			
    			//对调用的代码进行lock加锁,本地重入锁加锁
    			if (!this.localLock.tryLock(time, unit)) {
    				return false;
    			}
    			
    			try {
    				long expire = now + TimeUnit.MILLISECONDS.convert(time, unit);
    				boolean acquired;
    				
    				//判断是否加锁成功 并且 是否到达获取超时时间 ,如果加锁失败而且也没超时就一直重试 ,如果加锁成功 就返回ture; 如果最后还未加锁成功但是已经超时了,就返回false
    				while (!(acquired = obtainLock()) && System.currentTimeMillis() < expire) { //NOSONAR
    					Thread.sleep(100); //NOSONAR
    				}
    				
    				//因为分布式锁加锁失败了,所以也要解除本地重入锁了
    				if (!acquired) {
    					this.localLock.unlock();
    				}
    				
    				//返回分布式锁加锁结果
    				return acquired;
    			}
    			catch (Exception e) {
    				this.localLock.unlock();
    				rethrowAsLockException(e);
    			}
    			return false;
    		}
    		
    		
    		
    		
    		private boolean obtainLock() {
    			//执行脚本 底层就是调用reids的evalsha命令 ,(脚本,keys,args)
    			Boolean success =
    					RedisLockRegistry.this.redisTemplate.execute(RedisLockRegistry.this.obtainLockScript,
    							Collections.singletonList(this.lockKey), RedisLockRegistry.this.clientId,
    							String.valueOf(RedisLockRegistry.this.expireAfter));
    
    			boolean result = Boolean.TRUE.equals(success);
    
    			if (result) {
    				//设置一下获取到锁的时间,提供有需要时候的查询,重入的话就是最后一次续期时间
    				this.lockedAt = System.currentTimeMillis();
    			}
    			return result;
    		}
    
    RedisLockRegistry.this.obtainLockScript,  //加载脚本
    
    Collections.singletonList(this.lockKey),   //keys  将要加锁的keys的转为列表,即使只加锁一个key
    
    RedisLockRegistry.this.clientId,  //args1  生成的全局唯一UUID
    
    String.valueOf(RedisLockRegistry.this.expireAfter)  //args2 默认60000L过期
    
    --这个lua脚本就是保证了查询和加锁,两次请求进行原子性的绑定
    	private static final String OBTAIN_LOCK_SCRIPT =
    			"local lockClientId = redis.call('GET', KEYS[1])\\n" +   --先获取对应key的value
    					"if lockClientId == ARGV[1] then\\n" +               --判断获取到的value是否为之前加已经加锁set的value
    					"  redis.call('PEXPIRE', KEYS[1], ARGV[2])\\n" +     --如果是,PEXPIRE key 6000 进行续期
    					"  return true\\n" +
    					"elseif not lockClientId then\\n" +
    					"  redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2])\\n" +  --第一次加锁的会到此,执行SET key UUID PX 6000,对一个key进行加锁,时间为60秒
     					"  return true\\n" +
    					"end\\n" +
    					"return false";
    
  • unlock( )

    <aside> ⚠️ 因为在加锁的时候,采用了加锁的自定义标识+UUID,保证了加锁时候的唯一性 (即使因为超时错误释放原因,导致分布式环境中出现了两把锁,但是也不会错误释放掉别人的锁), 所以在解锁的时候为了提升效率,就不必再lua进行比对后删除,提升了删除的效率。

    </aside>

     		@Override
    		public void unlock() {
    			if (!this.localLock.isHeldByCurrentThread()) {
    				throw new IllegalStateException("You do not own lock at " + this.lockKey);
    			}
    			
    			//因为支持重入锁,所以本地ReentrantLock进行解锁。多次重入多次解锁
    			if (this.localLock.getHoldCount() > 1) {
    				this.localLock.unlock();
    				return;
    			}
    			try {
    				if (!isAcquiredInThisProcess()) {
    					throw new IllegalStateException("Lock was released in the store due to expiration. " +
    							"The integrity of data protected by this lock may have been compromised.");
    				}
    				
    				//判当前断线程是否被阻塞,支持线程池异步解锁(删除redis中的key)
    				if (Thread.currentThread().isInterrupted()) {
    					RedisLockRegistry.this.executor.execute(this::removeLockKey);
    				}
    				else {
    					//当前线程直接解锁
    					removeLockKey();
    				}
    
    				if (LOGGER.isDebugEnabled()) {
    					LOGGER.debug("Released lock; " + this);
    				}
    			}
    			catch (Exception e) {
    				ReflectionUtils.rethrowRuntimeException(e);
    			}
    			finally {
    				//分布式锁释放之后,解除掉最后一把ReentrantLock锁,进行本地重入锁彻底释放
    				this.localLock.unlock();
    			}
    		}
    
    		//直接调用redisTemplate进行删除即可,不必再像自己RedisTemplate实现分布式锁,lua比对校验解锁的方式了。
    		private void removeLockKey() {
    			//先尝试unlink,不支持就del
    			if (this.unlinkAvailable) {
    				try {
    					RedisLockRegistry.this.redisTemplate.unlink(this.lockKey);
    				}
    				catch (Exception ex) {
    					LOGGER.warn("The UNLINK command has failed (not supported on the Redis server?); " +
    							"falling back to the regular DELETE command", ex);
    					this.unlinkAvailable = false;
    					RedisLockRegistry.this.redisTemplate.delete(this.lockKey);
    				}
    			}
    			else {
    				RedisLockRegistry.this.redisTemplate.delete(this.lockKey);
    			}
    		}
    

Redisson

  • MultiLock

    连锁的特性 失败情况为0

    请求时间相等

    红锁重写了连锁的两个方法,失败情况变成了类似zookeeper的paxos算法的过半一致,请求时间变成了 [ 请求时间,过期时间*1.5 ],区间的随机数

    被严格保护,即使是其他的方法中,需要使其中的一个Key,此时也只能互斥的不能获取资源。

    一个订单中有多种商品,提交订单的时候,每种商品的库存需要被扣除。这种场景就需要用**MultiLock,**而不是RedLock 单独的key形成的lockKey,不同业务方法同一个key也会争抢锁,造成不必要的阻塞。

  • RedLock

    <aside> ⚠️ 已经封装好的RedLock是在Redisson中的,所以下面内容建立在使用Redisson的基础上。

    </aside>

    • 多实例独立,不允许主从
      • 解决了服务加锁后,主redis宕机前,没有及时异步同步加锁信息的问题。(从redis升主后内部没有分布式锁记录,系统中出现多把分布式锁。)
    • redis每条指令都需要持久化
      • 解决了意外重启后,加锁指令没有持久化,(重启的redis丢失了分布式锁的记录,系统中出现多把分布式锁)
    • 集群脑裂,加锁全部失败
      • 当客户端从大多数Redis实例获取锁失败时,应该尽快地释放(部分)已经成功取到的锁,方便别的客户端去获取锁。
    • 主节点越多,半数通过效率越低
      • Redis是CAP中AP架构设计,现在却想实现类似CP的架构。
      • RedLock 继承于 RedissonMultiLock,底层的半数通过机制,实际上还是paxos算法的核心,这种世界上唯一的分布式一致性算法,效率并不高,这样下来还不如使用zookeeper实现分布式锁。
    • 客户端大延迟,不能完全保证自动续期
      • redission虽然采用了catchDog进行自动续期,但这种行为本身就是网络请求操作,没办法保证
    • 时钟跳跃 / 时钟回拨
      • 其实我觉得这个有点太过了,运维人员修改时钟,导致锁提前到达超时释放。

Reference

Redis Client 之 Jedis与Lettuce - 掘金 (juejin.cn)

Redis高级客户端Lettuce详解 - throwable - 博客园 (cnblogs.com)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值