BUG 是最好的学习素材。

最近的BUG都不疼不痒,基本秒修复。昨天遇到一个大坑,修复了好几个小时。这是一个事务挂起导致数据库连接未释放,然后导致获取数据库连接失败的BUG

场景

运行测试用例集(包含多个测试用例),处理逻辑如下:1、首先去并发处理用例参数,例如关联用户的登录状态(这个比较麻烦,请参考旧文内容:我的开发日记(十五)中的分布式锁的实现);2、把用例组装成多线程任务,丢到线程池去执行;3、异步等待所有用例执行完成,处理数据,异步写入数据库。

BUG 代码

/**
 * 获取用户登录凭据,map缓存
 *
 * @param id
 * @param map
 * @return
 */
@Override
@Transactional(isolation = Isolation.REPEATABLE_READ, propagation = Propagation.REQUIRES_NEW)
public String getCertificate(int id, ConcurrentHashMap<Integer, String> map) {
    if (map.contains(id)) return map.get(id);
    Object o = UserLock.get(id);
    synchronized (o) {
        if (map.contains(id)) return map.get(id);
        logger.warn("非缓存读取用户数据{}", id);
        TestUserCheckBean user = testUserMapper.findUser(id);
        if (user == null) UserStatusException.fail("用户不存在,ID:" + id);
        String create_time = user.getCreate_time();
        long create = Time.getTimestamp(create_time);
        long now = Time.getTimeStamp();
        if (now - create < OkayConstant.CERTIFICATE_TIMEOUT && user.getStatus() == UserState.OK.getCode()) {
            map.put(id, user.getCertificate());
            return user.getCertificate();
        }
        boolean b = UserUtil.checkUserLoginStatus(user);
        if (!b) {
            updateUserStatus(user);
            if (user.getStatus() != UserState.OK.getCode()) UserStatusException.fail("用户不可用,ID:" + id);
        } else {
            testUserMapper.updateUserStatus(user);
        }
        map.put(id, user.getCertificate());
        return user.getCertificate();
    }
}

BUG 分析

这里犯了两个错误:

判断 key 方法错误

应该使用map.containsKey(id)来判断,而不是map.contains(id),可以看一下map.contains(id)的源码:


/**
 * Legacy method testing if some key maps into the specified value
 * in this table.  This method is identical in functionality to
 * {@link #containsValue(Object)}, and exists solely to ensure
 * full compatibility with class {@link java.util.Hashtable},
 * which supported this method prior to introduction of the
 * Java Collections framework.
 *
 * @param  value a value to search for
 * @return {@code true} if and only if some key maps to the
 *         {@code value} argument in this table as
 *         determined by the {@code equals} method;
 *         {@code false} otherwise
 * @throws NullPointerException if the specified value is null
 */
public boolean contains(Object value) {
    return containsValue(value);
}

其实map.contains(id)查的value而不是key,导致很多多余的查询和其他操作。

事务传播行为

具体知识点参考旧文:我的开发日记(三)中对于事务隔离级别事务传播行为的记录。

这里的REQUIRES_NEW表示REQUIRES_NEW :创建一个新的事务,如果当前存在事务,则把当前事务挂起。

每一个事务都会占用一个连接,然后会把之前的事务挂起等待,这样就导致会占用很多数据库连接而不释放。再加上本身有很多读写数据库的操作,所以导致了下面的报错:


2020-07-29 10:27:50 ERROR com.okay.family.service.impl.CaseCollectionServiceImpl:287 [] [Thread-176] 处理用例参数发生错误!
org.springframework.transaction.CannotCreateTransactionException: Could not open JDBC Connection for transaction; nested exception is java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30006ms.
    at org.springframework.jdbc.datasource.DataSourceTransactionManager.doBegin(DataSourceTransactionManager.java:308)
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.startTransaction(AbstractPlatformTransactionManager.java:400)
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction(AbstractPlatformTransactionManager.java:373)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.createTransactionIfNecessary(TransactionAspectSupport.java:572)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:360)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:118)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:212)
    at com.sun.proxy.$Proxy82.handleParams(Unknown Source)
    at com.okay.family.service.impl.CaseCollectionServiceImpl.lambda$null$4(CaseCollectionServiceImpl.java:282)

解决办法

调整事务传播行为

删除REQUIRES_NEW设置,恢复默认值。

设置超时时间

数据库连接池获取连接超时时间设置:

spring.datasource.hikari.connection-timeout=3000


热文精选


↙↙↙阅读原文可查看相关链接,并与作者交流