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 |
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 的版本,则不会改变默认的事务模型,即只有新创建的集群才会默认使用悲观事务模型。
TiDB提供了数据库内部自动重试机制,当事务提交时,如果发现写写冲突,TiDB内部重新执行包含写操作的SQL语句。
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。
在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可省略。
以下使用的示例项目下载地址为:https://github.com/Adrninistrator/TiDB-Transaction-test,使用说明可参考“README.md”,
1.8
4.0.12
默认使用乐观事务模型
REPEATABLE-READ
默认开启
以下为用于验证的数据库表结构:
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;
在事务中执行了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 = ?
在示例项目中,针对不同的场景下事务执行情况进行了验证,以下为对应的类及验证的场景:
类名 | 验证的场景 |
---|---|
LockService_1_Pessimistic | 使用悲观事务 |
LockService_2a_OptimisticAutoRetry | 使用乐观事务,启用自动重试 |
LockService_3a_OptimisticNoAutoRetry | 使用乐观事务,禁用自动重试 |
LockService_3b_OptimisticNoAutoRetryRecovery | 使用乐观事务,禁用自动重试,事务开始前记录自动重试设置,事务结束前恢复自动重试设置 |
LockService_4a_OptimisticAutoRetryNoTransactionNoCheck | 使用乐观事务,使用自动重试,不使用事务,update操作不判断修改前的值 |
LockService_4b_OptimisticAutoRetryNoTransactionWithCheck | 使用乐观事务,使用自动重试,不使用事务,update操作判断修改前的值 |
在进行验证时,会对相同的功能在本机同时启动两个进程,通过定时任务在事务中执行相同的SQL语句,验证不同场景下在事务并发操作数据库表同一条记录的执行情况。
在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
后,则会超时并结束事务。
与悲观事务模型不同,乐观事务模型不支持悲观锁,使用同一条记录锁的多个事务不会串行执行,而是会并行执行,在事务提交时进行冲突处理。
在出现事务提交冲突时,只有一个事务能够执行成功,其他事务会执行失败。
在乐观事务模型下,事务提交失败时,会根据自动重试开关,决定是否进行自动重试。
可参考“乐观事务模型下写写冲突问题排查”https://docs.pingcap.com/zh/tidb/v4.0/troubleshoot-write-conflicts中的说明:
写写冲突发生在 prewrite 阶段,当发现有其他的事务在写当前 Key (data.commit_ts > txn.start_ts),则会发生写写冲突。
TiDB中控制乐观事务模型下是否进行自动重试的系统变量包括tidb_disable_txn_auto_retry
与tidb_retry_limit
,当以上两个系统变量的条件均满足时,才会启用自动重试。
当系统变量tidb_disable_txn_auto_retry
值为ON/1时,代表禁用自动重试;值为OFF/0时,代表启用自动重试。默认值为on。
系统变量tidb_retry_limit
指定了乐观事务的最大重试次数,等于0时也会禁用自动重试。默认值为10。
在乐观事务模型,启用自动重试的示例项目执行日志中,可以看到TiDB在乐观事务模型下,与MySQL的悲观锁不同,在多个事务中对数据库表同一行记录执行select for update时,不同的事务会并行执行,在事务提交时会出现冲突:一个事务能够成功执行,其他事务会执行失败,出现java.sql.SQLException
异常,错误信息如下:
can not retry select for update statement
TiDB在乐观事务模型下,只支持对不需要查询的语句进行自动重试,不支持对select for update进行自动重试
,与以上异常信息相符。
在事务中执行以下命令,以禁用自动重试:
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”不同。
在示例项目中配置数据源连接数固定为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,在创建连接的方法中,在创建连接后修改对应的会话级系统变量。
在乐观事务模型下,若出现并发修改同一行记录的情况,事务提交时会出现写冲突,在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);
}
以上在事务中并发执行select for update与update语句时,update操作可以不判断修改前的值。因为多个事务并发执行时,只会有一个事务提交成功,其他事务会提交失败。
假如执行以上select for update与update语句时不使用事务,则select for update无法起到加锁的效果,update语句变成类似MySQL中乐观锁的模式,此时update操作是否判断修改前的值,会对执行结果造成影响。
并发执行以上select for update与update语句,且不使用事务时,则都能执行成功,若update操作不判断修改前的值,则返回行数都为1。
因此在TiDB中使用乐观事务模型,且不使用事务时,并发执行以上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,可以起到锁的效果。
以上在Java应用中对TiDB在不同场景下的事务执行情况进行了验证,相对而言,通过客户端命令验证TiDB事务执行情况会更直观。
以下分别启动两个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语句时事务会串行执行,需要等待其他事务提交或回滚后再执行。
以下分别启动两个MySQL客户端,在两个会话中分别执行以下SQL语句,验证乐观事务模型下并发修改冲突:
会话序号 | SQL语句 | 执行结果 |
---|---|---|
会话 1 | start transaction; | 立刻执行完毕 |
会话 1 | select * from task_lock where task_name = ‘SchedulerTask4TiDB’ for update; | 立刻执行完毕 |
会话 1 | update task_lock set lock_flag = 0 where task_name = ‘SchedulerTask4TiDB’; | 立刻执行完毕 |
会话 2 | start transaction; | 立刻执行完毕 |
会话 2 | select * from task_lock where task_name = ‘SchedulerTask4TiDB’ for update; | 立刻执行完毕 |
会话 2 | update task_lock set lock_flag = 0 where task_name = ‘SchedulerTask4TiDB’; | 立刻执行完毕 |
会话 1 | commit; | 提交成功 |
会话 2 | commit; | 提交失败,提示“ERROR 8002 (HY000): [xxx] can not retry select for update statement” |
以上执行结果与示例项目中的执行结果相同。
以下分别启动三个MySQL客户端,在三个会话中分别执行以下SQL语句,验证乐观事务模型下事务可见性:
会话序号 | SQL语句 | 执行结果 |
---|---|---|
会话 1 | start transaction; | 立刻执行完毕 |
会话 1 | select process_info from task_lock where task_name = ‘SchedulerTask4TiDB’; | 查询到修改前的值 |
会话 1 | select CONNECTION_ID(); | 查询到会话1的连接ID(线程ID) |
会话 1 | update task_lock set process_info = concat(CONNECTION_ID(), ‘@’, now()) where task_name = ‘SchedulerTask4TiDB’; | 立刻执行完毕 |
会话 1 | select process_info from task_lock where task_name = ‘SchedulerTask4TiDB’; | 查询到修改后的值 |
会话 2 | start transaction; | 立刻执行完毕 |
会话 2 | select process_info from task_lock where task_name = ‘SchedulerTask4TiDB’; | 查询到修改前的值 |
会话 3 | select process_info from task_lock where task_name = ‘SchedulerTask4TiDB’; | 查询到修改前的值 |
会话 1 | commit; | 立刻执行完毕 |
会话 3 | select process_info from task_lock where task_name = ‘SchedulerTask4TiDB’; | 查询到修改后的值 |
会话 2 | select process_info from task_lock where task_name = ‘SchedulerTask4TiDB’; | 查询到修改前的值 |
会话 2 | commit; | 立刻执行完毕 |
会话 2 | select 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中事务结束后才能查询到其他事务已提交的数据修改。
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语句与事务执行