1.前端方案:
通过js将按钮绑定一个方法,点击后3s内将按钮设置成不可用,或者隐藏。
缺点:绕过前端,例如通过postman发请求。
2.hashmap版:
请求携带一个参数,将请求携带的参数(可以是用户id)存到内存的hashmap(用syc修饰)里,value存放请求时间。
当下次发送请求时,先判断map里有没有参数参数作为key。当请求时先判断map里面有没有这个key,有并且时间超过3分钟就覆盖key,没有超过三分钟就返回请求频繁。没有请求成功。
缺点:随着时间推移map越来越大,oom。
3.commons-collections 轮子
Apache 为我们提供了一个 commons-collections 的框架,里面有一个非常好用的数据结构 LRUMap 可以保存指定数量的固定的数据,并且它会按照 LRU 算法,帮你清除最不常用的数据。
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
private LRUMap<String, Integer> reqCache = new LRUMap<>(100);
@RequestMapping("/add")
public String addUser1(String id) {
synchronized (this.getClass()) {
if (reqCache.containsKey(id)) {
System.out.println("请勿重复提交!!!" + id);
return "执行失败";
}
reqCache.put(id, 1);
}
System.out.println("添加用户ID:" + id);
return "执行成功!";
}
思考:当采用分布式部署,多台服务器上部署项目,用户请求时由于负载均衡、不知道请求到那台服务器上了,上面的方法就不好用了。
4.分布式锁(用在多台服务器时,上面的方法用在单台服务器)
分布式锁就是对于一个对象只能有一个线程修改(对对象修改的方法只能有一个线程调用)。
基于数据库实现、redis实现、zookeeper实现
(1)数据库实现
存方法名、线程信息(设计可重入性)、失效时间(设置定时任务清除,防止死锁)
(2)redis实现
- 将方法名和参数拼接作为key,uuid作为value,并设置有效时间(防止死锁)。利用setnx(如果不存在就存进入,如果存在就什么也不做)
- 先判断redis中有没有key,如果有就是重复请求,不做处理。
- 如果redis中没有key,就不是重复请求,执行业务。执行业务后,使用lua脚本释放锁。使用lua脚本,因为lua脚本在redis中以原子形式操作,要么全做,要么全不做。避免检查锁和删除锁引发的并发错误。
(3)ZooKeeper实现分布式锁
这里推荐一个Apache的开源库Curator,它是一个ZooKeeper客户端,Curator提供的InterProcessMutex是分布式锁的实现,acquire方法用于获取锁,release方法用于释放锁。
优点:具备高可用、可重入、阻塞锁特性,可解决失效死锁问题。
缺点:因为需要频繁的创建和删除节点,性能上不如Redis方式。