TiDB乐观事务、悲观事务模型验证

1. 前言

TiDB与MySQL中的事务处理并不完全相同,以下在TiDB事务中执行select for update与update语句,使用数据库进行锁控制,对TiDB不同条件下事务执行情况进行分析与验证。

可参考以下相关内容:

内容链接
MySQL SQL语句与事务执行及日志分析https://blog.csdn.net/a82514921/article/details/126563449
tcpdump、Wireshark抓包分析MySQL SQL语句与事务执行https://blog.csdn.net/a82514921/article/details/126563471
Spring、MyBatis、Druid、MySQL不使用事务执行SQL语句分析https://blog.csdn.net/a82514921/article/details/126563515
Spring、MyBatis、Druid、MySQL使用事务执行SQL语句分析https://blog.csdn.net/a82514921/article/details/126563542
Spring、MyBatis、Druid、MySQL执行SQL语句与事务监控https://blog.csdn.net/a82514921/article/details/126563558
数据源使用错误导致MySQL事务失效分析https://blog.csdn.net/a82514921/article/details/126563573
TiDB乐观事务、悲观事务模型验证https://blog.csdn.net/a82514921/article/details/126563502

2. TiDB中影响事务执行的概念

2.1. 事务模型

  • 乐观事务模型

TiDB一开始支持的事务模型是乐观事务模型,与传统数据库的事务模型不同。

可参考“TiDB 乐观事务模型”https://docs.pingcap.com/zh/tidb/v4.0/optimistic-transaction

  • 悲观事务模型

TiDB从v3.0版本开始在乐观事务模型的基础上支持了悲观事务模型,与传统数据库的使用方式更贴近。

可参考“TiDB 悲观事务模型”https://docs.pingcap.com/zh/tidb/v4.0/pessimistic-transaction

自 v3.0.8 开始,新创建的 TiDB 集群默认使用悲观事务模型。但如果从 v3.0.7 版本及之前创建的集群升级到 >= v3.0.8 的版本,则不会改变默认的事务模型,即只有新创建的集群才会默认使用悲观事务模型。

2.2. 自动重试

TiDB提供了数据库内部自动重试机制,当事务提交时,如果发现写写冲突,TiDB内部重新执行包含写操作的SQL语句。

可参考“事务的重试”https://docs.pingcap.com/zh/tidb/v4.0/optimistic-transaction#%E4%BA%8B%E5%8A%A1%E7%9A%84%E9%87%8D%E8%AF%95

2.3. 事务隔离级别

TiDB乐观事务模型下,事务隔离级别只支持可重复读隔离级别(Repeatable Read),TiDb的可重复读隔离级别与ANSI及MySQL的都不完全相同。

从TiDB v4.0.0-beta版本开始,支持读已提交隔离级别(Read Committed),仅在悲观事务模式下生效。

可参考“TiDB 事务隔离级别”https://docs.pingcap.com/zh/tidb/v4.0/transaction-isolation-levels

3. TiDB系统变量查询与设置

在TiDB中对系统变量查询与设置的命令与MySQL类似,即可通过“select @@global.xxx;”查询全局系统变量,通过“select @@session.xxx;”查询会话系统变量;通过“set @@global.xxx = yyy;”设置全局系统变量,通过“set @@session.xxx = yyy;”设置会话系统变量:

select @@global.tidb_txn_mode;
select @@session.tidb_txn_mode;

set @@global.tidb_txn_mode = 'optimistic';
set @@session.tidb_txn_mode = 'optimistic';

对会话级系统变量进行修改时,@@session可省略。

4. 示例项目

以下使用的示例项目下载地址为:https://github.com/Adrninistrator/TiDB-Transaction-test,使用说明可参考“README.md”,

5. 验证环境

  • JDK版本

1.8

  • TiDB版本

4.0.12

  • TiDB事务模型

默认使用乐观事务模型

  • TiDB事务隔离级别

REPEATABLE-READ

  • TiDB乐观事务自动重试

默认开启

  • 数据库表结构

