Spring JDBCTemplate事务探索

17 篇文章 0 订阅

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");

总结

  1. 事务大多都是依赖于数据库的支持。这里还特意看了一下set autocommit=0start transaction两个命令。总结来说是set autocommit=0的会把当前会话默认自动提交的行为改成为false,任何dml语句都需要commit或者rollbackstart transaction是在会话中将一些DML进行聚合,更精细。Mysql文档-autocommit,commit,rollback
  2. 是否使用使用数据库的支持,还取决于实现。有兴趣的同学可以了解一下Mybatis的缓存和com.mysql.jdbc.ConnectionImpl的实现。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值