这是我参与「第五届青训营 」伴学笔记创作活动的第 22 天
今天学习了如何使用Redis实现本地锁,实现分布式锁,以及使用红锁的场景,从零到整解决分布式锁问题的情况,以及具体的使用代码场景
-
常见分布式锁的实现方式
- Redis 实现分布式锁
- Zookeeper 实现分布式锁
- MySQL 专门用一张表来记录信息,实现分布式锁,也是常说的基于数据库实现分布式锁
-
分布式锁相关问题解决措施简述
-
如何获取锁: 使用setnx或者使用set一并加上参数
-
如何避免无人释放锁产生死锁: 给锁设置过期时间,注意下图通过两个标记将其设置时间和获取锁变成原子操作
- set keyName keyValue NX EX 60
-
任务执行超时,锁自动释放: 使用看门狗机制维持锁续期
- 在Redisson中,有一个著名的看门狗机制,当我们使用
Redisson来实现分布式锁时,加锁时,每次都会默认设置过期时间30s,然后当业务执行超过10s,也就是锁时间还剩下 20s 时,它就会自动续期。
- 在Redisson中,有一个著名的看门狗机制,当我们使用
-
如何避免被别人释放自己的锁: 将对应锁的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;
}
}