原文链接: http://blog.duhbb.com/2022/05/31/how-to-use-mysql-table-lock-in-spring/
欢迎访问我的个人博客: http://blog.duhbb.com/
引言
数据库是 MySQL 8.x, 在写一个批量修改加载新增的事务时, 为了避免幻读和并发修改, 我决定采用 MySQL 的表锁. 我们的业务并发量并不大, 即使不用锁也不是什么特别大的问题, 业务也不涉及到钱. 但是为了提高一下自己的姿势水平, 我还是决定处理这个并发问题. 众所周知,MySQL 的表锁的并发性能不是很高, 比 InnoDB 的行锁要差很多, 但是批量修改夹杂新增的这种操作, 并且查询条件也不是主键, 所以用 InnoDB 的行锁似乎不太行的通, 据我所著,InnoDB 加锁主要是锁主键记录的. 所以权衡之下我还是决定使用 MySQL 的表锁.
为了避免自己想当然得使用 MySQL 的表锁, 自然而然的我就上网查询 MySQL 的表锁, 结果出来的都是一些八股文, 对于写代码而言毫无用处, 他们甚至连 MySQL 如何加表锁以及如何释放表锁都不告诉, 就一张嘴, 叭叭叭...
基于这种情况, 我只能求助 MySQL 的官方文档, 不一会儿就找到了.
MySQL 8.0 中表锁的使用
原文在此: 13.3.6 LOCK TABLES and UNLOCK TABLES Statements
我大致翻译了一下:
表锁和事务之间存在着一些相互作用.
LOCK TABLES
并非事务安全的, 在尝试锁住指定的表之前会隐式地提交任何活跃的事务.UNLOCK TABLES
也会隐式地提交活跃的事务, 但前提是先使用LOCK TABLES
获取到了指定表的锁. 例如在下面的语句中,UNLOCK TABLES
会释放全局的读锁, 但是并不会提交事务, 因为根本没有获取到表锁:
FLUSH TABLES WITH READ LOCK;
START TRANSACTION;
SELECT ... ;
UNLOCK TABLES;
- 开启一个事务 (比如, 使用
START TRANSACTION
) 会隐式地提交任何当前的事务以及释放已经获取的表锁. FLUSH TABLES WITH READ LOCK
用于获取全局的读锁, 但并不会获取表锁, 因此它不会像LOCK TABLES
和UNLOCK TABLES
那样处理表锁以及隐式提交事务. 例如,START TRANSACTION
并不会释放已经获取的全局读锁.- 其他会隐式提交事务的语句不会释放已经存在的表锁, 这些语句我就不列出来了.
- 对于需要事务的表而言, 比如 InnoDB 中的表, 正确地使用
LOCK TABLES
和UNLOCK TABLES
的方法是使用SET autocommit = 0
开启事务 (而不是使用START TRANSACTION
), 然后使用LOCK TABLES
, 直到你显示地提交事务之后, 再用UNLOCK TABLES
来释放表锁. 例如, 如果你想要写入t1
并且读t2
, 你可以这么淦:
SET autocommit=0;
LOCK TABLES t1 WRITE, t2 READ, ...;
... do something with tables t1 and t2 here ...
COMMIT;
UNLOCK TABLES;
当你执行 LOCK TABLES
时,InnoDB 内部会自己搞一个表锁, 然后 MySQL 自己也会搞一个表锁.InnoDB 在下一次提交的时候会释放它自己内部的表锁, 但是 MySQL 要释放表锁得显式调用 UNLOCK TABLES
. 你不应该将 autocommit 设置为 1, 如果这样的话 InnoDB 会在调用 LOCK TABLES
后马上释放它内部的表锁, 然后就非常容易地发生死锁. 为了帮助之前的应用避免不必要的死锁,InnoDB 在 autocommit = 1
的时候鸭羹就获取不到内部的锁.
ROLLBACK
并不会释放已经获取的表锁.
我真的是没有想到, 一个普普通通的表锁, 居然还牵扯到这么多东西, 感觉我的大脑有点不够用了, 还好 MySQL 文档给出了示例, 按照下面这个套路来用就行了:
SET autocommit=0;
LOCK TABLES t1 WRITE, t2 READ, ...;
... do something with tables t1 and t2 here ...
COMMIT;
UNLOCK TABLES;
Spring 项目如何按照 MySQL 文档给出的建议操作?
反正我是会严格按照 MySQL 官网的文档来使用表锁的, 其实也很简单就是禁止 Spring 的事务, 自己来控制事务就行了. 最简单的方法就是使用 JDBC, 获取 connection 然后执行 SQL 语句. 但是 JDBC 用起来又太原始了, 我还是想用 JdbcTemplate.
我是这么做的:
首先通过 @Transactional
注解将原本 Spring 的声明式事务设置为 NOT SUPPORT
.
@Transactional(propagation= Propagation.NOT_SUPPORTED)
public Result hello() {}
这样的话,hello
方法的执行就没有 Spring 的事务包裹了, 这里 Propagation.NOT_SUPPORTED
的原理是 Spring 如果执行到 hello
发现有事务的话, 就会将现有的事务挂起, 然后再执行 hello
中的方法.
然后我用 jdbcTemplate 一通操作, 完成了这个功能.
使用 Propagation.NOT_SUPPORTED
有一个隐患, 就是 jdbcTemplate 每次执行 SQL 的时候, connection 如何保持获取到的是同一个 connection 呢?
今天想到了这个问题, 心里还有点忐忑, 代码已经提上去了.
下面是 jdbcTeamplate 获取 connection 的代码:
public <T> T execute(StatementCallback<T> action) throws DataAccessException {
Assert.notNull(action, "Callback object must not be null");
Connection con = DataSourceUtils.getConnection(getDataSource());
Statement stmt = null;
try {
Connection conToUse = con;
if (this.nativeJdbcExtractor != null &&
this.nativeJdbcExtractor.isNativeConnectionNecessaryForNativeStatements()) {
conToUse = this.nativeJdbcExtractor.getNativeConnection(con);
}
stmt = conToUse.createStatement();
applyStatementSettings(stmt);
Statement stmtToUse = stmt;
if (this.nativeJdbcExtractor != null) {
stmtToUse = this.nativeJdbcExtractor.getNativeStatement(stmt);
}
T result = action.doInStatement(stmtToUse);
handleWarnings(stmt);
return result;
}
catch (SQLException ex) {
// Release Connection early, to avoid potential connection pool deadlock
// in the case when the exception translator hasn't been initialized yet.
JdbcUtils.closeStatement(stmt);
stmt = null;
DataSourceUtils.releaseConnection(con, getDataSource());
con = null;
throw getExceptionTranslator().translate("StatementCallback", getSql(action), ex);
}
finally {
JdbcUtils.closeStatement(stmt);
DataSourceUtils.releaseConnection(con, getDataSource());
}
}
而 DataSourceUtils.java 获取数据库连接的代码如下:
public static Connection doGetConnection(DataSource dataSource) throws SQLException {
Assert.notNull(dataSource, "No DataSource specified");
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();
}
// Else we either got no holder or an empty thread-bound holder here.
logger.debug("Fetching JDBC Connection from DataSource");
Connection con = dataSource.getConnection();
if (TransactionSynchronizationManager.isSynchronizationActive()) {
try {
// Use same Connection for further JDBC actions within the transaction.
// Thread-bound object will get removed by synchronization at transaction completion.
ConnectionHolder holderToUse = conHolder;
if (holderToUse == null) {
holderToUse = new ConnectionHolder(con);
}
else {
holderToUse.setConnection(con);
}
holderToUse.requested();
TransactionSynchronizationManager.registerSynchronization(
new ConnectionSynchronization(holderToUse, dataSource));
holderToUse.setSynchronizedWithTransaction(true);
if (holderToUse != conHolder) {
TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
}
}
catch (RuntimeException ex) {
// Unexpected exception from external delegation call -> close Connection and rethrow.
releaseConnection(con, dataSource);
throw ex;
}
}
return con;
}
所以如果 TransactionSynchronizationManager.isSynchronizationActive()
为 true 的话,jdbcTemplate 每次获取的还是同一个 connection. 经过我对代码的 debug 发现 TransactionSynchronizationManager.isSynchronizationActive()
确实是为 true, 并且 jdbcTemplate 每次获取的都是同一个连接. 这就更让我费解了, 为什么都不在事务里面,Spring 还要费力气搞个局部变量存放连接了, 每次随机从 DataSource 中获取一个不行吗?
抱着这个疑问我 debug 了一下 Spring 事务相关的代码:
public final TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException {
Object transaction = doGetTransaction();
// Cache debug flag to avoid repeated checks.
boolean debugEnabled = logger.isDebugEnabled();
if (definition == null) {
// Use defaults if no transaction definition given.
definition = new DefaultTransactionDefinition();
}
if (isExistingTransaction(transaction)) {
// Existing transaction found -> check propagation behavior to find out how to behave.
return handleExistingTransaction(definition, transaction, debugEnabled);
}
// Check definition settings for new transaction.
if (definition.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) {
throw new InvalidTimeoutException("Invalid transaction timeout", definition.getTimeout());
}
// No existing transaction found -> check propagation behavior to find out how to proceed.
if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) {
throw new IllegalTransactionStateException(
"No existing transaction found for transaction marked with propagation 'mandatory'");
}
else if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
SuspendedResourcesHolder suspendedResources = suspend(null);
if (debugEnabled) {
logger.debug("Creating new transaction with name [" + definition.getName() + "]: " + definition);
}
try {
boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
DefaultTransactionStatus status = newTransactionStatus(
definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
doBegin(transaction, definition);
prepareSynchronization(status, definition);
return status;
}
catch (RuntimeException ex) {
resume(null, suspendedResources);
throw ex;
}
catch (Error err) {
resume(null, suspendedResources);
throw err;
}
}
else {
// Create "empty" transaction: no actual transaction, but potentially synchronization.
if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT && logger.isWarnEnabled()) {
logger.warn("Custom isolation level specified but no actual transaction initiated; " +
"isolation level will effectively be ignored: " + definition);
}
boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
return prepareTransactionStatus(definition, null, true, newSynchronization, debugEnabled, null);
}
}
如果是事务的传播行为是 Propagation.NOT_SUPPORTED
则走到最后一个 else 分支, 判断 newSynchronization
是否为 true, 而这个 newSynchronization
正是决定 jdbcTemplate 取出的 connection 是否放在线程局部变量中.
激动人心的时刻就要到了, 我们需要看下 getTransactionSynchronization
这个方法.
org.springframework.transaction.support.AbstractPlatformTransactionManager#setTransactionSynchronization
/**
* Set when this transaction manager should activate the thread-bound
* transaction synchronization support. Default is "always".
* <p>Note that transaction synchronization isn't supported for
* multiple concurrent transactions by different transaction managers.
* Only one transaction manager is allowed to activate it at any time.
* @see #SYNCHRONIZATION_ALWAYS
* @see #SYNCHRONIZATION_ON_ACTUAL_TRANSACTION
* @see #SYNCHRONIZATION_NEVER
* @see TransactionSynchronizationManager
* @see TransactionSynchronization
*/
public final void setTransactionSynchronization(int transactionSynchronization) {
this.transactionSynchronization = transactionSynchronization;
}
注释说了, 这个值默认的是 always
, 所以我在方法中声明了 @Transactional(propagation= Propagation.NOT_SUPPORTED)
, 然后又在这个方法中使用了 jdbcTemplate, 获取的 connection 肯定是相同的.
/**
* Always activate transaction synchronization, even for "empty" transactions
* that result from PROPAGATION_SUPPORTS with no existing backend transaction.
* @see org.springframework.transaction.TransactionDefinition#PROPAGATION_SUPPORTS
* @see org.springframework.transaction.TransactionDefinition#PROPAGATION_NOT_SUPPORTED
* @see org.springframework.transaction.TransactionDefinition#PROPAGATION_NEVER
*/
public static final int SYNCHRONIZATION_ALWAYS = 0;
/**
* Activate transaction synchronization only for actual transactions,
* that is, not for empty ones that result from PROPAGATION_SUPPORTS with
* no existing backend transaction.
* @see org.springframework.transaction.TransactionDefinition#PROPAGATION_REQUIRED
* @see org.springframework.transaction.TransactionDefinition#PROPAGATION_MANDATORY
* @see org.springframework.transaction.TransactionDefinition#PROPAGATION_REQUIRES_NEW
*/
public static final int SYNCHRONIZATION_ON_ACTUAL_TRANSACTION = 1;
/**
* Never active transaction synchronization, not even for actual transactions.
*/
public static final int SYNCHRONIZATION_NEVER = 2;
/** Constants instance for AbstractPlatformTransactionManager */
private static final Constants constants = new Constants(AbstractPlatformTransactionManager.class);
protected transient Log logger = LogFactory.getLog(getClass());
private int transactionSynchronization = SYNCHRONIZATION_ALWAYS;
虽然代码最终没有问题, 但我不太高兴, 因为自己当时写的时候并没有注意到这个;而且万一那天 Spring 改了这个逻辑, 那我的这个用法不就有问题了.
结束语
写的比较乱.... 自己还是有很多不懂, 基础不扎实
原文链接: http://blog.duhbb.com/2022/05/31/how-to-use-mysql-table-lock-in-spring/
欢迎访问我的个人博客: http://blog.duhbb.com/