定时任务实现-Redisson实现分布式锁

主要内容(后端):

  1. 分布式定时任务执行控制
  2. 锁的概念
  3. 分布式锁概念和常见问题
  4. 分布式锁实践

定时任务实现

  1. Spring Scheduler (spring boot 默认整合了,推荐使用这种方式)
  2. Quartz (独立于 Spring 存在的定时任务框架)
  3. XXL-Job 之类的分布式任务调度平台(界面 + SDK)

采用第一种方式:

  1. 主类开启 @EnableScheduling
  2. 给要定时执行的方法添加 @Scheduling , 指定 cron 表达式或者执行频率

cron 表达式 用现成的工具即可:
https://cron.qqe2.com/
https://www.matools.com/crontab/

控制定时任务的执行

要控制定时任务在同一时间只有 1 台服务器能执行

原因:

  1. 浪费资源
  2. 脏数据,比如重复插入

如何做?

方案种类:

  1. 分离定时任务程序和主程序,只在 1 个服务器运行定时任务,成本太大
  2. 写死配置,每个服务器都执行定时任务,但是只有 ip 符合配置的服务器才真正执行业务逻辑,其他的直接返回。成本最低;但是我们的 ip 可能不是固定的
  3. 动态配置,配置是可以轻松的、很方便地更新(代码无需重启),但是只有 ip 符合配置的服务器才真正执行业务逻辑。
    • 数据库
    • Redis
    • 配置中心(Nacos、 Apollo、 Spring Cloud Config)

问题:服务器多了, ip 不可控还是很麻烦,还需要人工修改

  1. 分布式锁,只有抢到锁的服务器才能执行业务逻辑。
    缺点:增加成本;
    优点:不用手动配置,多少服务器都一样

    注意:只要是单机,就会存在单点故障

有限资源的情况下,控制同一时间(段)只有某些线程(用户 / 服务器)能访问资源

Java 实现锁:sychronized 关键字、并发包的类

存在的问题:只对单个 JVM 有效

分布式锁

使用分布式锁的原因:

  1. 有限资源的情况下,控制同一时间(段)只有某些线程(用户 / 服务器)能访问资源
  2. Java 实现锁:sychronized 关键字、并发包的类

分布式锁的关键

抢锁机制

怎么保证同一时间只有 1 台服务器能抢到锁?

核心思想:先来的人先把数据改成自己的标识(服务器 ip),后来的人发现标识已存在,就抢锁失败,继续等待

等待先来的人执行方法结束,把标识清空,其他人继续抢锁

MYSQL 数据库:select for update 行级锁(最简单),或者用乐观锁

Redis 实现:内存数据库,读写速度快。支持 setnx、 lua 脚本,比较方便我们实现分布式锁

setnx: set if not exists 如果不存在,则设置;只有设置成功才返回 true ,否则返回 false

注意事项

1)用锁要释放

2)锁一定要加过期时间

3)如果方法执行时间过长,锁会提前过期

锁提前过期会导致的问题:

  1. 连锁反应:释放掉别人的锁
  2. 仍然存在多个方法同时执行的情况

解决方案:续期

比如:

1
2
3
4
5
6
7
boolean end = false;
new Thread(() -> {
if(!end){
//续期
}
})
end = true

4)释放锁的时候,有可能先判断出自己的锁,但这时锁过期了,最后也可能释放别人的锁

解决方案:Redis + lua 脚本保证操作原子性

1
2
3
4
5
//原子操作
if(get lock == A){
// set lock B
del lock
}

5)Redis 如果是集群(不是只有一个 Redis),如果分布式锁的数据不同步怎么办?

解决方案:https://blog.csdn.net/feiying0canglang/article/details/113258494

Redisson 实现分布式锁

Redisson 是一个 Java 操作 Redis 的客户端,提供了大量的分布式数据集来简化对 Redis 的操作和使用,可以让开发者像使用本地集合一样使用 Redis(继承了和 Java 相同的集合类)

关键词:Java Redis 客户端,分布式数据结构,实现了很多 Java 里支持的集合

两种引用方式

  1. spring boot starter 引入(不推荐,因为版本迭代太快,容易冲突):
    https://github.com/redisson/redisson/tree/master/redisson-spring-boot-starter
  2. 直接引入:https://github.com/redisson/redisson#quick-start

使用Redisson

示例代码,创建分布式列表、Map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//    list 数据存在本地 JVM   内存中
List<String> list = new ArrayList<>();
list.add("yupi");
System.out.println("list:" + list.get(0));

// 数据存在 Redis 内存中
RList<Object> rList = redissonClient.getList("test-list");
rList.add("yupi");
System.out.println("RList:" + rList.get(0));
//rList.remove(0);

// map
RMap<Object, Object> map = redissonClient.getMap("test-map");
map.put("yupi", 10);
map.get("yupi");

分布式锁保证定时任务不重复执行

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void testWatchDog(){
RLock lock = redissonClient.getLock("yupao:precachejob:docache:lock");
try {
//只有一个线程获取到锁
if(lock.tryLock(0, -1, TimeUnit.MILLISECONDS)){
// TODO 实际要执行的方法
dosomething();
System.out.println("getLock:" + Thread.currentThread().getId());
}
} catch (InterruptedException e) {
System.out.println(e.getMessage());
} finally {
// 只能释放自己的锁
if(lock.isHeldByCurrentThread()){
System.out.println("unLock:" + Thread.currentThread().getId());
lock.unlock();
}
}

}

注意:

  1. waitTime 设置为 0,只抢一次,抢不到就放弃
  2. 主要释放锁要写在 finally 语句块中,保证最后一定释放锁

Redisson 看门狗机制

Redisson 中提供的续期机制

开一个监听线程,如果方法还没执行完,就帮你重置 Redis 锁的过期时间

原理:

  1. 监听当前线程,默认过期时间是 30 秒,每 10 秒续期一次(续期到 30 秒)
  2. 如果线程挂掉(注意 debug 模式也会被它当成服务器宕机),则不会续期

详情参考文档:https://blog.csdn.net/qq_26222859/article/details/79645203