「黑马点评」五、分布式锁优化

28 天前
8

「黑马点评」五、分布式锁优化

基于 SETNX 的分布式锁问题

  • 不可重入:获得锁的线程可以再次进入到相同锁的代码块中,意义在于防止死锁,如在锁内调用另一个带锁的,产生死锁
  • 不可重试:没有重试机制,才获取一次就返回 false
  • 超时释放:锁然避免了死锁,但是如果是业务超时,锁自动释放,可能的额外风险
  • 主从一致性:如果使用 Redis 的主从集群,因为异步同步,可能会出现主节点宕机,数据未同步

Redisson 入门

Redisson 是一个在 Redis 的基础上实现的 Java 驻内存数据网格(In-Memory Data Grid) > 提供了一系列的分布式的 Java 常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现

引入依赖

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.23.1</version>
</dependency>

配置类

@Configuration
public class RedissonConfig {
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379").setPassword("123321");
        return Redisson.create(config);
    }
}

获取锁:lock.tryLock(waitTime, leasetime, timeUnit),空参为默认 30s 释放,仅获取一次 释放锁:lock.unlock()

业务代码使用

Long userId = UserHolder.getUser().getId();
RLock lock = redissonClient.getLock("lock:order:" + userId);
boolean isLock = lock.tryLock();
if (!isLock) {
    return Result.error("不允许重复下单");
}
try {
    IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
    return proxy.createVoucherOrder(voucherId);
} finally {
    lock.unlock();
}

Redisson 可重入锁

用信号量一样的形式对锁计数,核心就是利用哈希结构存储锁的线程以及次数

image.png|500

image.png|500

要保证原子性则必用 Lua 脚本

获取锁的 Lua 脚本

image.png|500

image.png|500

释放锁的 Lua 脚本

image.png|500

image.png|500

超时释放时间没传则为 -1,但在调用时会从 watchDog 处获取默认值为 30s

pttlpexpire 毫秒级别

Redisson 分布式锁流程

image.png|500

image.png|500

这里的看门狗是用来给为锁续有效期的,一旦宕机无法续约便自行释放

  • 可重入:利用 hash 结构记录线程 id 和重入次数
  • 可重试:利用信号量和 Publish & Subscribe 功能实现等待、唤醒,获取锁失败的重试机制
  • 超时续约:利用 watchDog,每隔 leaseTime/3 重置过期时间

Redisson 分布式锁主从一致性问题

问题:获取锁刚成功,主节点就宕机了,此时被选为主节点的从节点并未同步完数据,锁失效

image.png|500

image.png|500

解决:使用多个主节点,只有当所有主节点都获取锁成功,那才是真正的成功

此时如果担心出现宕机,可以为每个从节点添加字节点,这时,从节点被选为主节点,哪怕有一个线程趁虚而入,因为无法获取全部节点的锁,所以也无法加锁成功(前提多个主节点没有全挂)

image.png|500

image.png|500

使用

RLock lock1 = redissonClient.getLock("lock:order:" + userId);
RLock lock2 = redissonClient.getLock("lock:order:" + userId);
RLock lock3 = redissonClient.getLock("lock:order:" + userId);
RLock lock = redissonClient.getMultiLock(lock1, lock2, lock3);

TODO:就这样吧,以后再研究源码分析

分布式锁总结

  1. 不可重入 Redis 分布式锁
    • 原理:利用 SETNX 的互斥性;利用 EX 避免死锁;利用线程标示检测并释放锁
    • 缺陷:不可重入,无法重试、锁超时失效
    • SETNX 直接放 key,如果存在返回 null 此时获取锁失败;如果不存在则放入成功;key 存在代表锁存在;删除 key 代表释放锁;无法重入,并且有超时误删的情况
  2. 可重入 Redis 分布式锁
    • 原理:利用 HASH 结构记录线程标示以及重入次数;利用 watchDog 自动续时;利用信号量控制锁重试等待
    • 缺陷:Redis 宕机导致锁失效
    • Redisson 的 getLock,基本方法 tryLock 和 unlock;可以设置获取锁的等待时间、锁的持有时间、时间单位;利用 HASH 结构所以可以重入,重入一次就给 value 加 1;看门狗机制是为超时误删准备的,不设置锁的持有时间,默认为 -1,在内部实现中是 30s,每过 30s/3=10s 就会再次修正过期时间为 30s;当其他线程竞争锁会被阻塞在等待时间内,并且会订阅消息,一旦释放锁就发送释放消息,阻塞的线程就会来竞争锁;如果在主从场景下,刚获取锁成功,主节点还没同步完消息就宕机了,那锁就失效了
  3. Redisson 的 multiLock
    • 原理:利用多个独立的 Redis 节点,当全部主节点获取到重入锁,才算获取锁成功
    • 缺陷:运维成本高、实现复杂
    • 多个 redisson 的 lock 联合起来,getMultiLock(lock1, lock2, lock3),每个锁代表了不同的 Redis 服务器;要获取锁成功就要每个节点都上锁成功;某个节点宕机就无法上锁,可以再加一层从节点;redLock 与 multiLock 差不多,超过一半节点返回才是获取锁成功

使用社交账号登录

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...