原博客链接: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分布式锁有普及的有两种实现RedisLockRegistry和Redisson. 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加载到的RedisProperties配置文件进行初始化,从而得到super入参需要的类。
- 触发@Import配置类加载,得到
- 通过@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进行自动续期,但这种行为本身就是网络请求操作,没办法保证
- 时钟跳跃 / 时钟回拨
- 其实我觉得这个有点太过了,运维人员修改时钟,导致锁提前到达超时释放。
- 多实例独立,不允许主从