以下为用于验证的数据库表结构:

CREATE TABLE task_lock (
  task_name varchar(45) NOT NULL COMMENT '任务名',
  lock_flag int(10) NOT NULL COMMENT '锁定标识 1是已经锁 0是未锁',
  begin_time datetime NOT NULL COMMENT '任务开始时间',
  end_time datetime NULL COMMENT '任务结束时间',
  process_info varchar(100) NULL COMMENT '进程信息',
  PRIMARY KEY (task_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
  • 事务中执行的SQL语句

在事务中执行了select for update与update两条SQL语句,分别用于查询及锁定数据库表中的记录,以及将数据库表中的记录状态修改为已锁定:

select * from task_lock
where task_name = ? for update
update task_lock
set lock_flag = 1, begin_time = now(), process_info = ?
where task_name = ?

6. TiDB不同场景下事务执行情况验证

在示例项目中,针对不同的场景下事务执行情况进行了验证,以下为对应的类及验证的场景:

类名验证的场景
LockService_1_Pessimistic使用悲观事务
LockService_2a_OptimisticAutoRetry使用乐观事务,启用自动重试
LockService_3a_OptimisticNoAutoRetry使用乐观事务,禁用自动重试
LockService_3b_OptimisticNoAutoRetryRecovery使用乐观事务,禁用自动重试,事务开始前记录自动重试设置,事务结束前恢复自动重试设置
LockService_4a_OptimisticAutoRetryNoTransactionNoCheck使用乐观事务,使用自动重试,不使用事务,update操作不判断修改前的值
LockService_4b_OptimisticAutoRetryNoTransactionWithCheck使用乐观事务,使用自动重试,不使用事务,update操作判断修改前的值

在进行验证时,会对相同的功能在本机同时启动两个进程,通过定时任务在事务中执行相同的SQL语句,验证不同场景下在事务并发操作数据库表同一条记录的执行情况。

6.1. 悲观事务模型

在TiDB中,可以通过系统变量tidb_txn_mode设置事务模式,pessimistic代表悲观事务模型,optimistic代表乐观事务模型。可通过以下命令修改对应的全局系统变量,使所有会话使用悲观事务模型:

SET GLOBAL tidb_txn_mode = 'pessimistic';
set @@global.tidb_txn_mode = 'pessimistic';

也可在事务中执行“BEGIN PESSIMISTIC;”语句,显式使用悲观事务模型。

在悲观事务模型的示例项目执行日志中,可以看到TiDB在悲观事务模型下,与MySQL的悲观锁效果相同,在多个事务中对数据库表同一行记录执行select for update时,不同的事务会串行执行。

某个事务在执行时,假如被操作记录的锁已被其他事务获取,则会等待直到持有锁的事务提交或回滚释放锁后才能继续执行;若等待时间超过系统变量innodb_lock_wait_timeout后,则会超时并结束事务。

6.2. 乐观事务模型

与悲观事务模型不同,乐观事务模型不支持悲观锁,使用同一条记录锁的多个事务不会串行执行,而是会并行执行,在事务提交时进行冲突处理。

在出现事务提交冲突时,只有一个事务能够执行成功,其他事务会执行失败。

在乐观事务模型下,事务提交失败时,会根据自动重试开关,决定是否进行自动重试。

可参考“乐观事务模型下写写冲突问题排查”https://docs.pingcap.com/zh/tidb/v4.0/troubleshoot-write-conflicts中的说明:

写写冲突发生在 prewrite 阶段,当发现有其他的事务在写当前 Key (data.commit_ts > txn.start_ts),则会发生写写冲突。

TiDB中控制乐观事务模型下是否进行自动重试的系统变量包括tidb_disable_txn_auto_retrytidb_retry_limit,当以上两个系统变量的条件均满足时,才会启用自动重试。

当系统变量tidb_disable_txn_auto_retry值为ON/1时,代表禁用自动重试;值为OFF/0时,代表启用自动重试。默认值为on。

系统变量tidb_retry_limit指定了乐观事务的最大重试次数,等于0时也会禁用自动重试。默认值为10。

6.2.1. 启用自动重试

在乐观事务模型,启用自动重试的示例项目执行日志中,可以看到TiDB在乐观事务模型下,与MySQL的悲观锁不同,在多个事务中对数据库表同一行记录执行select for update时,不同的事务会并行执行,在事务提交时会出现冲突:一个事务能够成功执行,其他事务会执行失败,出现java.sql.SQLException异常,错误信息如下:

can not retry select for update statement

TiDB在乐观事务模型下,只支持对不需要查询的语句进行自动重试,不支持对select for update进行自动重试,与以上异常信息相符。

6.2.2. 禁用自动重试

在事务中执行以下命令,以禁用自动重试:

set @@tidb_disable_txn_auto_retry = on;

在乐观事务模型,启用自动重试的示例项目执行日志中,可以看到对同一行记录加锁的不同的事务会并行执行,在事务提交时会出现冲突:一个事务能够成功执行,其他事务会执行失败,也会出现java.sql.SQLException异常,错误信息如下:

Write conflict, txnStartTS=xx, conflictStartTS=xx, conflictCommitTS=xx, key={tableID=xx, indexID=xx, indexValues={xx, }} primary={tableID=xx, indexID=xx, indexValues={xx, }} \[try again later\]

禁用自动重试以后,在事务提交出现写冲突时,异常信息为“Write conflict”,与启用自动重试时的“can not retry select for update statement”不同。

6.2.2.1. 恢复自动重试系统变量

在示例项目中配置数据源连接数固定为1,确保每次数据库操作使用同一个连接,便于观察对应连接(会话)的自动重试系统变量。

由于使用的TiDB数据库默认启用了乐观事务自动重试,为了禁用自动重试功能,在示例项目的事务中首先修改会话级系统变量tidb_disable_txn_auto_retry为是,可以实现禁用所使用数据库连接(会话)的自动重试功能的效果。

在对应的事务执行完毕后,若将所使用的数据库连接直接归还到连接池中,会导致对应数据库连接(会话)的自动重试系统变量保持为修改后的值,与数据库默认值不同,后续使用对应的数据连接(会话)时可能产生难以预料的结果(可以观察示例项目LockService_3a_OptimisticNoAutoRetry对应的执行日志,在事务执行完毕后,查询tidb_disable_txn_auto_retry值为1,与数据库默认值不同)。

因此,在事务一开始查询了系统变量tidb_disable_txn_auto_retry的值并记录,在事务结束时,再修改该系统变量,恢复为原始值。在示例项目中的finally代码块中执行以上处理,保证出现异常时也能够修改(可以观察示例项目LockService_3b_OptimisticNoAutoRetryRecovery对应的执行日志,在事务执行完毕后,查询tidb_disable_txn_auto_retry值为0,与数据库默认值相同)。

在事务中修改当前数据库连接(会话)的自动重试系统变量时,修改后立刻生效,无论事务提交成功还是失败,系统变量均能修改成功。

假如需要将每个连接的会话级系统变量tidb_disable_txn_auto_retry都修改为与全局系统变量不同的值,可使用Druid的自定义Filter,在创建连接的方法中,在创建连接后修改对应的会话级系统变量。

6.2.3. 减少应用日志中预期内的异常堆栈

在乐观事务模型下,若出现并发修改同一行记录的情况,事务提交时会出现写冲突,在Java应用中出现异常的异常信息与是否启用自动重试有关。

因为并发执行导致的事务提交失败的问题是已知的正常现象,在这种情况下可以不打印异常的完整堆栈,避免在日志中出现不需要关注的异常堆栈,并减少日志量。

以示例项目在事务中执行select for update与update语句为例,以上情况出现的异常,异常类型为org.springframework.transaction.TransactionSystemException(示例项目中使用了Spring),cause的异常类型为java.sql.SQLException,cause的异常信息中包含“can not retry”或“Write conflict”,可通过以上信息进行判断,决定是否需要在日志中打印异常堆栈信息:

public static final String[] KNOWN_ERROR_MESSAGES = {"can not retry select for update statement", "Write conflict"};

catch (Exception e) {
    if (e instanceof TransactionSystemException &&
            e.getCause() instanceof SQLException &&
            StringUtils.containsAny(e.getCause().getMessage(), KNOWN_ERROR_MESSAGES)) {
        logger.warn("并发执行时,未获取到锁的事务执行失败,已知的正常现象,不需要关注 {}", e.getCause().getMessage());
        return;
    }
    logger.error("出现异常 ", e);
}

6.3. 不使用事务

以上在事务中并发执行select for update与update语句时,update操作可以不判断修改前的值。因为多个事务并发执行时,只会有一个事务提交成功,其他事务会提交失败。

假如执行以上select for update与update语句时不使用事务,则select for update无法起到加锁的效果,update语句变成类似MySQL中乐观锁的模式,此时update操作是否判断修改前的值,会对执行结果造成影响。

6.3.1. update操作不判断修改前的值

并发执行以上select for update与update语句,且不使用事务时,则都能执行成功,若update操作不判断修改前的值,则返回行数都为1。

因此在TiDB中使用乐观事务模型,且不使用事务时,并发执行以上update语句时需要判断修改前的值,避免锁失效。

6.3.2. update操作判断修改前的值

在update操作中增加对修改前的值的判断,例如每次执行时都会被修改的begin_time、process_info等字段:

update task_lock
set lock_flag = 1, begin_time = now(), process_info = ?
where task_name = ? and begin_time = ? and process_info = ?

在TiDB中使用乐观事务模型,且不使用事务时,在update操作中判断修改前的值后,执行效果与MySQL中乐观锁相同:并发更新时只有一个会话执行update语句返回行数为1,其他会话返回行数为0,可以起到锁的效果。

7. 使用客户端命令验证TiDB事务执行情况

以上在Java应用中对TiDB在不同场景下的事务执行情况进行了验证,相对而言,通过客户端命令验证TiDB事务执行情况会更直观。

7.1. 悲观事务模型

以下分别启动两个MySQL客户端,在两个会话中分别一次执行以下全部SQL语句,先在会话1中先执行,再马上在会话2中执行,验证悲观事务模型下事务执行情况:

BEGIN PESSIMISTIC;
set @@innodb_lock_wait_timeout = 5;
select now();
select * from task_lock where task_name = 'SchedulerTask4TiDB' for update;
select now();

在会话1中执行以上SQL语句时,每条语句都立刻执行完毕;

在会话2中执行以上SQL语句时,执行select for update语句时会等待:

  • 若会话1在innodb_lock_wait_timeout时间内不提交也不回滚,则会话2的select for update语句会在等待一段时间后结束,并提示“ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction”,通过两次select now()的结果可以看到两次查询到的时间相差5秒;

  • 若会话2在innodb_lock_wait_timeout时间内提交或回滚,则会话2的select for update语句会结束等待并执行完毕。

以上执行结果与示例项目及MySQL中的执行结果相同。

系统变量innodb_lock_wait_timeout在TiDB中用于设置悲观事务语句等锁时间,单位为秒,可参考https://docs.pingcap.com/zh/tidb/v4.0/system-variables#innodb_lock_wait_timeout;在MySQL中存在同名的系统变量,可参考https://dev.mysql.com/doc/refman/5.6/en/innodb-parameters.html#sysvar_innodb_lock_wait_timeout

若在TiDB使用悲观事务模型,在两个事务中执行相同的update语句,与上述SQL语句效果类似,在执行相同的udpate语句时事务会串行执行,需要等待其他事务提交或回滚后再执行。

7.2. 乐观事务模型

7.2.1. 并发修改冲突

以下分别启动两个MySQL客户端,在两个会话中分别执行以下SQL语句,验证乐观事务模型下并发修改冲突:

会话序号SQL语句执行结果
会话 1start transaction;立刻执行完毕
会话 1select * from task_lock where task_name = ‘SchedulerTask4TiDB’ for update;立刻执行完毕
会话 1update task_lock set lock_flag = 0 where task_name = ‘SchedulerTask4TiDB’;立刻执行完毕
会话 2start transaction;立刻执行完毕
会话 2select * from task_lock where task_name = ‘SchedulerTask4TiDB’ for update;立刻执行完毕
会话 2update task_lock set lock_flag = 0 where task_name = ‘SchedulerTask4TiDB’;立刻执行完毕
会话 1commit;提交成功
会话 2commit;提交失败,提示“ERROR 8002 (HY000): [xxx] can not retry select for update statement”

以上执行结果与示例项目中的执行结果相同。

7.2.2. 事务可见性

以下分别启动三个MySQL客户端,在三个会话中分别执行以下SQL语句,验证乐观事务模型下事务可见性:

会话序号SQL语句执行结果
会话 1start transaction;立刻执行完毕
会话 1select process_info from task_lock where task_name = ‘SchedulerTask4TiDB’;查询到修改前的值
会话 1select CONNECTION_ID();查询到会话1的连接ID(线程ID)
会话 1update task_lock set process_info = concat(CONNECTION_ID(), ‘@’, now()) where task_name = ‘SchedulerTask4TiDB’;立刻执行完毕
会话 1select process_info from task_lock where task_name = ‘SchedulerTask4TiDB’;查询到修改后的值
会话 2start transaction;立刻执行完毕
会话 2select process_info from task_lock where task_name = ‘SchedulerTask4TiDB’;查询到修改前的值
会话 3select process_info from task_lock where task_name = ‘SchedulerTask4TiDB’;查询到修改前的值
会话 1commit;立刻执行完毕
会话 3select process_info from task_lock where task_name = ‘SchedulerTask4TiDB’;查询到修改后的值
会话 2select process_info from task_lock where task_name = ‘SchedulerTask4TiDB’;查询到修改前的值
会话 2commit;立刻执行完毕
会话 2select process_info from task_lock where task_name = ‘SchedulerTask4TiDB’;查询到修改后的值

可以观察到以下现象:

会话3未开启事务,可在会话1的事务提交后,查询到会话1在事务中执行的修改;

会话2在会话1之后开启事务,在会话1的事务提交后,且会话2本身的事务提交后,会话2才能查询到会话1在事务中执行的修改。

对于以上现象的说明,可参考“可重复读隔离级别 (Repeatable Read)”https://docs.pingcap.com/zh/tidb/v4.0/transaction-isolation-levels#%E5%8F%AF%E9%87%8D%E5%A4%8D%E8%AF%BB%E9%9A%94%E7%A6%BB%E7%BA%A7%E5%88%AB-repeatable-read

当事务隔离级别为可重复读时,只能读到该事务启动时已经提交的其他事务修改的数据,未提交的数据或在事务启动后其他事务提交的数据是不可见的。对于本事务而言,事务语句可以看到之前的语句做出的修改。

在MySQL中,使用READ COMMITTED事务隔离级别时,事务可见性与TiDB在乐观事务模型下使用REPEATABLE READ事务隔离级别时时相比,存在相同点,即事务中查询不到其他事务未提交的数据修改;也存在不同点,即MySQL的事务中可以查询到其他事务已提交的数据修改,TiDB中事务结束后才能查询到其他事务已提交的数据修改。

8. 其他分析方法

TiDB兼容MySQL协议,且客户端使用MySQL驱动访问MySQL服务器,因此可以使用分析MySQL的方法,对TiDB的SQL语句与事务执行情况进行分析,可参考以下内容:

Spring、MyBatis、Druid、MySQL不使用事务执行SQL语句分析
Spring、MyBatis、Druid、MySQL使用事务执行SQL语句分析
Spring、MyBatis、Druid、MySQL执行SQL语句与事务监控
数据源使用错误导致MySQL事务失效分析
tcpdump、Wireshark抓包分析MySQL SQL语句与事务执行
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值