个人使用:17 接口防止重复点击后端处理
接口防止重复点击后端处理场景:用户对于新增保存/修改保存,往往会进行点击。但是部分接口后端逻辑处理时间稍长,那么就会出现当前数据已经在处理了,但是没有返回执行完成数据或者页面跳转时候,依然停留在原页面上,用户不由自主的进行反复点击。处理方案:A、前端约束,前端在用户点击之后设置悬浮页/遮罩层等,放置用户再次点击,但是这些对于客户在页面按了F5,约束重置取消。B、部分人 进行接口调用,也就是该接口对
接口防止重复点击后端处理
场景:用户对于新增保存/修改保存,往往会进行点击。但是部分接口后端逻辑处理时间稍长,那么就会出现当前数据已经在处理了,但是没有返回执行完成数据或者页面跳转时候,依然停留在原页面上,用户不由自主的进行反复点击。
处理方案:
A、前端约束,前端在用户点击之后设置悬浮页/遮罩层等,放置用户再次点击,但是这些对于客户在页面按了F5,约束重置取消。
B、部分人 进行接口调用,也就是该接口对外提供,供第三方调用,这种无前端,你没法凭空变出一个前端控制第三方调用次数以及时间。所以接口防止重复点击的本质应该是后端控制。
这里的接口防止重复点击后端处理跟接口限流是二码事。
接口限流是某一接口被调用多次,次数频率过高,系统消耗严重,所以约束时间段内的调用次数。这里用到的也是redis限流。
接口防止重复点击是针对一些接口调用之后会变更数据。查询这种幂等性返回数据的,一般不会去设置接口防止重复点击,因为单次跟多次点击,数据返回没有变动。新增会导致数据变更;修改如果是全量修改的话,多次点击变更看具体业务;修改属于加某些数量或者减些数量,按照你点击操作次数,进行数量*次数的数值变化。 这就是为什么需要部分接口进行放置重复点击操作,原因就是为了保持数据的正确性以及幂等接口的稳定性。重复提交会导致非幂等性。
通常的解决方案。常见的是加锁。java单体应用可以用语言本身的synchronized锁机制。分布式系统,一般是借助redis或zk等分布式锁。
代码实操。
1、编写对应 redis的防重复点击锁。
配置对应导入jar包
对应pom文件导入
<properties>
<redisson.version>3.9.1</redisson.version>
</properties>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>${redisson.version}</version>
</dependency>
通用方法类建立。
通用方法建立。 暂时会去掉作者信息。尽量方法公共。
/**
* 提供运行防重点击锁接口
* @desc
**/
public interface LockCallbackWithoutResult {
void doWithLock();
}
/**
*
* @desc 防止并发控制
**/
public interface LockTemplateService {
/**
* 加锁控制
* @param bizNo 业务单号
* @param service 业务层相关接口
* @return
*/
void execute(String bizNo, LockCallbackWithoutResult service);
}
// 包名省略,大致就是那里红了,自动导包就好
/**
* @desc 防止并发控制实现
**/
@Slf4j
@Service
@Primary
public class LockTemplateServiceImpl implements LockTemplateService {
@Autowired
private RedissonClient redissonClient;
private final static String pre_lock = "claim_lock:";
/** 对应时间为5,默认单位是TimeUnit.SECONDS 秒; 该方法作为基础的接口防重点击接口,默认是5S内进行防止重复调用
后续 可以提供额外接口,调用方自定义 锁定时间,也就是 time值作为 传参写入到方法体内。目前默认5S内满足现有逻辑运行。
*/
private final static Integer time = 5;
@Override
public void execute(String bizNo, LockCallbackWithoutResult service) throws InterruptedException {
if (StringUtils.isEmpty(bizNo) || service == null) {
throw new IllegalArgumentException("bizNo or service is empty");
}
RLock lock = redissonClient.getLock(pre_lock + bizNo);
try {
if (lock.tryLock(time, TimeUnit.SECONDS)) {
//执行相关业务层代码
service.doWithLock();
} else {
LoggerUtils.error(log, "concurrent biz work error,bizNo={0}", pre_lock + bizNo);
throw new BizException(BizErrorCodeEnum.OPERATE_FREQUENTLY);
}
} finally {
if(lock.isHeldByCurrentThread()){
lock.unlock();
}
}
}
}
实际调用
// 原先方法调用。
/**
* @param no
*/
public RestResponse claimSubmit(@PathVariable("no") String no,
@RequestBody Info info) {
Assert.hasText(no, "XXX不能为空");
CaseInfoChecker.checkClaimInfo(info, false);
caseService.saveClaimInfo(info);
return RestResponse.succ();
}
// 现在方法加防重锁。
/**
* 悲观锁组件
*/
@Autowired
private LockTemplateService lockTemplateService;
/**
* @param no
*/
public RestResponse claimSubmit(@PathVariable("no") String no,
@RequestBody Info info) {
Assert.hasText(no, "XXX不能为空");
CaseInfoChecker.checkClaimInfo(info, false);
lockTemplateService.execute(reportNo, () -> caseService.saveClaimInfo(info));
return RestResponse.succ();
}
这是目前 某一项目中关于接口防重复点击的示例应用。
这里需要注意的是,怎么来判断是同一次调用的多次点击。
目前使用的还是比较简单的 通过某一个关键的 报案号字段 作为约束本次代码执行的关键值。
其实 目前数据库比较缺乏的是 版本号 version。 如果对应主键 + 版本号,同一版本号的修改 默认只有一次,下次再修改时候,版本已经发生改变。从这个逻辑来看,如果数据库存在版本号数据,前端在修改时候版本号也作为影响接口防重复点击方法对应redis key值拼接因素之一。
然后是本次接口点击修改时间戳,类似版本号作用。本次参数以时间戳 + 对应关键字段 作为约束。
后续也存在,将表单或者数据用md5加密,然后该md5 作为约束条件之一。
然后就是令牌方法,用户进行接口调用,都是会按某一规则申请一个令牌,然后携带令牌进行接口调用,多次调用申请令牌,回去判断对应参数是否被锁住了,如果被锁住了就不返回令牌,不知道哪里看的,可行性不高,主要是技术难度可能较大,不易实现。
对应部分存在问题进行优化更改。 主要是 LockTemplateServiceImpl
/**
* @desc 防止并发控制实现
**/
@Slf4j
@Service
@Primary
public class LockTemplateServiceImpl implements LockTemplateService {
@Autowired
private RedissonClient redissonClient;
private final static String pre_lock = "claim_lock:";
/** 对应时间为5,默认单位是TimeUnit.SECONDS 秒; 该方法作为基础的接口防重点击接口,默认是5S内进行防止重复调用
后续 可以提供额外接口,调用方自定义 锁定时间,也就是 time值作为 传参写入到方法体内。目前默认5S内满足现有逻辑运行。
*/
private final static Integer time = 5;
@Override
public void execute(String bizNo, LockCallbackWithoutResult service) throws InterruptedException {
if (StringUtils.isEmpty(bizNo) || service == null) {
throw new IllegalArgumentException("bizNo or service is empty");
}
RLock lock = redissonClient.getLock(pre_lock + bizNo);
try {
// 1. 最常见的使用方法
//lock.lock();
// 2. 支持过期解锁功能,10秒钟以后自动解锁, 无需调用unlock方法手动解锁
//lock.lock(10, TimeUnit.SECONDS);
// 3. 尝试加锁,最多等待3秒,上锁以后10秒自动解锁 (time 秒后自动解锁)
if (lock.tryLock(3, time, TimeUnit.SECONDS)) {
//执行相关业务层代码
service.doWithLock();
} else {
LoggerUtils.error(log, "concurrent biz work error,bizNo={0}", pre_lock + bizNo);
throw new BizException(BizErrorCodeEnum.OPERATE_FREQUENTLY);
}
} finally {
//此处代码 需要进行讨论。 lock.isHeldByCurrentThread() 跟 lock.isLocked() 二选一。
// lock.isHeldByCurrentThread()的作用是查询当前线程是否保持此锁定,和lock.hasQueueThread()不同的地方是:lock.hasQueueThread(Thread thread)的作用是判断当前线程是否处于等待lock的状态。
// lock.isLocked()的作用是查询此锁定是否由任意线程保持。
if(lock.isHeldByCurrentThread()){
lock.unlock();
}
//可能代码如下
if (lock.isLocked()) {
lock.unlock();
}
}
}
}
这里可以考虑公平锁概念
公平锁(Fair Lock)
Redisson分布式可重入公平锁也是实现了java.util.concurrent.locks.Lock接口的一种RLock对象。在提供了自动过期解锁功能的同时,保证了当多个Redisson客户端线程同时请求加锁时,优先分配给先发出请求的线程。
public void testFairLock(RedissonClient redisson){
RLock fairLock = redisson.getFairLock("anyLock");
try{
// 最常见的使用方法
fairLock.lock();
// 支持过期解锁功能, 10秒钟以后自动解锁,无需调用unlock方法手动解锁
fairLock.lock(10, TimeUnit.SECONDS);
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = fairLock.tryLock(100, 10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
fairLock.unlock();
}
参考网址
https://blog.csdn.net/weixin_42129270/article/details/114086185 分布式锁的isFair()、isHeldByCurrentThread()和isLocked()的用法和区别
https://blog.csdn.net/l1028386804/article/details/73523810 Java之——redis并发读写锁,使用Redisson实现分布式锁
漫漫长路,一个小周跟他一个小陈朋友一起努力奔跑。
更多推荐
所有评论(0)