分布式锁实现 | 青训营笔记

100 阅读6分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 22 天

今天学习了如何使用Redis实现本地锁,实现分布式锁,以及使用红锁的场景,从零到整解决分布式锁问题的情况,以及具体的使用代码场景

  • 常见分布式锁的实现方式

    • Redis 实现分布式锁
    • Zookeeper 实现分布式锁
    • MySQL 专门用一张表来记录信息,实现分布式锁,也是常说的基于数据库实现分布式锁
  • 分布式锁相关问题解决措施简述

    • 如何获取锁: 使用setnx或者使用set一并加上参数

    • 如何避免无人释放锁产生死锁: 给锁设置过期时间,注意下图通过两个标记将其设置时间和获取锁变成原子操作

      • set keyName keyValue NX EX 60
    • 任务执行超时,锁自动释放: 使用看门狗机制维持锁续期

      • 在Redisson中,有一个著名的看门狗机制,当我们使用 Redisson 来实现分布式锁时,加锁时,每次都会默认设置过期时间30s,然后当业务执行超过10s,也就是锁时间还剩下 20s 时,它就会自动续期。
    • 如何避免被别人释放自己的锁: 将对应锁的value设置为一个uuid或者其他值,删除时判断相同才能删除

      • if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
      • 注意这里的判断相同和删除需要使用lua脚本来维持原子性
    • 分布式锁中存在的原子性需求

      • 上锁和设置过期时间需要保证原子性;(加锁)
      • 判断锁ID是否为自己所有和解锁需要保证原子性 (解锁)
    • 对于单体Redis可以满足,但是如果Redis集群如何保证单点事故的可用性

      • 红锁
  • 红锁相关

    • 类似于Raft的实现,并且红锁本身是很重的

    • 用Redis中的多个master实例,来获取锁,只有大多数实例获取到了锁,才算是获取成功。具体的红锁算法分为以下五步:

      • 获取当前的时间(单位是毫秒)。
      • 使用相同的key和随机值在N个节点上请求锁。这里获取锁的尝试时间要远远小于锁的超时时间,防止某个masterDown了,我们还在不断的获取锁,而被阻塞过长的时间。
      • 只有在大多数节点上获取到了锁,而且总的获取时间小于锁的超时时间的情况下,认为锁获取成功了。
      • 如果锁获取成功了,锁的超时时间就是最初的锁超时时间进去获取锁的总耗时时间。
      • 如果锁获取失败了,不管是因为获取成功的节点的数目没有过半,还是因为获取锁的耗时超过了锁的释放时间,都会将已经设置了key的master上的key删除。
  • 在分布式调用过程中由于重试带来的问题

    • 幂等性问题: (查询操作具有天然幂等性)

      在分布式架构下,服务之间调用会因为网络原因出现超时失败情况,而重试机制会重复多次调用服务,但是对于被调用放,就可能收到了多次调用。如果被调用方不具有天生的幂等性,那就需要增加服务调用的判重模块,并对每次调用都添加一个唯一的id。

      大量请求超时堆积

      超高并发下,大量的请求如果都进行超时重试的话,如果你的重试时间设置不安全的话,会导致大量的请求占用服务器线程进行重试,这时候服务器线程负载就会暴增,导致服务器宕机。对于这种超高并发下的重试设计,我们不能让重试放在业务线程,而是统一由异步任务来执行

  • 具体操作代码

/**
 * @description:
 * @author: Ning Zaichun
 * @date: 2022年09月20日 20:59
 */
@Service
public class RedissonServiceImpl implements IRedissonService {
​
    @Autowired
    StringRedisTemplate stringRedisTemplate;
​
    @Autowired
    private MenuMapper menuMapper;
​
    private static final String REDISSON_MENU_LIST = "redisson:menu:list";
    private static final String REDISSON_MENU_LIST_LOCK_VALUE = "redisson:lock";
    @Autowired
    private RedissonClient redissonClient;
​
​
    @Override
    public List<MenuEntity> getList() {
        // 判断缓存是否有数据
        String menuJson = stringRedisTemplate.opsForValue().get(REDISSON_MENU_LIST);
        if (menuJson != null) {
​
​
            System.out.println("缓存中有,直接返回缓存中的数据");
            List<MenuEntity> menuEntityList = JSON.parseObject(menuJson, new TypeReference<List<MenuEntity>>() {
            });
            return menuEntityList;
        }
​
        // 从数据库中查询
        List<MenuEntity> result = getMenuJsonFromDbWithRedissonLock();
​
        return result;
    }
​
​
​
    /**
     * 问题:其实写成上面那种模样,相对来说,也能解决很多时候的问题了,从头到尾看过来的话,其实也能发现就一个锁自动过期问题没有解决了。
     * 但在实现这个之前,我还是说明一下,为什么说在没有解决锁自动过期问题时,就已经能应付大多数场景了。
     * 重点在于如何评估 锁自动过期时间,锁自动过期时间到底设置多少合适呢?
     * 其实如果对于业务理解较为透彻,对于这一部分的业务代码执行时间能有一个较清晰的估算,给定一个合适的时间,在不出现极端情况,基本都能应付过来了。
     * <p>
     * 但是呢,很多时候,还是会怕这个万一的,万一真出现了,可能造成的损失就不止 一万了,哈哈。
     * 解决方案
     * 1、在市场主流的 Redission 中,针对这样的问题,已经有了解决方案。这也是Redission中常说的看门狗机制。
     * <p>
     * 如果需要自己实现的思路:
     * 1、这方面的问题,也做了十分浅显的思考,我觉得应该还是依赖于定时任务去实现,但到底该如何实现这个定时任务,我还没法给出一个合适的解决方案。 或许我应该会尝试一下。
     *
     * @return
     */
    public List<MenuEntity> getMenuJsonFromDbWithRedissonLock() {
        System.out.println("从数据库中查询");
        //1、占分布式锁。去redis占坑
        //(锁的粒度,越细越快:具体缓存的是某个数据,11号商品) product-11-lock
        //RLock catalogJsonLock = redissonClient.getLock("catalogJson-lock");
        //创建读锁
        RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(REDISSON_MENU_LIST_LOCK_VALUE);
        RLock rLock = readWriteLock.readLock();
        List<MenuEntity> result = null;
        try {
            rLock.lock();
            String menuJson = stringRedisTemplate.opsForValue().get(REDISSON_MENU_LIST);
            if (menuJson != null) {
                System.out.println("缓存中有,直接返回缓存中的数据");
                List<MenuEntity> menuEntityList = JSON.parseObject(menuJson, new TypeReference<List<MenuEntity>>() {
                });
                return menuEntityList;
            }
            try {
                Thread.sleep(50000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            //加锁成功...执行业务
            //加锁成功...执行业务
            result = menuMapper.selectList(new QueryWrapper<MenuEntity>());
            // 构建缓存
            stringRedisTemplate.opsForValue().set(REDISSON_MENU_LIST, JSON.toJSONString(result));
        } finally {
            rLock.unlock();
        }
        return result;
    }
​
​
    @Override
    public Boolean updateMenuById(MenuEntity menu) {
        return updateMenuWithRedissonLock(menu);
    }
​
    public Boolean updateMenuWithRedissonLock(MenuEntity menu) {
        RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(REDISSON_MENU_LIST_LOCK_VALUE);
        RLock writeLock = readWriteLock.writeLock();
        Boolean update = false;
        try {
            writeLock.lock();
            //加锁成功...执行业务
            //加锁成功...执行业务
            update = menuMapper.updateById(menu) > 0;
        } finally {
            writeLock.unlock();
        }
        return update;
    }
​
}
​