SpringBoot使用RedisTemplate+Lua脚本实现Redis分布式锁

SpringBoot使用RedisTemplate+Lua脚本实现Redis分布式锁

问题:定时任务部署在多台Tomcat上,因此到达指定的定时时间时,多台机器上的定时器可能会同时启动,造成重复数据或者程序异常等问题。

1
2
3
4
5
6
7
8
9
//发送消息,不能重复发送
@Scheduled(cron = "0 0/15 * * * ? ")
public void sendMsg(String userId) {

}

项目部署在Tom 1 ,Tom 2

如何控制只有一个Tomcat在同一时刻执行任务

使用分布式锁来控制,谁抢到了锁就让谁执行。

一、基于Redis实现分布式锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
package cn.pconline.pcloud.base.util;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisCluster;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
* @Description 使用RedisTemplate+Lua脚本实现Redis分布式锁
* @Author jie.zhao
* @Date 2019/11/19 11:46
*/
@Component
public class RedisLock {
@Autowired
private StringRedisTemplate template;

private static final Long RELEASE_SUCCESS = 1L;

private static final long DEFAULT_TIMEOUT = 1000 * 10;

private static final String UNLOCK_LUA = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

/**
* 尝试获取锁 立即返回
*
* @param key
* @param value
* @param timeout
* @return
*/
public boolean lock(String key, String value, long timeout) {
return template.opsForValue().setIfAbsent(key, value, timeout, TimeUnit.MILLISECONDS);
}

/**
* 以阻塞方式的获取锁
*
* @param key
* @param value
* @param timeout
* @return
*/
public boolean lockBlock(String key, String value, long timeout) {
long start = System.currentTimeMillis();
while (true) {
//检测是否超时
if (System.currentTimeMillis() - start > timeout) {
return false;
}
//执行set命令
//1
Boolean absent = template.opsForValue().setIfAbsent(key, value, timeout, TimeUnit.MILLISECONDS);
//其实没必要判NULL,这里是为了程序的严谨而加的逻辑
if (absent == null) {
return false;
}
//是否成功获取锁
if (absent) {
return true;
}
}
}

public boolean unlock(String key, String value) {
// 使用Lua脚本:先判断是否是自己设置的锁,再执行删除
// 使用lua脚本删除redis中匹配value的key,可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁
// spring自带的执行脚本方法中,集群模式直接抛出不支持执行脚本的异常EvalSha is not supported in cluster environment.,所以只能拿到原redis的connection来执行脚本

List<String> keys = new ArrayList<>();
keys.add(key);
List<String> args = new ArrayList<>();
args.add(value);

Long result = template.execute(new RedisCallback<Long>() {
@Override
public Long doInRedis(RedisConnection connection) throws DataAccessException {
Object nativeConnection = connection.getNativeConnection();
// 集群模式和单机模式虽然执行脚本的方法一样,但是没有共同的接口,所以只能分开执行
// 集群模式
if (nativeConnection instanceof JedisCluster) {
return (Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA, keys, args);
}

// 单机模式
else if (nativeConnection instanceof Jedis) {
return (Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA, keys, args);
}
return 0L;
}
});

//返回最终结果
return RELEASE_SUCCESS.equals(result);
}
}

使用方法:

1
2
3
4
5
6
7
8
9
10
@Scheduled(cron = "0 0/15 * * * ? ")
public void sendExamTemplateMsg() {
if (redisLock.lock(RedisKey.REDIS_JOB_SEND_KEY, RedisKey.REDIS_JOB_SEND_VALUE, 1000 * 60)) {

//....
log.info("定时轮询考试安排通知结束 \t" + new Date());
} else {
log.info("定时轮询考试安排,未获取到锁其他应用正在执行 \t" + new Date());
}
}

二、分布式锁的要求

  • 互斥性。在任意时刻,只有一个客户端能持有锁。
  • 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
  • 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
  • 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

三、存在的问题

注意!!!! 该加锁方法仅针对单实例 Redis 可实现分布式加锁,或者使用场景少的业务。

原因对于 Redis 集群会有一定几率出现问题

例如:当进程1对master节点写入了锁,此时master节点宕机,slave节点提升为master而刚刚写入master的锁还未同步,此时进程2也将能够获取锁成功,此时必然会导致数据不同步问题。还有另一个问题即: key 超时之后业务并没有执行完毕但却自动释放锁了,这样就会导致并发问题。

如果需要更加健壮的Redis集群分布式锁,推荐使用Redisson

-------------已经触及底线 感谢您的阅读-------------

本文标题:SpringBoot使用RedisTemplate+Lua脚本实现Redis分布式锁

文章作者:趙小傑~~

发布时间:2019年11月19日 - 14:35:11

最后更新:2019年11月28日 - 22:04:51

原始链接:https://cnsyear.com/posts/c0a3bc8.html

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%