前言
🧓我是[提前退休的java猿],一名7年java开发经验的开发组长,分享工作中的各种问题!
加了锁和事务还是重复报名了。这个问题只看代码的话,90% 的后端程序员都定位不到问题。甚至整个职业生涯你们都碰不到这个问题!
上周五同事小李哥遇到一个活动重复报名的生产问题,始终找不到原因。最后就过来问我,我看了一下代码是没啥大问题的(没有并发量)。
既然代码没有问题,其实我已经知道问题出在哪儿了。
重复报名
示例代码
大家先看一下controller
的实现,锁的是活动的ID
,就是同一活动同一时刻只能有一个用户能够报名成功! 同一时刻其他用户就会阻塞获取锁?
java
@AutoLog(value = "活动报名")
@ApiOperation(value = "活动报名")
@PostMapping("enroll")
public Result enroll(@RequestBody EnrollParameReq req) {
RLock lock = redissonClient.getLock(RedisKeyConstant.ENROLL_LOCK_KEY+req.getActivityId());
try {
lock.lock();
// ---- Transaction标记的方法:(查询没有报过名的才能报名,插入报名记录 剩余报名人数-1)
String msg = service.enroll(req);
if(oConvertUtils.isNotEmpty(msg)){
return Result.error(msg);
}
return Result.OK();
} finally {
lock.unlock();
}
}
锁没有设置过期时间,并且service.enrolle
里面的业务逻辑,以及当前业务根本没啥并发量。所以这个代码至少来说不会出现重复报名的情况。
定位问题
看了代码之后我就猜到是什么原因了,就是我们的数据库环境迁移之后一直存在一个问题: com.kingbase8.util.KSQLException: This _connection has been closed.
此前定时任务执行一段时间就停止了,也是这个问题引起的。
✔产生重复报名的执行逻辑:
当程序在执行完成业务逻辑之后,代理类在
commit
事务的时候,程序和数据库之间就断开连接了。程序员就直接抛异常了,这时候rollback
也没用了,分布式锁也释放了!此时数据库实际上还在执行
commit
操作,同样的请求又进来获取到锁,又执行同样逻辑。发现没有报名经,又提交报名信息。所以就引发了重复报名的问题
执行流程如下图:

证明问题
既然是commit
异常了导致的重复报名,那么肯定有异常的日志。当时就让小李哥 看了日志。果然我预料的一样commit
事务的时候异常了。日志简化如下:
java
2025-07-22 18:01:03.504 [http-nio-8016-exec-4] ERROR
com.alibaba.druid.pool.DruidDataSource:2146 - recycle error com.kingbase8.util.KSQLException: This _connection has been closed. at
com.kingbase8.jdbc.KbConnection.checkIsClosed(KbConnection.java:1401) at
com.kingbase8.jdbc.KbConnection.setAutoCommit(KbConnection.java:1288) at
com.kingbase8.dispatcher.entity.DispatchConnection.setAutoCommit(DispatchConnection.java:1115)
at com.alibaba.druid.filter.FilterChainImpl.connection_setAutoCommit(FilterChainImpl.java:699)
at com.alibaba.druid.filter.logging.LogFilter.connection_setAutoCommit(LogFilter.java:467) at
com.alibaba.druid.filter.FilterChainImpl.connection_setAutoCommit(FilterChainImpl.java:694) at
..............................................................
org.jeecg.modules.activity.service.impl.OaActivityEnrollServiceImpl$$EnhancerBySpringCGLIB$$80154743.activityEnroll(<generated>) at
org.jeecg.modules.activity.controller.app.OaActivityAppController.enroll(OaActivityAppController.java:136) at
org.jeecg.modules.activity.controller.app.OaActivityAppController$$FastClassBySpringCGLIB$$5019
..........................................................
org.jeecg.modules.activity.controller.app.OaActivityAppController$$EnhancerBySpringCGLIB$$56f17
e58.enroll(<generated>) ......................................................
org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) at
org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) at
java.lang.Thread.run(Thread.java:748)
----------
2025-07-22 18:01:04.784 [http-nio-8016-exec-10] ERROR o.jeecg.common.exception.JeecgBootExceptionHandler:78 - JDBC rollback; This _connection has been closed.;.........
同时我们controller
有@AutoLog(value = "活动报名")
日志注解,这个注解是实现AOP的环绕通知,执行完成之后会向数据库插入日志,同时记录用户的Id。 代码如下:
java
@Around("logPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
long beginTime = System.currentTimeMillis();
//执行方法
Object result = point.proceed();
//执行时长(毫秒)
long time = System.currentTimeMillis() - beginTime;
//保存日志
insertLog(point, time, result);
return result;
}
这个日志也是我们排查问题的重要方式,因为没有捕捉异常,所以日志中也是没有这个人报名的操作记录的。(可以优化前前置处理吧!!)
代码优化
引发这个问题,还有两处还有点问题,优化之后也能很大概率减少重复报名问题的发生:
- 前端没有做重复点击的控制。
- 后端实际上可以对锁进行优化,锁用户就行了;没有获取到锁直接返回失败,一直阻塞的获取锁很消耗资源。
- 细化事务的粒度,缩短事务的占用时间
java
// 锁用户 (KEY:活动ID + 用户ID)----------------------
RLock lock = redissonClient.getLock("活动ID+userId");
try {
//获取用户锁,不用等待
Boolean lockRet = lock.tryLock(0, 10, TimeUnit.SECONDS);
if(lockRet){
// 执行业务逻辑:多个用户可能同时报名防止报超过了(活动人数通过 利用数据乐观锁 update t set num = num -1 where id = #{id} num > 1 )
// 更新失败则回滚,表示报名人数已满
oaActivityEnrollService.activityEnroll(req);
return Result.OK();
}
//----------- 没有获取到直接失败
return Result.error("当前活动报名人数过多,请稍后再试");
}catch (Exception e){
log.error("erorr",e);
return Result.error("当前活动报名人数过多,请稍后再试");
}finally {
if(lock.isHeldByCurrentThread()){
lock.unlock();
}
}
总结
本次通过重复报名的问题,定位到是就是数据的老毛病了。就是经常出现 java.net.SocketTimeoutException: Read timed out
的问题。这个问题导致我们程序员无法感知 事务的提交结果,从而引发的一系列问题。
当然,我们分析了代码,因为前端没有做重复提交的控制,后端也是锁的活动,并且会一直尝试获取锁。这就大大增加了重复报名的风险。虽然根本问题是数据库引起的,但是我们的代码也有很大的优化空间,优化之后,也能大大降低重复报名,以及提高并发,提升用户的体验。
关于如何解决
SocketTimeoutException
目前正在和厂家沟通解决中,解决之后会公布解决方案!