Skip to content

Commit 74e2186

Browse files
committed
feat: 添加单机版的 redis 锁工具
1 parent 1cd3686 commit 74e2186

1 file changed

Lines changed: 255 additions & 0 deletions

File tree

  • uni-boot-common/src/main/java/top/cadecode/uniboot/common/util
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
package top.cadecode.uniboot.common.util;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Data;
5+
import lombok.RequiredArgsConstructor;
6+
import org.springframework.data.redis.core.StringRedisTemplate;
7+
import org.springframework.stereotype.Component;
8+
9+
import java.util.Map;
10+
import java.util.Objects;
11+
import java.util.concurrent.ConcurrentHashMap;
12+
import java.util.concurrent.ScheduledFuture;
13+
import java.util.concurrent.ScheduledThreadPoolExecutor;
14+
import java.util.concurrent.TimeUnit;
15+
16+
/**
17+
* @author Cade Li
18+
* @date 2022/2/15
19+
* @description Redis 版分布式锁
20+
*/
21+
@Component
22+
@RequiredArgsConstructor
23+
public class RedisLock {
24+
25+
private final StringRedisTemplate redisTemplate;
26+
27+
private final Map<String, LockContent> contentMap = new ConcurrentHashMap<>();
28+
29+
// 定时续期任务线程池,合理设置大小
30+
private final ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(10);
31+
32+
/**
33+
* 阻塞式的获取锁
34+
*
35+
* @param name 锁名称
36+
*/
37+
public void lock(String name) {
38+
lock(name, "");
39+
}
40+
41+
public void lock(String name, String value) {
42+
if (checkReentrant(name)) {
43+
storeLock(name, null, true);
44+
return;
45+
}
46+
while (true) {
47+
if (tryLock0(name, value)) {
48+
return;
49+
}
50+
sleep();
51+
}
52+
}
53+
54+
/**
55+
* 尝试一次获取锁
56+
*
57+
* @param name 锁名称
58+
* @return 是否获取到
59+
*/
60+
public boolean tryLock(String name) {
61+
return tryLock(name, "");
62+
}
63+
64+
public boolean tryLock(String name, String value) {
65+
if (checkReentrant(name)) {
66+
storeLock(name, null, true);
67+
return true;
68+
}
69+
return tryLock0(name, value);
70+
}
71+
72+
/**
73+
* 尝试在一段时间内阻塞获取锁
74+
*
75+
* @param name 锁名称
76+
* @param timeout 超时时间
77+
* @param timeUnit 时间单位
78+
* @return 是否获取到
79+
*/
80+
public boolean tryLock(String name, long timeout, TimeUnit timeUnit) {
81+
return tryLock(name, "", timeout, timeUnit);
82+
}
83+
84+
public boolean tryLock(String name, String value, long timeout, TimeUnit timeUnit) {
85+
if (checkReentrant(name)) {
86+
storeLock(name, null, true);
87+
return true;
88+
}
89+
long totalTime = timeUnit.toMillis(timeout);
90+
long current = System.currentTimeMillis();
91+
while (System.currentTimeMillis() - current <= totalTime) {
92+
if (tryLock0(name, value)) {
93+
return true;
94+
}
95+
sleep();
96+
}
97+
return false;
98+
}
99+
100+
/**
101+
* 释放锁
102+
*
103+
* @param name 锁名称
104+
*/
105+
public void unlock(String name) {
106+
if (!checkReentrant(name)) {
107+
return;
108+
}
109+
LockContent lockContent = contentMap.get(name);
110+
Integer count = lockContent.getCount();
111+
if (count > 0) {
112+
// 重入次数减一
113+
lockContent.setCount(--count);
114+
}
115+
// 释放锁
116+
if (count == 0) {
117+
// 清除重入记录
118+
contentMap.remove(name);
119+
// 停止续期任务
120+
lockContent.getFuture().cancel(true);
121+
// 删除 Redis key
122+
redisTemplate.delete(name);
123+
}
124+
}
125+
126+
/**
127+
* 清除锁,不检查是不是本线程持有锁,强行删除缓存,应该在确认锁在当前节点持有的时候使用
128+
* 两种情况,假设当前持有锁的线程为 A 节点线程 A1,其他线程有 A 节点线程 A2,B 节点线程 B1:
129+
* 1. A 节点的线程清除了 A1 的锁,续期任务正常关闭
130+
* 2. B1 清除了其他节点持有的锁
131+
* 2.1 没有继续抢锁,A1 的续期任务会抛出异常后停止,并清理 contentMap
132+
* 2.2 B1 抢到锁,如果间隔时间没有达到续期任务周期,A1 的续期任务可能会无限执行下去
133+
* 2.3 A2 抢到锁,storeLock 时会关闭原续期任务
134+
*
135+
* @param name 锁名称
136+
*/
137+
public void clear(String name) {
138+
LockContent lockContent = contentMap.get(name);
139+
if (Objects.nonNull(lockContent)) {
140+
// 清除重入记录
141+
contentMap.remove(name);
142+
// 停止续期任务
143+
lockContent.getFuture().cancel(true);
144+
}
145+
// 删除 Redis key
146+
redisTemplate.delete(name);
147+
}
148+
149+
/**
150+
* 检查重入
151+
*
152+
* @param name 锁名称
153+
* @return 是否重入
154+
*/
155+
private boolean checkReentrant(String name) {
156+
if (Objects.isNull(name)) {
157+
throw new RuntimeException("lock name cannot be null");
158+
}
159+
// 判断是否重入
160+
return Objects.nonNull(contentMap.get(name))
161+
&& contentMap.get(name).getCurrThread() == Thread.currentThread();
162+
}
163+
164+
/**
165+
* 保存重入次数到 contentMap
166+
*
167+
* @param name 锁名称
168+
*/
169+
private void storeLock(String name, ScheduledFuture<?> future, boolean reentrant) {
170+
LockContent lockContent = contentMap.get(name);
171+
if (reentrant) {
172+
// 重入次数加一
173+
lockContent.setCount(lockContent.getCount() + 1);
174+
return;
175+
}
176+
// 防止有旧锁数据残留
177+
if (Objects.nonNull(lockContent)) {
178+
// 停止续期任务
179+
lockContent.getFuture().cancel(true);
180+
}
181+
// 创建新的 LockContent
182+
lockContent = new LockContent(future, 1, Thread.currentThread());
183+
contentMap.put(name, lockContent);
184+
}
185+
186+
/**
187+
* 尝试设置 redis key
188+
*
189+
* @param name 锁名称
190+
* @return 是否设置成功
191+
*/
192+
private boolean tryLock0(String name, String value) {
193+
Boolean success = redisTemplate.opsForValue().setIfAbsent(name, value, 30, TimeUnit.SECONDS);
194+
if (Objects.equals(success, false)) {
195+
return false;
196+
}
197+
// 设置成功 开启续期任务
198+
ScheduledFuture<?> future = renewLock(name, value);
199+
storeLock(name, future, false);
200+
return true;
201+
}
202+
203+
/**
204+
* 开启锁续期任务
205+
*
206+
* @param name 锁名称
207+
* @return ScheduledFuture
208+
*/
209+
private ScheduledFuture<?> renewLock(String name, String value) {
210+
// 有效期设置为 30s,每 15 秒重置
211+
return executor.scheduleAtFixedRate(() -> {
212+
Boolean success = redisTemplate.opsForValue().setIfPresent(name, value, 30, TimeUnit.SECONDS);
213+
if (Objects.equals(success, true)) {
214+
return;
215+
}
216+
// 删除锁
217+
contentMap.remove(name);
218+
// 抛出异常停止任务
219+
throw new RuntimeException("renew lock fail, key is " + name);
220+
}, 15, 15, TimeUnit.SECONDS);
221+
}
222+
223+
/**
224+
* 休眠一定时间
225+
*/
226+
private void sleep() {
227+
try {
228+
TimeUnit.MILLISECONDS.sleep(300);
229+
} catch (InterruptedException e) {
230+
// 不响应中断
231+
}
232+
}
233+
234+
/**
235+
* 锁内容
236+
* 维护续期任务和重入次数
237+
*/
238+
@Data
239+
@AllArgsConstructor
240+
private static class LockContent {
241+
/**
242+
* 续期任务
243+
*/
244+
private ScheduledFuture<?> future;
245+
/**
246+
* 重入次数
247+
*/
248+
private Integer count;
249+
250+
/**
251+
* 当前线程
252+
*/
253+
private Thread currThread;
254+
}
255+
}

0 commit comments

Comments
 (0)