数据库
首页 > 数据库> > 在Spring中使用MySQL的表锁

在Spring中使用MySQL的表锁

作者:互联网

原文链接: 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

我大致翻译了一下:

表锁和事务之间存在着一些相互作用.

FLUSH TABLES WITH READ LOCK;
START TRANSACTION;
SELECT ... ;
UNLOCK TABLES;
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 的时候鸭羹就获取不到内部的锁.

我真的是没有想到, 一个普普通通的表锁, 居然还牵扯到这么多东西, 感觉我的大脑有点不够用了, 还好 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/

标签:TABLES,transaction,definition,Spring,null,MySQL,表锁
来源: https://www.cnblogs.com/tuhooo/p/16331122.html