• 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';
    
    • 1
    • 2
    • 3
    • 4
    • 5

    对会话级系统变量进行修改时,@@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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 事务中执行的SQL语句

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

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

    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';
    
    • 1
    • 2

    也可在事务中执行“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),则会发生写写冲突。
    
    • 1

    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
    
    • 1

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

    6.2.2. 禁用自动重试

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

    set @@tidb_disable_txn_auto_retry = on;
    
    • 1

    在乐观事务模型,启用自动重试的示例项目执行日志中,可以看到对同一行记录加锁的不同的事务会并行执行,在事务提交时会出现冲突:一个事务能够成功执行,其他事务会执行失败,也会出现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\]
    
    • 1

    禁用自动重试以后,在事务提交出现写冲突时,异常信息为“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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    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 = ?
    
    • 1
    • 2
    • 3

    在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
    • 2
    • 3
    • 4
    • 5

    在会话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

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

    在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语句与事务执行
    
    • 1
    • 2
    • 3
    • 4
    • 5
  • 相关阅读:
    git-使用命令笔记
    六、Clion和STM32CubeMx---OLED(附案例工程)
    坠落的蚂蚁(暑假每日一题 40)
    Shiro自定义Token
    07【保姆级】-GO语言的程序流程控制【if switch for while 】
    React: JSX 、虚拟 DOM、组件配置(props、state、PropTypes、createContext、props.children)
    【HTML】【休闲益智】真相?真香?只有一个!看看谁是大馋虫 or 贪吃鬼(找出真正吃了月饼的人
    APK构建过程-命令行编译
    ASEMI整流桥KBPC3510,KBPC3510封装,KBPC3510应用
    LeetCode 周赛 335,纯纯手速场!
  • 原文地址:https://blog.csdn.net/a82514921/article/details/126563502