我们已经实现了定时任务,如果用户体量上来了,就可能由多台服务器分摊压力,一个用户可能在某个时段访问A服务器,另一个时段又访问B服务器,而在实际业务中,可能同时有上百上千个服务器存在,每个服务器上都设置了定时任务,那会发生什么呢?
-
浪费资源,想象 10000 台服务器同时 “打鸣”
-
脏数据,比如重复插入
要控制定时任务在同一时间只有 1 个服务器能执行。
怎么做?
-
分离定时任务程序和主程序,只在 1 个服务器运行定时任务。成本太大
-
写死配置,每个服务器都执行定时任务,但是只有 ip 符合配置的服务器才真实执行业务逻辑,其他的直接返回。成本最低;但是我们的 IP 可能是不固定的,把 IP 写的太死了
-
动态配置,配置是可以轻松的、很方便地更新的(代码无需重启),但是只有 ip 符合配置的服务器才真实执行业务逻辑。
-
数据库
-
Redis
-
配置中心(Nacos、Apollo、Spring Cloud Config)
问题:服务器多了、IP 不可控还是很麻烦,还是要人工修改
-
-
分布式锁,只有抢到锁的服务器才能执行业务逻辑。坏处:增加成本;好处:不用手动配置,多少个服务器都一样。
锁
有限资源的情况下,控制同一时间(段)只有某些线程(用户 / 服务器)能访问到资源。
Java 实现锁:synchronized 关键字、并发包的类
问题:只对单个 JVM 有效
分布式锁
为啥需要分布式锁?
-
有限资源的情况下,控制同一时间(段)只有某些线程(用户 / 服务器)能访问到资源。
-
由于单个锁只对单个 JVM 有效,为此出现了分布式锁
分布式锁实现的关键
抢锁机制
怎么保证同一时间只有 1 个服务器能抢到锁?
核心思想 就是:先来的人先把数据改成自己的标识(服务器 ip),后来的人发现标识已存在,就抢锁失败,继续等待。
等先来的人执行方法结束,把标识清空,其他的人继续抢锁。
MySQL 数据库:select for update 行级锁(最简单)
(乐观锁)
✔ Redis 实现:内存数据库,读写速度快 。支持 setnx、lua 脚本,比较方便我们实现分布式锁。
setnx:set if not exists 如果不存在,则设置;只有设置成功才会返回 true,否则返回 false
注意事项
-
用完锁要释放(腾地方)√
因为使用Redis的setnx实现分布式锁,nx表示如果不存在则设置,如果不释放【即设置为null】的话,那第二天所有服务器再次抢锁的时候,会出现所有服务器都无法破锁的情况。
另一方面也是为了节省资源。
-
锁一定要加过期时间 √
这是针对使用锁的服务器突然挂掉的情况。
-
如果方法执行时间过长,锁提前过期了?
即如果服务器A已经抢到锁了,但是它执行任务的时间还要大于锁设置的过期时间。
问题:
-
连锁效应:释放掉别人的锁
即当A占据锁时,还没有执行完任务,A的锁就过期释放了,接着可能B就前来占据锁,这样A执行任务完后可能会将B的锁给释放掉,依次下去会产生连锁效应,使得服务器释放的不是自己的锁。
解决方法:每次释放锁之前判断是不是自己加的锁,不是就不管
-
这样还是会存在多个方法同时执行的情况
-
解决方案:续期
boolean end = false;
new Thread(() -> {
if (!end)}{
续期
})
end = true;
-
释放锁的时候,有可能先判断出是自己的锁,但这时锁过期了,最后还是释放了别人的锁
// 原子操作 if(get lock == A) {//服务器A判断出是A锁 //release lock A 此时刻A锁过期了 // set lock B 服务器B见缝插针设置B锁 del lock//导致服务器A释放锁B }
Redis + lua 脚本实现
-
Redis 如果是集群(而不是只有一个 Redis),如果分布式锁的数据不同步怎么办?
Redisson--红锁(Redlock)--使用/原理_IT利刃出鞘的博客-CSDN博客
Redisson 实现分布式锁
Redisson 是一个 java 操作 Redis 的客户端,提供了大量的分布式数据集来简化对 Redis 的操作和使用,可以让开发者像使用本地集合一样使用 Redis,完全感知不到 Redis 的存在。
2 种引入方式
-
spring boot starter 引入(不推荐,版本迭代太快,容易冲突)https://github.com/redisson/redisson/tree/master/redisson-spring-boot-starter
<!-- 引入Redission依赖-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.17.5</version>
</dependency>
示例代码
// list,数据存在本地 JVM 内存中
List<String> list = new ArrayList<>();
list.add("yupi");
System.out.println("list:" + list.get(0));
list.remove(0);
// 数据存在 redis 的内存中
RList<String> rList = redissonClient.getList("test-list");
rList.add("yupi");
System.out.println("rlist:" + rList.get(0));
rList.remove(0);
缓存预热实现分布式锁进行优化
/**
* 缓存预热任务
*/
@Component
@Slf4j
public class PreCacheJob {
@Resource
private UserService userService;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private RedissonClient redissonClient;
//设置重点用户
private List<Long> mainUserList = Arrays.asList(1L, 2L);
//每天执行,预热缓存
@Scheduled(cron = "0 00 22 * * *")//cron表达式
public void doCacheRecommend(){
RLock rLock = redissonClient.getLock("zj:precachejob:docache:lock");
//参数1:服务器等待锁的时间 这里设置等待时间为0,是因为该定时任务只要求每天执行依次,所以只要有一个服务器的线程执行了就可以了
//参数2:如果请求到锁,锁的过期时间
//参数3:单位
//只有一个线程获取锁
try {//如果请求到锁了就执行
if (rLock.tryLock(0, 30000L, TimeUnit.MILLISECONDS)){
log.info("getLock", Thread.currentThread().getId());
for(Long userId : mainUserList) {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
Page<User> userPage = new Page<>(1, 10);
userService.page(userPage, queryWrapper);
List<User> userList = userPage.getRecords();
userList = userList.stream().map((user) -> {
return userService.getSafetyUser(user);
}).collect(Collectors.toList());
userPage.setRecords(userList);
String redisKey = String.format("zj:user:recommend:%s", userId);
ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
//写缓存
try {
valueOperations.set(redisKey, userPage, 30000, TimeUnit.MILLISECONDS);
} catch (Exception e) {
log.info("redis set key error", e);
}
}
}
} catch (InterruptedException e) {
log.error("doCacheRecommendUser error", e);
} finally {
//判断当前锁是否是该线程设置的锁
if(rLock.isHeldByCurrentThread()){
log.info("unLock", Thread.currentThread().getId());
rLock.unlock();//释放锁
}
}
}
}