一种后台管理系统接口防重复点击实现方式

我们知道,分布式系统中实现接口的幂等性操作需要用到分布式锁,比如使用 Redis 的 setnx 命令实现。但是在一些内部管理系统中可以使用更加简便的方式,下面就分享其中一种。

使用前提

后台管理系统一般也是多台服务器部署的,下面这种方式使用的前提是,同一用户的请求会发送到同一服务器的应用上,可以使用 nginx 的 ip hash 或者根据 cookie 分流方式。

使用方式

添加帮助类:

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
import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;

/**
* 接口防重复点击帮助类
*/
public class IdempotentHelper {

private static Logger logger = LoggerFactory.getLogger(IdempotentHelper.class);

/** 缓存 */
private static Cache<String, Object> cache = CacheBuilder.newBuilder()
.maximumSize(1000) // 最大 Key 数
.expireAfterWrite(3, TimeUnit.SECONDS) // 3 秒超期,一般接口 3 秒足够执行完成了。
.build();

/**
* 给 Key 加锁
* @param key
* @return
*/
public static synchronized boolean lock(String key) {
Object obj = cache.getIfPresent(key);
if (obj != null) {
logger.error("Try lock key: {} fail.", key);
return false; // 加锁失败
}
cache.put(key, new Object());
logger.info("Lock key: {} success.", key);
return true;
}

/**
* 加锁失败抛出异常
* @param key
* @return
*/
public static void lockThrowException(String key) {
if (!lock(key)) {
throw new MyException("系统正在处理中,请勿重复操作!");
}
}

/**
* 释放锁
* @param key
* @return
*/
public static void unlock(String key) {
logger.info("Unlock key: {}", key);
cache.invalidate(key);
}

}

在接口中使用:

1
2
3
4
5
6
7
8
9
10
11
12
@RequestMapping(value = "/order/submit.do", method = RequestMethod.POST)
@ResponseBody
public String submit(HttpServletRequest request, HttpServletResponse response, String orderId) {
String key = "SUBMIT_ORDER_" + orderId;
IdempotentHelper.lockThrowException(key); // 尝试加锁,加锁失败直接抛异常
try {
// TODO 具体的业务处理逻辑
return "SUCCESS";
} finally {
IdempotentHelper.unlock(key); // finally 释放锁
}
}

工具优势

使用 Guava 的本地缓存,不用调用远程服务。