Spring JDBCTemplate事务探索
背景
我们应用里有一个获取序列的服务,主要的流程是每次去数据库取号段,然后存储到本地。
在数据库取号段的时候使用乐观锁来保证号段的唯一性,如果失败会有重试。
交代完服务流程,下面来说一下出现的问题,偶尔会出现重试20次获取的值都一样的问题。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qkVRzpFu-1621487081077)(https://raw.githubusercontent.com/Sutonline/md-img-bed/master/sequence.jpg)]
分析问题
这里先谈一下走的弯路,因为有应用引用序列的jar包是一样,所以最开始对着另外一个应用在看代码。但是后来发现在真实出现问题的应用里,代码是有细微差异的。所以还是最好从真实现场入手。
下面是代码,我们可以看到在获取id的过程中是被事务包裹的。
@Resource(name = "productTransactionTemplateForId")
private TransactionTemplate productTransactionTemplateForId;
@Override
public long get(String name) {
return productTransactionTemplateForId.execute(transactionStatus -> {
return super.get(name);
});
}
根据对事务隔离级别的理解,默认的级别都是可重复读。所以在开启事务后,读取的数据就肯定是一致的。
// 获取当前值
DbOperator dbOperator = acquireDbOperator();
Long value = dbOperator.getPersistenceValue(name, DbOperator.BackupFlag.False);
// 加上步长去更新数据库
long updateValue = value + getBlockSize();
return acquireJdbcTemplate().update(updateSql(), updateValue, name, value);
所以即便有重试,因为在有事务中,也会取到一样的值,导致失败。验证的方法也很简单。
// 先读取到当前值 然后数据库中commit一个更新 发现每次读取到的值都是一样的 导致更新失败
public void get_with_committed() {
idHandle.get(TEST_KEY);
}
问题到这里基本可以解决了,但是不妨让我们多走一步,来看看JdbcTemplate的事务是如何运作的。
分析代码
我们先来看一个示意图,有个整体的了解之后再去细看每一部分的代码。
重点说明:
- 事务在jdbc层面是挂在数据库连接上的。
- 连接经常会有连接池进行管理
- 开启事务后会把线程和连接做绑定
下面我们一起看看代码。
开启事务:
org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin
if (txObject.getConnectionHolder() == null ||
txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
// 获取一个连接
Connection newCon = this.dataSource.getConnection();
if (logger.isDebugEnabled()) {
logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
}
txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
}
txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
con = txObject.getConnectionHolder().getConnection();
Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
txObject.setPreviousIsolationLevel(previousIsolationLevel);
// 设置autocommit = false
if (con.getAutoCommit()) {
txObject.setMustRestoreAutoCommit(true);
if (logger.isDebugEnabled()) {
logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
}
con.setAutoCommit(false);
// 绑定连接和当前线程
if (txObject.isNewConnectionHolder()) {
TransactionSynchronizationManager.bindResource(getDataSource(), txObject.getConnectionHolder());
}
}
设置AutoCommit:
com.mysql.jdbc.ConnectionImpl#setAutoCommit
if (needsSetOnServer) {
this.execSQL((StatementImpl)null, autoCommitFlag ? "SET autocommit=1" : "SET autocommit=0", -1, (Buffer)null, 1003, 1007, false, this.database, (Field[])null, false);
}
获取连接:
org.springframework.jdbc.datasource.DataSourceUtils#doGetConnection
ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
...
ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
conHolder.requested();
if (!conHolder.hasConnection()) {
logger.debug("Fetching resumed JDBC Connection from DataSource");
conHolder.setConnection(dataSource.getConnection());
}
return conHolder.getConnection();
}
其中TransactionSynchronizationManager的resource是一个threadlocal的, private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal("Transactional resources");
总结
- 事务大多都是依赖于数据库的支持。这里还特意看了一下
set autocommit=0
和start transaction
两个命令。总结来说是set autocommit=0
的会把当前会话默认自动提交的行为改成为false,任何dml语句都需要commit
或者rollback
。start transaction
是在会话中将一些DML进行聚合,更精细。Mysql文档-autocommit,commit,rollback - 是否使用使用数据库的支持,还取决于实现。有兴趣的同学可以了解一下Mybatis的缓存和
com.mysql.jdbc.ConnectionImpl
的实现。