加了锁,加了事务 还是重复报名❓

前言

🧓我是[提前退休的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;
}

这个日志也是我们排查问题的重要方式,因为没有捕捉异常,所以日志中也是没有这个人报名的操作记录的。(可以优化前前置处理吧!!)

代码优化

引发这个问题,还有两处还有点问题,优化之后也能很大概率减少重复报名问题的发生:

  1. 前端没有做重复点击的控制。
  2. 后端实际上可以对锁进行优化,锁用户就行了;没有获取到锁直接返回失败,一直阻塞的获取锁很消耗资源。
  3. 细化事务的粒度,缩短事务的占用时间
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 目前正在和厂家沟通解决中,解决之后会公布解决方案!

相关推荐
装不满的克莱因瓶17 分钟前
【踩坑】IDEA提交Git .gitignore忽略文件不起作用
java·git·.gitignore·踩坑
都叫我大帅哥17 分钟前
Docker Swarm 部署方案
后端
都叫我大帅哥21 分钟前
在Swarm中部署Nacos并配置外部MySQL
后端
专注于大数据技术栈23 分钟前
java学习--Collection的迭代器
java·python·学习
想摆烂的不会研究的研究生7 小时前
每日八股——Redis(1)
数据库·经验分享·redis·后端·缓存
码熔burning8 小时前
MySQL 8.0 新特性爆笑盘点:从青铜到王者的骚操作都在这儿了!(万字详解,建议收藏)
数据库·mysql
毕设源码-郭学长8 小时前
【开题答辩全过程】以 基于SpringBoot技术的美妆销售系统为例,包含答辩的问题和答案
java·spring boot·后端
猫头虎8 小时前
2025最新OpenEuler系统安装MySQL的详细教程
linux·服务器·数据库·sql·mysql·macos·openeuler
梨落秋霜8 小时前
Python入门篇【文件处理】
android·java·python
Java 码农8 小时前
RabbitMQ集群部署方案及配置指南03
java·python·rabbitmq