diff --git a/dev/TOC.md b/dev/TOC.md index 8a16f458af23..01f4f9e814cf 100644 --- a/dev/TOC.md +++ b/dev/TOC.md @@ -235,9 +235,9 @@ - [基于角色的访问控制](/dev/reference/security/role-based-access-control.md) - [TiDB 证书鉴权使用指南](/dev/reference/security/cert-based-authentication.md) + 事务 - - [事务语句](/dev/reference/transactions/overview.md) - - [事务模型](/dev/reference/transactions/transaction-model.md) + - [事务概览](/dev/reference/transactions/overview.md) - [隔离级别](/dev/reference/transactions/transaction-isolation.md) + - [乐观事务](/dev/reference/transactions/transaction-optimistic.md) - [悲观事务](/dev/reference/transactions/transaction-pessimistic.md) + 系统数据库 - [`mysql`](/dev/reference/system-databases/mysql.md) @@ -271,7 +271,6 @@ - [Grafana 监控最佳实践](/dev/reference/best-practices/grafana-monitor.md) - [PD 调度策略最佳实践](/dev/reference/best-practices/pd-scheduling.md) - [海量 Region 集群调优最佳实践](/dev/reference/best-practices/massive-regions.md) - - [乐观锁事务最佳实践](/dev/reference/best-practices/optimistic-transaction.md) + [TiSpark 使用指南](/dev/reference/tispark.md) + TiDB Binlog - [概述](/dev/reference/tidb-binlog/overview.md) diff --git a/dev/glossary.md b/dev/glossary.md index dc91ff18aa8b..3b31e69e5d20 100644 --- a/dev/glossary.md +++ b/dev/glossary.md @@ -1,6 +1,6 @@ --- title: 术语表 -summary: 学习 TiDB 相关术语。 +summary: 了解 TiDB 相关术语。 category: glossary --- @@ -13,29 +13,16 @@ category: glossary ACID 是指数据库管理系统在写入或更新资料的过程中,为保证[事务](#事务)是正确可靠的,所必须具备的四个特性:原子性 (atomicity)、一致性 (consistency)、隔离性(isolation)以及持久性(durability)。 * 原子性 (atomicity) 指一个事务中的所有操作,或者全部完成,或者全部不完成,不会结束在中间某个环节。TiDB 通过 Primary Key 所在 [Region](#regionpeerraft-group) 的原子性来保证分布式事务的原子性。 - * 一致性 (consistency) 指在事务开始之前和结束以后,数据库的完整性没有被破坏。TiDB 在写入数据之前,会校验数据的一致性,校验通过才会写入内存并返回成功。 - * 隔离性 (isolation) 指数据库允许多个并发事务同时对其数据进行读写和修改的能力。隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致,主要用于处理并发场景。TiDB 目前只支持一种隔离级别,即可重复读。 - * 持久性 (durability) 指事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。在 TiDB 中,事务一旦提交成功,数据全部持久化存储到 TiKV,此时即使 TiDB 服务器宕机也不会出现数据丢失。 -## B - -### 悲观事务 - -悲观事务假定并发事务会发生冲突,所以每一条 SQL 语句执行后都会检测冲突,只有在确保事务一定能够执行成功后,才开始提交。另有[乐观事务](#乐观事务)。 - ## L ### Leader/Follower/Learner 它们分别对应 [Peer](#regionpeerraft-group) 的三种角色。其中 Leader 负责响应客户端的读写请求;Follower 被动地从 Leader 同步数据,当 Leader 失效时会进行选举产生新的 Leader;Learner 是一种特殊的角色,它只参与同步 raft log 而不参与投票,在目前的实现中只短暂存在于添加副本的中间步骤。 -### 乐观事务 - -乐观事务假定不会发生并发冲突,只有在事务最终提交时才会检测冲突。另有[悲观事务](#悲观事务)。 - ## O ### Operator @@ -86,16 +73,6 @@ Scheduler(调度器)是 PD 中生成调度的组件。PD 中每个调度器 - `hot-region-scheduler`:保持不同节点的读写热点 Region 均衡。 - `evict-leader-{store-id}`:驱逐某个节点的所有 Leader。(常用于滚动升级) -### 事务 - -事务指一系列有限的数据库操作序列。TiDB 中的事务具备 [ACID](#ACID) 四个特性。 - ### Store PD 中的 Store 指的是集群中的存储节点,也就是 tikv-server 实例。Store 与 TiKV 实例是严格一一对应的,即使在同一主机甚至同一块磁盘部署多个 TiKV 实例,这些实例也对会对应不同的 Store。 - -## X - -### 显式事务/隐式事务 - -由事务控制语句定义开始和结束的事务为显式事务。无需定义开始的事务为隐式事务。 \ No newline at end of file diff --git a/dev/reference/best-practices/optimistic-transaction.md b/dev/reference/best-practices/optimistic-transaction.md deleted file mode 100644 index f05a16c5438f..000000000000 --- a/dev/reference/best-practices/optimistic-transaction.md +++ /dev/null @@ -1,267 +0,0 @@ ---- -title: 乐观锁事务最佳实践 -summary: 了解 TiDB 的乐观事务模型。 -category: reference ---- - -# 乐观锁事务最佳实践 - -本文介绍 TiDB 乐观锁机制的实现原理,并通过分析乐观锁在多种场景下的应用为业务提供最佳实践。本文假定你对 [TiDB 的整体架构](/dev/architecture.md#tidb-整体架构)和 [Percolator](https://www.usenix.org/legacy/event/osdi10/tech/full_papers/Peng.pdf) 事务模型都有一定了解,相关核心概念如下: - -- [ACID](/dev/glossary.md#acid) -- [事务](/dev/glossary.md#事务) -- [乐观事务](/dev/glossary.md#乐观事务) -- [悲观事务](/dev/glossary.md#悲观事务) -- [显式事务/隐式事务](/dev/glossary.md#显式事务隐式事务) - -## 乐观事务原理 - -TiDB 中事务使用两阶段提交,流程如下: - -![TiDB 中的两阶段提交](/media/best-practices/2pc-in-tidb.png) - -1. 客户端开始一个事务。 - - TiDB 从 PD 获取一个全局唯一递增的版本号作为当前事务的开始版本号,这里定义为该事务的 `start_ts` 版本。 - -2. 客户端发起读请求。 - - a. TiDB 从 PD 获取数据路由信息,即数据具体存在哪个 TiKV 节点上。 - - b. TiDB 从 TiKV 获取 `start_ts` 版本下对应的数据信息。 - -3. 客户端发起写请求。 - - TiDB 校验写入数据是否符合一致性约束(如数据类型是否正确、是否符合唯一索引约束等)。**校验通过的数据将存放在内存里。** - -4. 客户端发起 commit。 - -5. TiDB 开始两阶段提交,保证分布式事务的原子性,让数据真正落盘。 - - a. TiDB 从当前要写入的数据中选择一个 Key 作为当前事务的 Primary Key。 - - b. TiDB 从 PD 获取所有数据的写入路由信息,并将所有的 Key 按照所有的路由进行分类。 - - c. TiDB 并发地向所有涉及的 TiKV 发起 prewrite 请求。TiKV 收到 prewrite 数据后,检查数据版本信息是否存在冲突或已过期。符合条件的数据会被加锁。 - - d. TiDB 成功收到所有 prewrite 请求。 - - e. TiDB 向 PD 获取第二个全局唯一递增版本号,定义为本次事务的 `commit_ts`。 - - f. TiDB 向 Primary Key 所在 TiKV 发起第二阶段提交。TiKV 收到 commit 操作后,检查数据合法性,清理 prewrite 阶段留下的锁。 - - g. TiDB 收到 f 成功信息。 - -6. TiDB 向客户端返回事务提交成功的信息。 - -7. TiDB 异步清理本次事务遗留的锁信息。 - -## 优缺点分析 - -通过分析 TiDB 中事务的处理流程,可以发现 TiDB 事务有如下优点: - -* 实现原理简单,易于理解。 -* 基于单实例事务实现了跨节点事务。 -* 锁管理实现了去中心化。 - -但 TiDB 事务也存在以下缺点: - -* 两阶段提交使网络交互增多。 -* 缺少一个中心化的版本管理服务。 -* 事务数据量过大时易导致内存暴涨。 - -## 事务大小 - -对于 TiDB 乐观事务而言,事务太大或者太小,都会影响事务性能。为了克服上述事务在处理过程中的不足,在实际应用中可以根据事务大小进行针对性处理。 - -### 小事务 - -在自动提交状态 (`autocommit = 1`) 下,下面三条语句各为一个事务: - -```sql -# 使用自动提交的原始版本。 -UPDATE my_table SET a ='new_value' WHERE id = 1; -UPDATE my_table SET a ='newer_value' WHERE id = 2; -UPDATE my_table SET a ='newest_value' WHERE id = 3; -``` - -此时每一条语句都需要经过两阶段提交,频繁的网络交互致使小事务延迟率高。为提升事务执行效率,可以选择使用显式事务,即在一个事务内执行三条语句: - -```sql -# 优化后版本。 -START TRANSACTION; -UPDATE my_table SET a ='new_value' WHERE id = 1; -UPDATE my_table SET a ='newer_value' WHERE id = 2; -UPDATE my_table SET a ='newest_value' WHERE id = 3; -COMMIT; -``` - -同理,执行 `INSERT` 语句时,建议使用显式事务。 - -### 大事务 - -通过分析两阶段提交的过程,可以发现单个事务过大时会存在以下问题: - -* 客户端在提交之前,数据都写在内存中,而数据量过多时易导致 OOM (Out of Memory) 错误。 -* 在第一阶段写入数据耗时增加,与其他事务出现写冲突的概率会指数级增长。 -* 最终导致事务完成提交的耗时增加。 - -TiDB 对事务做了一些限制: - -* 单个事务包含的 SQL 语句不超过 5000 条(默认) -* 每个键值对不超过 6 MB - -为了使性能达到最优,建议每 100~500 行写入一个事务。 - -TiDB 设置了键值对的总大小不超过 100 MB 默认限制,可以通过配置文件中的配置项 `txn-total-size-limit` 进行修改,最大支持到 10GB。实际的单个事务大小限制还取决于用户的内存,执行大事务时 TiDB 进程的内存消耗大约是事务大小 6 倍以上。 - -## 事务冲突 - -事务的冲突,主要指事务并发执行时对相同的 Key 进行了读写操作。冲突主要有两种形式: - -* 读写冲突:部分事务进行读操作时,有事务在同一时间对相同的 Key 进行写操作。 -* 写写冲突:不同事务同时对相同的 Key 进行写操作。 - -在 TiDB 当前的事务模型下,不会出现读写冲突,所有的读操作都不会被写操作阻塞。 - -对于写写冲突,在 TiDB 的乐观锁机制中,只有在客户端执行 `commit` 时,才会触发两阶段提交并检测是否存在写写冲突。也就是说,在乐观事务下,如果存在写写冲突,只有到事务提交阶段才会暴露出来。相对而言,悲观事务则在执行过程中就会暴露。 - -### 默认冲突行为 - -乐观事务下,默认在最终提交时才会进行冲突检测。当两个事务同时更新同一行数据,即并发事务存在冲突时,不同时间点的执行结果如下: - -![并发事务冲突流程](/media/best-practices/optimistic-transaction-table1.png) - -根据乐观锁检测写写冲突的设定,该实例的执行逻辑分析如下: - -![并发事务冲突逻辑](/media/best-practices/optimistic-transaction-case1.png) - -1. 如上图,事务 A 在时间点 `t1` 开始,事务 B 在 `t2` 开始。 - -2. 事务 A、事务 B 同时更新同一行数据。 - -3. `t4` 时,事务 A 更新 `id = 1` 的同一行数据。 虽然 `t3` 时,事务 B 已经更新了这一行数据,但是乐观事务只有在事务 commit 时才检测冲突,因此 `t4` 的操作执行成功了。 - -4. `t5` 时,事务 B 成功提交,数据落盘。 - -5. `t6` 时,事务 A 尝试提交,检测冲突时发现 `t1` 之后有新的数据写入,因此返回错误,提示客户端重试,事务 A 提交失败。 - -### 重试机制 - -TiDB 中默认使用乐观事务模型,因而在高冲突率的场景中,事务很容易提交失败。而 MySQL 内部使用的是悲观事务模型,对应到上面的实例中,事务 A 在 `t4` 时就会返回错误,提示客户端根据需求去重试。 - -换言之,MySQL 在执行 SQL 语句的过程中进行冲突检测,所以提交时很难出现异常。由于 TiDB 使用乐观锁机制造成了两边行为不一致,要兼容 MySQL 的悲观事务行为,需要在客户端修改大量的代码。为了便于广大 MySQL 用户使用,TiDB 提供了重试机制。当事务提交后,如果发现冲突,TiDB 内部重新执行包含写操作的 SQL 语句。你可以通过设置 `tidb_disable_txn_auto_retry` 和 `tidb_retry_limit` 开启自动重试: - -```toml -# 用于设置是否禁用自动重试,默认不重试。 -tidb_disable_txn_auto_retry = on -# 用来控制重试次数。只有自动重试启用时该参数才会生效。 -# 当 “tidb_retry_limit= 0” 时,也会禁用自动重试。 -tidb_retry_limit = 10 -``` - -推荐通过以下两种方式进行参数设置: - -1. Session 级别设置: - - {{< copyable "sql" >}} - - ```sql - set @@tidb_disable_txn_auto_retry = off; - set @@tidb_retry_limit = 10; - ``` - -2. Global 级别设置: - - {{< copyable "sql" >}} - - ```sql - set @@global.tidb_disable_txn_auto_retry = off; - set @@global.tidb_retry_limit = 10; - ``` - -### 重试的局限性 - -基于重试机制的原理,可将重试过程概括为以下三个步骤: - -1. 重新获取 `start_ts`。 - -2. 重新执行包含写操作的 SQL 语句。 - -3. 两阶段提交。 - -根据第二步,重试时仅重新执行包含写操作的 SQL 语句,并不涉及读操作的 SQL 语句。这会引发以下问题: - -1. `start_ts` 发生了变更。当前事务中,读到数据的时间与事务真正开始的时间发生了变化。同理,写入的版本变成了重试时获取的 `start_ts` 而非事务一开始时获取的 `start_ts`。 - -2. 当前事务中,如果存在依赖查询结果来更新的语句,结果将变得不可控。 - -以下实例来具体说明了重试的局限性。开启自动重试后,当同时更新同一行数据时,Session A 和 Session B 在不同时间点的执行结果如下: - -![自动重试流程](/media/best-practices/optimistic-transaction-table2.png) - -该实例的执行逻辑分析如下: - -![自动重试逻辑](/media/best-practices/optimistic-transaction-case2.png) - -1. 如图,Session B 在 `t2` 时开始事务 2,`t5` 时提交成功。Session A 的事务 1 在事务 2 之前开始,在事务 2 提交完成后提交。 - -2. 事务 1、事务 2 同时更新同一行数据。 - -3. Session A 提交事务 1 时发现冲突,TiDB 内部重试事务 1。 - - 1. 重新取得新的 `start_ts` 为 `t8’`。 - 2. 重新执行更新语句 `update tidb set name='pd' where id =1 and status=1`。 - 1. 发现当前版本 `t8’` 下并不存在符合条件的语句,不需要更新。 - 2. 没有数据更新,返回上层成功。 - -4. TiDB 认为事务 1 重试成功,返回客户端成功。 - -5. Session A 认为事务执行成功。如果在不存在其他更新,此时查询结果会发现数据与预想的不一致。 - -由上述分析可知,对于重试事务,当事务中更新语句需要依赖查询结果时,会重新取版本号作为 `start_ts`,所以无法保证事务原本可重复读的隔离级别,最终可能导致结果与预期出现不一致。 - -因此,如果存在依赖查询结果来更新 SQL 语句的事务,建议不要打开 TiDB 乐观锁的重试机制。 - -### 冲突预检 - -由上文可以知道,检测底层数据是否存在写写冲突是一个很重要的操作。具体而言,TiKV 在 prewrite 阶段就需要读取数据进行检测。为了优化这一块性能,TiDB 集群会在内存里面进行一次冲突预检测。 - -作为一个分布式系统,TiDB 在内存中的冲突检测主要在两个模块进行: - -* TiDB 层。如果发现 TiDB 实例本身就存在写写冲突,那么第一个写入发出后,后面的写入已经清楚地知道自己冲突了,无需再往下层 TiKV 发送请求去检测冲突。 -* TiKV 层。主要发生在 prewrite 阶段。因为 TiDB 集群是一个分布式系统,TiDB 实例本身无状态,实例之间无法感知到彼此的存在,也就无法确认自己的写入与别的 TiDB 实例是否存在冲突,所以会在 TiKV 这一层检测具体的数据是否有冲突。 - -其中 TiDB 层的冲突检测可以选择关闭,具体配置项如下: - -```toml -# 事务内存锁相关配置,当本地事务冲突比较多时建议开启。 -[txn-local-latches] -# 是否开启内存锁,默认为关闭。 -enabled = false -# Hash 对应的 slot 数,会自动向上调整为 2 的指数倍。 -# 每个 slot 占 32 Bytes 内存。当写入数据的范围比较广时(如导数据), -# 设置过小会导致变慢,性能下降。(默认为 2048000) -capacity = 2048000 -``` - -配置项 `capacity` 主要影响到冲突判断的正确性。在实现冲突检测时,不可能把所有的 Key 都存到内存里,所以真正存下来的是每个 Key 的 Hash 值。有 Hash 算法就有碰撞也就是误判的概率,这里可以通过配置 `capacity` 来控制 Hash 取模的值: - -* `capacity` 值越小,占用内存小,误判概率越大。 -* `capacity` 值越大,占用内存大,误判概率越小。 - -实际应用时,如果业务场景能够预判断写入不存在冲突(如导入数据操作),建议关闭冲突检测。 - -相应地,在 TiKV 层检测内存中是否存在冲突也有类似的机制。不同的是,TiKV 层的检测会更严格且不允许关闭,仅支持对 Hash 取模值进行配置: - -```toml -# scheduler 内置一个内存锁机制,防止同时对一个 Key 进行操作。 -# 每个 Key hash 到不同的 slot。(默认为 2048000) -scheduler-concurrency = 2048000 -``` - -此外,TiKV 支持监控等待 latch 的时间: - -![Scheduler latch wait duration](/media/best-practices/optimistic-transaction-metric.png) - -当 `Scheduler latch wait duration` 的值特别高时,说明大量时间消耗在等待锁的请求上。如果不存在底层写入慢的问题,基本上可以判断该段时间内冲突比较多。 diff --git a/dev/reference/configuration/tidb-server/tidb-specific-variables.md b/dev/reference/configuration/tidb-server/tidb-specific-variables.md index 500c2f146a45..6717ec68231e 100644 --- a/dev/reference/configuration/tidb-server/tidb-specific-variables.md +++ b/dev/reference/configuration/tidb-server/tidb-specific-variables.md @@ -311,7 +311,7 @@ set @@global.tidb_distsql_scan_concurrency = 10; 默认值:10 -这个变量用来设置最多可重试次数,即在一个事务执行中遇到可重试的错误(例如事务冲突、事务提交过慢或表结构变更)时,这个事务可以被重新执行,这个变量值表明最多可重试的次数。 +这个变量用来设置最大重试次数。一个事务执行中遇到可重试的错误(例如事务冲突、事务提交过慢或表结构变更)时,会根据该变量的设置进行重试。注意当 `tidb_retry_limit = 0` 时,也会禁用自动重试。 ### tidb_disable_txn_auto_retry @@ -325,7 +325,7 @@ set @@global.tidb_distsql_scan_concurrency = 10; 这个变量不会影响自动提交的隐式事务和 TiDB 内部执行的事务,它们依旧会根据 `tidb_retry_limit` 的值来决定最大重试次数。 -是否需要禁用自动重试,请参考[事务自动重试及带来的异常](/dev/reference/transactions/transaction-isolation.md#事务自动重试及带来的异常)。 +是否需要禁用自动重试,请参考[重试的局限性](/dev/reference/transactions/transaction-optimistic.md#重试的局限性)。 ### tidb_backoff_weight diff --git a/dev/reference/mysql-compatibility.md b/dev/reference/mysql-compatibility.md index a8620b96bb46..69368f679e95 100644 --- a/dev/reference/mysql-compatibility.md +++ b/dev/reference/mysql-compatibility.md @@ -13,7 +13,7 @@ TiDB 支持 MySQL 传输协议及其绝大多数的语法。这意味着您现 > **注意:** > -> 本页内容仅涉及 MySQL 与 TiDB 的总体差异。关于[安全特性](/dev/reference/security/compatibility.md)及[事务模型](/dev/reference/transactions/transaction-model.md)的兼容信息请查看各自具体页面。 +> 本页内容仅涉及 MySQL 与 TiDB 的总体差异。关于[安全特性](/dev/reference/security/compatibility.md)、[悲观事务模型](/dev/reference/transactions/transaction-pessimistic.md#和-mysql-innodb-的差异)的兼容信息请查看各自具体页面。 ## 不支持的特性 diff --git a/dev/reference/sql/statements/load-data.md b/dev/reference/sql/statements/load-data.md index fc0d84b09e99..94a27adaaa63 100644 --- a/dev/reference/sql/statements/load-data.md +++ b/dev/reference/sql/statements/load-data.md @@ -69,4 +69,4 @@ LOAD DATA LOCAL INFILE '/mnt/evo970/data-sets/bikeshare-data/2017Q4-capitalbikes ## 另请参阅 * [INSERT](/dev/reference/sql/statements/insert.md) -* [Transaction Model](/dev/reference/transactions/transaction-model.md) +* [乐观事务模型](/dev/reference/transactions/transaction-optimistic.md) diff --git a/dev/reference/sql/statements/select.md b/dev/reference/sql/statements/select.md index faff2fb6d6e4..e8b83945c812 100644 --- a/dev/reference/sql/statements/select.md +++ b/dev/reference/sql/statements/select.md @@ -74,7 +74,7 @@ category: reference |`HAVING where_condition` | Having 子句与 Where 子句作用类似,Having 子句可以让过滤 GroupBy 后的各种数据,Where 子句用于在聚合前过滤记录。| |`ORDER BY` | OrderBy 子句用于指定结果排序顺序,可以按照列、表达式或者是 `select_expr` 列表中某个位置的字段进行排序。| |`LIMIT` | Limit 子句用于限制结果条数。Limit 接受一个或两个数字参数,如果只有一个参数,那么表示返回数据的最大行数;如果是两个参数,那么第一个参数表示返回数据的第一行的偏移量(第一行数据的偏移量是 0),第二个参数指定返回数据的最大条目数。| -|`FOR UPDATE` | 对查询结果集所有行上锁(对于在查询条件内,但是不在结果集的行,将不会加锁,如事务启动后由其他事务写入的行),以监测其他事务对这些的并发修改。TiDB 使用[乐观事务模型](/dev/reference/transactions/transaction-model.md#事务模型)在语句执行期间不会检测锁,因此,不会像 PostgreSQL 之类的数据库一样,在当前事务结束前阻止其他事务执行 UPDATE、DELETE 和 SELECT FOR UPDATE。在事务的提交阶段 SELECT FOR UPDATE 读到的行,也会进行两阶段提交,因此,它们也可以参与事务冲突检测。如发生写入冲突,那么包含 SELECT FOR UPDATE 语句的事务会提交失败。如果没有冲突,事务将成功提交,当提交结束时,这些被加锁的行,会产生一个新版本,可以让其他尚未提交的事务,在将来提交时发现写入冲突。| +|`FOR UPDATE` | 对查询结果集所有行上锁(对于在查询条件内,但是不在结果集的行,将不会加锁,如事务启动后由其他事务写入的行),以监测其他事务对这些的并发修改。使用[乐观事务模型](/dev/reference/transactions/transaction-optimistic.md)时,语句执行期间不会检测锁,因此,不会像 PostgreSQL 之类的数据库一样,在当前事务结束前阻止其他事务执行 `UPDATE`、`DELETE` 和 `SELECT FOR UPDATE`。在事务的提交阶段 `SELECT FOR UPDATE` 读到的行,也会进行两阶段提交,因此,它们也可以参与事务冲突检测。如发生写入冲突,那么包含 `SELECT FOR UPDATE` 语句的事务会提交失败。如果没有冲突,事务将成功提交,当提交结束时,这些被加锁的行,会产生一个新版本,可以让其他尚未提交的事务,在将来提交时发现写入冲突。若使用悲观事务,则行为与其他数据库基本相同,不一致之处参考[和 MySQL InnoDB 的差异](/dev/reference/transactions/transaction-pessimistic.md#和-mysql-innodb-的差异)。 | |`LOCK IN SHARE MODE` | TiDB 出于兼容性解析这个语法,但是不做任何处理| ## 示例 diff --git a/dev/reference/transactions/overview.md b/dev/reference/transactions/overview.md index f1997839d16f..f0b40f2687b0 100644 --- a/dev/reference/transactions/overview.md +++ b/dev/reference/transactions/overview.md @@ -1,15 +1,16 @@ --- title: TiDB 事务概览 +summary: 了解 TiDB 中的事务。 category: reference --- # TiDB 事务概览 -TiDB 支持完整的分布式事务。本文主要介绍涉及到事务的语句、显式/隐式事务以及事务的隔离级别和惰性检查。 +TiDB 支持完整的分布式事务,提供[乐观事务](/dev/reference/transactions/transaction-optimistic.md)与[悲观事务](/dev/reference/transactions/transaction-pessimistic.md)(TiDB 3.0 中引入)两种事务模型。本文主要介绍涉及到事务的语句、显式/隐式事务、事务的隔离级别和惰性检查,以及事务大小的限制。 -常用的变量包括 `autocommit`、[`tidb_disable_txn_auto_retry`](/dev/reference/configuration/tidb-server/tidb-specific-variables.md#tidb_disable_txn_auto_retry) 以及 [`tidb_retry_limit`](/dev/reference/configuration/tidb-server/tidb-specific-variables.md#tidb_retry_limit)。 +常用的变量包括 [`autocommit`](#自动提交)、[`tidb_disable_txn_auto_retry`](/dev/reference/configuration/tidb-server/tidb-specific-variables.md#tidb_disable_txn_auto_retry) 以及 [`tidb_retry_limit`](/dev/reference/configuration/tidb-server/tidb-specific-variables.md#tidb_retry_limit)。 -## 事务常用语句 +## 常用事务语句 ### `BEGIN` 和 `START TRANSACTION` @@ -69,13 +70,11 @@ ROLLBACK; SET autocommit = {0 | 1} ``` -当 `autocommit = 1` 时(默认),当前的 Session 为自动提交状态。设置 `autocommit = 0` 时将更改当前 Session 为非自动提交状态。 - -自动提交状态下,每条语句运行后,TiDB 会自动将修改提交到数据库中。非自动提交状态下,通过执行 `COMMIT` 语句来手动提交事务。 +当 `autocommit = 1` 时(默认),当前的 Session 为自动提交状态,即每条语句运行后,TiDB 会自动将修改提交到数据库中。设置 `autocommit = 0` 时更改当前 Session 更改为非自动提交状态,通过执行 `COMMIT` 语句来手动提交事务。 > **注意:** > -> 某些语句执行后会导致隐式提交。例如,执行 `[BEGIN|START TRANCATION]` 语句的时候,TiDB 会试图提交上一个事务,并开启一个新的事务。详情参见 [implicit commit](https://dev.mysql.com/doc/refman/8.0/en/implicit-commit.html)。 +> 某些语句执行后会导致隐式提交。例如,执行 `[BEGIN|START TRANCATION]` 语句时,TiDB 会试图提交上一个事务,并开启一个新的事务。详情参见 [implicit commit](https://dev.mysql.com/doc/refman/8.0/en/implicit-commit.html)。 另外,`autocommit` 也是一个系统变量,你可以通过变量赋值语句修改当前 Session 或 Global 的值。 @@ -93,7 +92,7 @@ SET @@GLOBAL.autocommit = {0 | 1}; ## 显式事务和隐式事务 -TiDB 可以显式地使用事务 (`[BEGIN|START TRANSACTION]`/`COMMIT`) 或者隐式地使用事务 (`SET autocommit = 1`)。 +TiDB 可以显式地使用事务(通过 `[BEGIN|START TRANSACTION]`/`COMMIT` 语句定义事务的开始和结束) 或者隐式地使用事务 (`SET autocommit = 1`)。 在自动提交状态下,使用 `[BEGIN|START TRANSACTION]` 语句会显式地开启一个事务,同时也会禁用自动提交,使隐式事务变成显式事务。直到执行 `COMMIT` 或 `ROLLBACK` 语句时才会恢复到此前默认的自动提交状态。 @@ -109,7 +108,7 @@ TiDB **只支持** `SNAPSHOT ISOLATION`,可以通过下面的语句将当前 S SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; ``` -## 事务的惰性检查 +## 惰性检查 TiDB 中,对于普通的 `INSERT` 语句写入的值,会进行惰性检查。惰性检查的含义是,不在 `INSERT` 语句执行时进行唯一约束的检查,而在事务提交时进行唯一约束的检查。 @@ -159,4 +158,57 @@ insert into test values (3); rollback; ``` -以上例子中,第二条语句执行失败。由于调用了 `rollback`,因此事务不会将任何数据写入数据库。 +以上例子中,第二条语句执行失败。由于调用了 `ROLLBACK`,因此事务不会将任何数据写入数据库。 + +## 事务大小 + +对于 TiDB 事务而言,事务太大或太小,都会影响事务的执行效率。 + +### 小事务 + +以如下 query 为例,当 `autocommit = 1` 时,下面三条语句各为一个事务: + +{{< copyable "sql" >}} + +```sql +UPDATE my_table SET a ='new_value' WHERE id = 1; +UPDATE my_table SET a ='newer_value' WHERE id = 2; +UPDATE my_table SET a ='newest_value' WHERE id = 3; +``` + +此时每一条语句都需要经过两阶段提交,频繁的网络交互致使延迟率高。为提升事务执行效率,可以选择使用显式事务,即在一个事务内执行三条语句。 + +优化后版本: + +{{< copyable "sql" >}} + +```sql +START TRANSACTION; +UPDATE my_table SET a ='new_value' WHERE id = 1; +UPDATE my_table SET a ='newer_value' WHERE id = 2; +UPDATE my_table SET a ='newest_value' WHERE id = 3; +COMMIT; +``` + +同理,执行 `INSERT` 语句时,建议使用显式事务。 + +> **注意:** +> +> 由于 TiDB 中的资源是分布式的,TiDB 中单线程 workload 可能不会很好地利用分布式资源,因此性能相比于单实例部署的 MySQL 较低。这与 TiDB 中的事务延迟较高的情況类似。 + +### 大事务 + +由于 TiDB 两阶段提交的要求,修改数据的单个事务过大时会存在以下问题: + +* 客户端在提交之前,数据都写在内存中,而数据量过多时易导致 OOM (Out of Memory) 错误。 +* 在第一阶段写入数据耗时增加,与其他事务出现写冲突的概率会指数级增长。 +* 最终导致事务完成提交的耗时增加。 + +因此,TiDB 对事务做了一些限制: + +* 单个事务包含的 SQL 语句不超过 5000 条(默认) +* 每个键值对不超过 6 MB + +为了使性能达到最优,建议每 100~500 行写入一个事务。 + +TiDB 设置了键值对的总大小不超过 100 MB 默认限制,可以通过配置文件中的配置项 `txn-total-size-limit` 进行修改,最大支持到 10GB。实际的单个事务大小限制还取决于用户的内存,执行大事务时 TiDB 进程的内存消耗大约是事务大小 6 倍以上。 \ No newline at end of file diff --git a/dev/reference/transactions/transaction-isolation.md b/dev/reference/transactions/transaction-isolation.md index 9eb3a164dd10..2c64a949fd4b 100644 --- a/dev/reference/transactions/transaction-isolation.md +++ b/dev/reference/transactions/transaction-isolation.md @@ -1,13 +1,14 @@ --- title: TiDB 事务隔离级别 +summary: 了解 TiDB 事务的隔离级别。 category: reference --- # TiDB 事务隔离级别 -事务隔离级别是数据库事务处理的基础,ACID 中 I,即 Isolation,指的就是事务的隔离性。 +事务隔离级别是数据库事务处理的基础,[ACID](/dev/glossary.md#acid) 中的 “I”,即 Isolation,指的就是事务的隔离性。 -SQL 92 标准定义了 4 种隔离级别:读未提交、读已提交、可重复读、串行化。详见下表: +SQL-92 标准定义了 4 种隔离级别:读未提交 (READ UNCOMMITTED)、读已提交 (READ COMMITTED)、可重复读 (REPEATABLE READ)、串行化 (SERIALIZABLE)。详见下表: | Isolation Level | Dirty Write | Dirty Read | Fuzzy Read | Phantom | | ---------------- | ------------ | ------------ | ------------ | ------------ | @@ -16,13 +17,11 @@ SQL 92 标准定义了 4 种隔离级别:读未提交、读已提交、可重 | REPEATABLE READ | Not Possible | Not possible | Not possible | Possible | | SERIALIZABLE | Not Possible | Not possible | Not possible | Not possible | -TiDB 实现了快照隔离 (Snapshot Isolation) 级别的一致性。为与 MySQL 保持一致,又称其为“可重复读”。该隔离级别不同于 [ANSI 可重复读隔离级别](#与-ansi-可重复读隔离级别的区别)和 [MySQL 可重复读隔离级别](#与-mysql-可重复读隔离级别的区别)。 +TiDB 实现了快照隔离 (Snapshot Isolation, SI) 级别的一致性。为与 MySQL 保持一致,又称其为“可重复读”。该隔离级别不同于 [ANSI 可重复读隔离级别](#与-ansi-可重复读隔离级别的区别)和 [MySQL 可重复读隔离级别](#与-mysql-可重复读隔离级别的区别)。 > **注意:** > -> 在 TiDB v3.0 的默认设置中,事务的自动重试功能已经关闭。关于该项功能对隔离级别的影响以及如何开启该项功能,请参考[事务自动重试及带来的异常](#事务自动重试及带来的异常)。 - -TiDB 使用 [Percolator 事务模型](https://research.google.com/pubs/pub36726.html),当事务启动时会获取全局读时间戳,事务提交时也会获取全局提交时间戳,并以此确定事务的执行顺序,如果想了解 TiDB 事务模型的实现可以详细阅读以下两篇文章:[TiKV 的 MVCC (Multi-Version Concurrency Control) 机制](https://pingcap.com/blog-cn/mvcc-in-tikv/),[Percolator 和 TiDB 事务算法](https://pingcap.com/blog-cn/percolator-and-txn/)。 +> 在 TiDB v3.0 中,事务的自动重试功能默认为禁用状态。关于该项功能对隔离级别的影响以及如何开启该项功能,请参考[事务重试](/dev/reference/transactions/transaction-optimistic.md#重试机制)。 ## 可重复读隔离级别 (Repeatable Read) @@ -45,7 +44,7 @@ commit; | ### 与 ANSI 可重复读隔离级别的区别 -尽管名称是可重复读隔离级别,但是 TiDB 中可重复读隔离级别和 ANSI 可重复隔离级别是不同的。按照 [A Critique of ANSI SQL Isolation Levels](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/tr-95-51.pdf) 论文中的标准,TiDB 实现的是论文中的 Snapshot 隔离级别 (SI)。该隔离级别不会出现狭义上的幻读 (A3),但不会阻止广义上的幻读 (P3),同时,SI 还会出现写偏斜,而 ANSI 可重复读隔离级别不会出现写偏斜,会出现幻读。 +尽管名称是可重复读隔离级别,但是 TiDB 中可重复读隔离级别和 ANSI 可重复隔离级别是不同的。按照 [A Critique of ANSI SQL Isolation Levels](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/tr-95-51.pdf) 论文中的标准,TiDB 实现的是论文中的快照隔离级别。该隔离级别不会出现狭义上的幻读 (A3),但不会阻止广义上的幻读 (P3),同时,SI 还会出现写偏斜,而 ANSI 可重复读隔离级别不会出现写偏斜,会出现幻读。 ### 与 MySQL 可重复读隔离级别的区别 @@ -61,50 +60,6 @@ TiDB 仅在[悲观事务模式](/dev/reference/transactions/transaction-pessimis MySQL 的读已提交隔离级别大部分符合一致性读特性,但其中存在某些特例,如半一致性读 ([semi-consistent read](https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.html)),TiDB 没有兼容这个特殊行为。 -## 事务自动重试及带来的异常 - -TiDB 默认不进行事务重试,因为重试事务可能会导致更新丢失,从而破坏快照隔离。如果业务可以容忍事务重试导致的异常,或并不关注事务是否以快照隔离级别来执行,则可以开启自动重试。通过设置 `tidb_disable_txn_auto_retry = off` 可开启该项功能。需注意 `tidb_retry_limit` 的值不能为 `0`,否则会禁用自动重试。开启自动重试以后,事务遇到提交出错的可能性会降低。 - -开启自动重试后,显式事务遇到冲突可能会导致最终结果不符合预期。 - -比如下面这两个例子: - -| Session1 | Session2 | -| ---------------- | ------------ | -| `begin;` | `begin;` | -| `select balance from t where id = 1;` | `update t set balance = balance -100 where id = 1;` | -| | `update t set balance = balance -100 where id = 2;` | -| // 使用 select 的结果决定后续的逻辑 | `commit;` | -| `if balance > 100 {` | | -| `update t set balance = balance + 100 where id = 2;` | | -| `}` | | -| `commit;` // 自动重试 | | - -| Session1 | Session2 | -| ---------------- | ------------ | -| `begin;` | `begin;` | -| `update t set balance = balance - 100 where id = 1;` | `delete from t where id = 1;` | -| | `commit;` | -| // 使用 affected_rows 的结果决定后续的逻辑 | | -| `if affected_rows > 0 {` | | -| `update t set balance = balance + 100 where id = 2;` | | -| `}` | | -| `commit;` // 自动重试 | | - -因为 TiDB 自动重试机制会把事务第一次执行的所有语句重新执行一遍,当一个事务里的后续语句是否执行取决于前面语句执行结果的时候,自动重试会违反快照隔离,导致更新丢失。这种情况下,需要在应用层重试整个事务。 - -通过配置 `tidb_disable_txn_auto_retry = on` 变量可以关掉显示事务的重试。 - -{{< copyable "sql" >}} - -```sql -SET GLOBAL tidb_disable_txn_auto_retry = on; -``` - -改变 `tidb_disable_txn_auto_retry` 变量不会影响 `autocommit = 1` 的单语句的隐式事务,因为该语句的自动重试,不会造成丢失更新等异常,即不会破坏事务的隔离性。 - -关掉显式事务重试后,如果出现事务冲突,commit 语句会返回错误,错误信息会包含 `try again later` 这个字符串,应用层可以用来判断遇到的错误是否是可以重试的。 - -如果事务执行过程中包含了应用层的逻辑,建议在应用层添加显式事务的重试,并关闭自动重试。 +## 更多阅读 -`tidb_retry_limit` 变量决定了事务重试的最大次数,默认值为 10,当它被设置为 0 时,所有事务都不会自动重试,包括自动提交的单语句隐式事务。这是彻底禁用 TiDB 中自动重试机制的方法。当用户相比于事务隔离性,更关心事务执行的延迟时,可以将它设置为 0,所有冲突的事务都会以最快的方式上报失败给应用层。 +- [TiKV 的 MVCC (Multi-Version Concurrency Control) 机制](https://pingcap.com/blog-cn/mvcc-in-tikv/) \ No newline at end of file diff --git a/dev/reference/transactions/transaction-model.md b/dev/reference/transactions/transaction-model.md deleted file mode 100644 index 5b72a91d5718..000000000000 --- a/dev/reference/transactions/transaction-model.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -title: 事务模型 -category: reference ---- - -# 事务模型 - -TiDB 默认使用乐观事务模型。也就是说,在执行 `UPDATE`、`INSERT`、`DELETE` 等语句时,只有在提交过程中才会检查写写冲突,而不是像 MySQL 一样使用行锁来避免写写冲突。类似的,诸如 `GET_LOCK()` 和 `RELEASE_LOCK()` 等函数以及 `SELECT .. FOR UPDATE` 之类的语句在 TiDB 和 MySQL 中的执行方式并不相同。所以业务端在执行 SQL 语句后,需要注意检查 `COMMIT` 的返回值,即使执行时没有出错,`COMMIT` 的时候也可能会出错。 - -## 事务限制 - -由于 TiDB 分布式两阶段提交的要求,修改数据的大事务可能会出现一些问题。因此,TiDB 特意对事务大小设置了一些限制以减少这种影响: - -* 单个事务包含的 SQL 语句不超过 5000 条(默认) -* 每个键值对不超过 6MB -* 键值对的总大小不超过 100MB - -## 基于事务模型的优化实践 - -由于 TiDB 中的每个事务都需要跟 PD leader 进行两次 round trip,TiDB 中的事务相比于 MySQL 中的事务延迟更高。以如下的 query 为例,用显式事务代替 `autocommit`,可优化该 query 的性能。 - -使用 `autocommit` 的原始版本: - -{{< copyable "sql" >}} - -```sql -UPDATE my_table SET a='new_value' WHERE id = 1; -UPDATE my_table SET a='newer_value' WHERE id = 2; -UPDATE my_table SET a='newest_value' WHERE id = 3; -``` - -优化后的版本: - -{{< copyable "sql" >}} - -```sql -START TRANSACTION; -UPDATE my_table SET a='new_value' WHERE id = 1; -UPDATE my_table SET a='newer_value' WHERE id = 2; -UPDATE my_table SET a='newest_value' WHERE id = 3; -COMMIT; -``` - -> **注意:** -> -> 由于 TiDB 中的资源是分布式的,TiDB 中单线程 workload 可能不会很好地利用分布式资源,因此性能相比于单实例部署的 MySQL 较低。这与 TiDB 中的事务延迟较高的情況类似。 diff --git a/dev/reference/transactions/transaction-optimistic.md b/dev/reference/transactions/transaction-optimistic.md new file mode 100644 index 000000000000..19c146e5ec0d --- /dev/null +++ b/dev/reference/transactions/transaction-optimistic.md @@ -0,0 +1,178 @@ +--- +title: TiDB 乐观事务模型 +summary: 了解 TiDB 的乐观事务模型。 +category: reference +aliases: ['/docs-cn/dev/reference/transactions/transaction-model/'] +--- + +# TiDB 乐观事务模型 + +本文介绍 TiDB 乐观事务的原理,以及相关特性。本文假定你对 [TiDB 的整体架构](/dev/architecture.md#tidb-整体架构)、[Percolator](https://www.usenix.org/legacy/event/osdi10/tech/full_papers/Peng.pdf) 事务模型以及事务的 [ACID 特性](/dev/glossary.md#acid)都有一定了解。 + +TiDB 默认使用乐观事务模型,不会出现读写冲突,所有的读操作都不会被写操作阻塞。对于写写冲突,只有在客户端执行 `COMMIT` 时,才会触发两阶段提交并检测是否存在写写冲突。 + +> **注意:** +> +> 自 v3.0.8 开始,TiDB 默认使用[悲观事务模型](/dev/reference/transactions/transaction-pessimistic.md)。但如果从 3.0.7 及之前的版本升级到 >= 3.0.8 的版本,不会改变默认事务模型,即**只有新创建的集群才会默认使用悲观事务模型**。 + +## 乐观事务原理 + +TiDB 中事务使用两阶段提交,流程如下: + +![TiDB 中的两阶段提交](/media/2pc-in-tidb.png) + +1. 客户端开始一个事务。 + + TiDB 从 PD 获取一个全局唯一递增的版本号作为当前事务的开始版本号,这里定义为该事务的 `start_ts` 版本。 + +2. 客户端发起读请求。 + + 1. TiDB 从 PD 获取数据路由信息,即数据具体存在哪个 TiKV 节点上。 + 2. TiDB 从 TiKV 获取 `start_ts` 版本下对应的数据信息。 + +3. 客户端发起写请求。 + + TiDB 校验写入数据是否符合一致性约束(如数据类型是否正确、是否符合唯一索引约束等)。**校验通过的数据将存放在内存里。** + +4. 客户端发起 commit。 + +5. TiDB 开始两阶段提交,保证分布式事务的原子性,让数据真正落盘。 + + 1. TiDB 从当前要写入的数据中选择一个 Key 作为当前事务的 Primary Key。 + 2. TiDB 从 PD 获取所有数据的写入路由信息,并将所有的 Key 按照所有的路由进行分类。 + 3. TiDB 并发地向所有涉及的 TiKV 发起 prewrite 请求。TiKV 收到 prewrite 数据后,检查数据版本信息是否存在冲突或已过期。符合条件的数据会被加锁。 + 4. TiDB 收到所有 prewrite 响应且所有 prewrite 都成功。 + 5. TiDB 向 PD 获取第二个全局唯一递增版本号,定义为本次事务的 `commit_ts`。 + 6. TiDB 向 Primary Key 所在 TiKV 发起第二阶段提交。TiKV 收到 commit 操作后,检查数据合法性,清理 prewrite 阶段留下的锁。 + 7. TiDB 收到两阶段提交成功的信息。 + +6. TiDB 向客户端返回事务提交成功的信息。 + +7. TiDB 异步清理本次事务遗留的锁信息。 + +## 优缺点分析 + +通过分析 TiDB 中事务的处理流程,可以发现 TiDB 事务有如下优点: + +* 实现原理简单,易于理解。 +* 基于单实例事务实现了跨节点事务。 +* 锁管理实现了去中心化。 + +但 TiDB 事务也存在以下缺点: + +* 两阶段提交使网络交互增多。 +* 需要一个中心化的版本管理服务。 +* 事务数据量过大时易导致内存暴涨。 + +实际应用中,你可以[根据事务的大小进行针对性处理](/dev/reference/transactions/overview.md#事务大小),以提高事务的执行效率。 + +## 事务的重试 + +使用乐观事务模型时,在高冲突率的场景中,事务很容易提交失败。而 MySQL 内部使用的是悲观事务模型,在执行 SQL 语句的过程中进行冲突检测,所以提交时很难出现异常。为了兼容 MySQL 的悲观事务行为,TiDB 提供了重试机制。 + +### 重试机制 + +当事务提交后,如果发现冲突,TiDB 内部重新执行包含写操作的 SQL 语句。你可以通过设置 `tidb_disable_txn_auto_retry = off` 开启自动重试,并通过 `tidb_retry_limit` 设置重试次数: + +```sql +# 设置是否禁用自动重试,默认为 “on”,即不重试。 +tidb_disable_txn_auto_retry = off +# 控制重试次数,默认为 “10”。只有自动重试启用时该参数才会生效。 +# 当 “tidb_retry_limit= 0” 时,也会禁用自动重试。 +tidb_retry_limit = 10 +``` + +你也可以修改当前 Session 或 Global 的值: + +- Session 级别设置: + + {{< copyable "sql" >}} + + ```sql + set @@tidb_disable_txn_auto_retry = off; + ``` + + {{< copyable "sql" >}} + + ```sql + set @@tidb_retry_limit = 10; + ``` + +- Global 级别设置: + + {{< copyable "sql" >}} + + ```sql + set @@global.tidb_disable_txn_auto_retry = off; + ``` + + {{< copyable "sql" >}} + + ```sql + set @@global.tidb_retry_limit = 10; + ``` + +> **注意:** +> +> `tidb_retry_limit` 变量决定了事务重试的最大次数。当它被设置为 0 时,所有事务都不会自动重试,包括自动提交的单语句隐式事务。这是彻底禁用 TiDB 中自动重试机制的方法。禁用自动重试后,所有冲突的事务都会以最快的方式上报失败信息 (`try again later`) 给应用层。 + +### 重试的局限性 + +TiDB 默认不进行事务重试,因为重试事务可能会导致更新丢失,从而破坏[可重复读的隔离级别](/dev/reference/transactions/transaction-isolation.md)。 + +事务重试的局限性与其原理有关。事务重试可概括为以下三个步骤: + +1. 重新获取 `start_ts`。 +2. 重新执行包含写操作的 SQL 语句。 +3. 再次进行两阶段提交。 + +第二步中,重试时仅重新执行包含写操作的 SQL 语句,并不涉及读操作的 SQL 语句。但是当前事务中读到数据的时间与事务真正开始的时间发生了变化,写入的版本变成了重试时获取的 `start_ts` 而非事务一开始时获取的 `start_ts`。因此,当事务中存在依赖查询结果来更新的语句时,重试将无法保证事务原本可重复读的隔离级别,最终可能导致结果与预期出现不一致。 + +如果业务可以容忍事务重试导致的异常,或并不关注事务是否以可重复读的隔离级别来执行,则可以开启自动重试。 + +## 冲突检测 + +乐观事务下,检测底层数据是否存在写写冲突是一个很重要的操作。具体而言,TiKV 在 prewrite 阶段就需要读取数据进行检测。为了优化这一块性能,TiDB 集群会在内存里面进行一次冲突预检测。 + +作为一个分布式系统,TiDB 在内存中的冲突检测主要在两个模块进行: + +- TiDB 层。如果发现 TiDB 实例本身就存在写写冲突,那么第一个写入发出后,后面的写入已经清楚地知道自己冲突了,无需再往下层 TiKV 发送请求去检测冲突。 +- TiKV 层。主要发生在 prewrite 阶段。因为 TiDB 集群是一个分布式系统,TiDB 实例本身无状态,实例之间无法感知到彼此的存在,也就无法确认自己的写入与别的 TiDB 实例是否存在冲突,所以会在 TiKV 这一层检测具体的数据是否有冲突。 + +其中 TiDB 层的冲突检测可以根据场景需要选择打开或关闭,具体配置项如下: + +```toml +# 事务内存锁相关配置,当本地事务冲突比较多时建议开启。 +[txn-local-latches] +# 是否开启内存锁,默认为 false,即不开启。 +enabled = false +# Hash 对应的 slot 数,会自动向上调整为 2 的指数倍。 +# 每个 slot 占 32 Bytes 内存。当写入数据的范围比较广时(如导数据), +# 设置过小会导致变慢,性能下降。(默认为 2048000) +capacity = 2048000 +``` + +配置项 `capacity` 主要影响到冲突判断的正确性。在实现冲突检测时,不可能把所有的 Key 都存到内存里,所以真正存下来的是每个 Key 的 Hash 值。有 Hash 算法就有碰撞也就是误判的概率,这里可以通过配置 `capacity` 来控制 Hash 取模的值: + +* `capacity` 值越小,占用内存小,误判概率越大。 +* `capacity` 值越大,占用内存大,误判概率越小。 + +实际应用时,如果业务场景能够预判断写入不存在冲突(如导入数据操作),建议关闭冲突检测。 + +相应地,在 TiKV 层检测内存中是否存在冲突也有类似的机制。不同的是,TiKV 层的检测会更严格且不允许关闭,仅支持对 Hash 取模值进行配置: + +```toml +# scheduler 内置一个内存锁机制,防止同时对一个 Key 进行操作。 +# 每个 Key hash 到不同的 slot。(默认为 2048000) +scheduler-concurrency = 2048000 +``` + +此外,TiKV 支持监控等待 latch 的时间: + +![Scheduler latch wait duration](/media/optimistic-transaction-metric.png) + +当 `Scheduler latch wait duration` 的值特别高时,说明大量时间消耗在等待锁的请求上。如果不存在底层写入慢的问题,基本上可以判断该段时间内冲突比较多。 + +## 更多阅读 + +- [Percolator 和 TiDB 事务算法](https://pingcap.com/blog-cn/percolator-and-txn/) \ No newline at end of file diff --git a/dev/reference/transactions/transaction-pessimistic.md b/dev/reference/transactions/transaction-pessimistic.md index dcba48a52a52..2bebc1a600e7 100644 --- a/dev/reference/transactions/transaction-pessimistic.md +++ b/dev/reference/transactions/transaction-pessimistic.md @@ -1,12 +1,12 @@ --- -title: TiDB 悲观事务模式 +title: TiDB 悲观事务模型 +summary: 了解 TiDB 的悲观事务模型。 category: reference --- -# TiDB 悲观事务模式 +# TiDB 悲观事务模型 -TiDB 默认使用乐观事务模式,存在事务提交时因为冲突而失败的问题。为了保证事务的成功率,需要修改应用程序,加上重试的逻辑。 -悲观事务模式可以避免这个问题,应用程序无需添加重试逻辑,就可以正常执行。 +TiDB 的乐观事务模型会导致事务提交时因为冲突而失败。为了保证事务的成功率,需要修改应用程序,加上重试的逻辑。悲观事务模式可以避免这个问题,应用程序无需添加重试逻辑,就可以正常执行。 ## 悲观事务模式的行为 diff --git a/media/best-practices/2pc-in-tidb.png b/media/2pc-in-tidb.png similarity index 100% rename from media/best-practices/2pc-in-tidb.png rename to media/2pc-in-tidb.png diff --git a/media/best-practices/optimistic-transaction-case1.png b/media/best-practices/optimistic-transaction-case1.png deleted file mode 100644 index 443a6fb6de28..000000000000 Binary files a/media/best-practices/optimistic-transaction-case1.png and /dev/null differ diff --git a/media/best-practices/optimistic-transaction-case2.png b/media/best-practices/optimistic-transaction-case2.png deleted file mode 100644 index 6651b0199fcc..000000000000 Binary files a/media/best-practices/optimistic-transaction-case2.png and /dev/null differ diff --git a/media/best-practices/optimistic-transaction-table1.png b/media/best-practices/optimistic-transaction-table1.png deleted file mode 100644 index 6e19d089fcf4..000000000000 Binary files a/media/best-practices/optimistic-transaction-table1.png and /dev/null differ diff --git a/media/best-practices/optimistic-transaction-table2.png b/media/best-practices/optimistic-transaction-table2.png deleted file mode 100644 index 00a96cc195c4..000000000000 Binary files a/media/best-practices/optimistic-transaction-table2.png and /dev/null differ diff --git a/media/best-practices/optimistic-transaction-metric.png b/media/optimistic-transaction-metric.png similarity index 100% rename from media/best-practices/optimistic-transaction-metric.png rename to media/optimistic-transaction-metric.png diff --git a/v2.1/TOC.md b/v2.1/TOC.md index d7fbecc23d8b..d3dbd2a7fa79 100644 --- a/v2.1/TOC.md +++ b/v2.1/TOC.md @@ -216,9 +216,9 @@ - [TiDB 数据库权限管理](/v2.1/reference/security/privilege-system.md) - [TiDB 用户账户管理](/v2.1/reference/security/user-account-management.md) + 事务 - - [事务语句](/v2.1/reference/transactions/overview.md) - - [事务模型](/v2.1/reference/transactions/transaction-model.md) + - [事务概览](/v2.1/reference/transactions/overview.md) - [隔离级别](/v2.1/reference/transactions/transaction-isolation.md) + - [乐观事务](/v2.1/reference/transactions/transaction-optimistic.md) + 系统数据库 - [`mysql`](/v2.1/reference/system-databases/mysql.md) - [`information_schema`](/v2.1/reference/system-databases/information-schema.md) @@ -244,7 +244,6 @@ - [Grafana 监控最佳实践](/v2.1/reference/best-practices/grafana-monitor.md) - [PD 调度策略最佳实践](/v2.1/reference/best-practices/pd-scheduling.md) - [海量 Region 集群调优最佳实践](/v2.1/reference/best-practices/massive-regions.md) - - [乐观锁事务最佳实践](/v2.1/reference/best-practices/optimistic-transaction.md) + [TiSpark 使用指南](/v2.1/reference/tispark.md) + TiDB Binlog - [概述](/v2.1/reference/tidb-binlog/overview.md) diff --git a/v2.1/glossary.md b/v2.1/glossary.md index dc91ff18aa8b..3b31e69e5d20 100644 --- a/v2.1/glossary.md +++ b/v2.1/glossary.md @@ -1,6 +1,6 @@ --- title: 术语表 -summary: 学习 TiDB 相关术语。 +summary: 了解 TiDB 相关术语。 category: glossary --- @@ -13,29 +13,16 @@ category: glossary ACID 是指数据库管理系统在写入或更新资料的过程中,为保证[事务](#事务)是正确可靠的,所必须具备的四个特性:原子性 (atomicity)、一致性 (consistency)、隔离性(isolation)以及持久性(durability)。 * 原子性 (atomicity) 指一个事务中的所有操作,或者全部完成,或者全部不完成,不会结束在中间某个环节。TiDB 通过 Primary Key 所在 [Region](#regionpeerraft-group) 的原子性来保证分布式事务的原子性。 - * 一致性 (consistency) 指在事务开始之前和结束以后,数据库的完整性没有被破坏。TiDB 在写入数据之前,会校验数据的一致性,校验通过才会写入内存并返回成功。 - * 隔离性 (isolation) 指数据库允许多个并发事务同时对其数据进行读写和修改的能力。隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致,主要用于处理并发场景。TiDB 目前只支持一种隔离级别,即可重复读。 - * 持久性 (durability) 指事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。在 TiDB 中,事务一旦提交成功,数据全部持久化存储到 TiKV,此时即使 TiDB 服务器宕机也不会出现数据丢失。 -## B - -### 悲观事务 - -悲观事务假定并发事务会发生冲突,所以每一条 SQL 语句执行后都会检测冲突,只有在确保事务一定能够执行成功后,才开始提交。另有[乐观事务](#乐观事务)。 - ## L ### Leader/Follower/Learner 它们分别对应 [Peer](#regionpeerraft-group) 的三种角色。其中 Leader 负责响应客户端的读写请求;Follower 被动地从 Leader 同步数据,当 Leader 失效时会进行选举产生新的 Leader;Learner 是一种特殊的角色,它只参与同步 raft log 而不参与投票,在目前的实现中只短暂存在于添加副本的中间步骤。 -### 乐观事务 - -乐观事务假定不会发生并发冲突,只有在事务最终提交时才会检测冲突。另有[悲观事务](#悲观事务)。 - ## O ### Operator @@ -86,16 +73,6 @@ Scheduler(调度器)是 PD 中生成调度的组件。PD 中每个调度器 - `hot-region-scheduler`:保持不同节点的读写热点 Region 均衡。 - `evict-leader-{store-id}`:驱逐某个节点的所有 Leader。(常用于滚动升级) -### 事务 - -事务指一系列有限的数据库操作序列。TiDB 中的事务具备 [ACID](#ACID) 四个特性。 - ### Store PD 中的 Store 指的是集群中的存储节点,也就是 tikv-server 实例。Store 与 TiKV 实例是严格一一对应的,即使在同一主机甚至同一块磁盘部署多个 TiKV 实例,这些实例也对会对应不同的 Store。 - -## X - -### 显式事务/隐式事务 - -由事务控制语句定义开始和结束的事务为显式事务。无需定义开始的事务为隐式事务。 \ No newline at end of file diff --git a/v2.1/reference/best-practices/optimistic-transaction.md b/v2.1/reference/best-practices/optimistic-transaction.md deleted file mode 100644 index 742c481808fb..000000000000 --- a/v2.1/reference/best-practices/optimistic-transaction.md +++ /dev/null @@ -1,264 +0,0 @@ ---- -title: 乐观锁事务最佳实践 -summary: 了解 TiDB 的乐观事务模型。 -category: reference ---- - -# 乐观锁事务最佳实践 - -本文介绍 TiDB 乐观锁机制的实现原理,并通过分析乐观锁在多种场景下的应用为业务提供最佳实践。本文假定你对 [TiDB 的整体架构](/v2.1/architecture.md#tidb-整体架构)和 [Percolator](https://www.usenix.org/legacy/event/osdi10/tech/full_papers/Peng.pdf) 事务模型都有一定了解,相关核心概念如下: - -- [ACID](/v2.1/glossary.md#acid) -- [事务](/v2.1/glossary.md#事务) -- [乐观事务](/v2.1/glossary.md#乐观事务) -- [悲观事务](/v2.1/glossary.md#悲观事务) -- [显式事务/隐式事务](/v2.1/glossary.md#显式事务隐式事务) - -## 乐观事务原理 - -TiDB 中事务使用两阶段提交,流程如下: - -![TiDB 中的两阶段提交](/media/best-practices/2pc-in-tidb.png) - -1. 客户端开始一个事务。 - - TiDB 从 PD 获取一个全局唯一递增的版本号作为当前事务的开始版本号,这里定义为该事务的 `start_ts` 版本。 - -2. 客户端发起读请求。 - - a. TiDB 从 PD 获取数据路由信息,即数据具体存在哪个 TiKV 节点上。 - - b. TiDB 从 TiKV 获取 `start_ts` 版本下对应的数据信息。 - -3. 客户端发起写请求。 - - TiDB 校验写入数据是否符合一致性约束(如数据类型是否正确、是否符合唯一索引约束等)。**校验通过的数据将存放在内存里。** - -4. 客户端发起 commit。 - -5. TiDB 开始两阶段提交,保证分布式事务的原子性,让数据真正落盘。 - - a. TiDB 从当前要写入的数据中选择一个 Key 作为当前事务的 Primary Key。 - - b. TiDB 从 PD 获取所有数据的写入路由信息,并将所有的 Key 按照所有的路由进行分类。 - - c. TiDB 并发地向所有涉及的 TiKV 发起 prewrite 请求。TiKV 收到 prewrite 数据后,检查数据版本信息是否存在冲突或已过期。符合条件的数据会被加锁。 - - d. TiDB 成功收到所有 prewrite 请求。 - - e. TiDB 向 PD 获取第二个全局唯一递增版本号,定义为本次事务的 `commit_ts`。 - - f. TiDB 向 Primary Key 所在 TiKV 发起第二阶段提交。TiKV 收到 commit 操作后,检查数据合法性,清理 prewrite 阶段留下的锁。 - - g. TiDB 收到 f 成功信息。 - -6. TiDB 向客户端返回事务提交成功的信息。 - -7. TiDB 异步清理本次事务遗留的锁信息。 - -## 优缺点分析 - -通过分析 TiDB 中事务的处理流程,可以发现 TiDB 事务有如下优点: - -* 实现原理简单,易于理解。 -* 基于单实例事务实现了跨节点事务。 -* 锁管理实现了去中心化。 - -但 TiDB 事务也存在以下缺点: - -* 两阶段提交使网络交互增多。 -* 缺少一个中心化的版本管理服务。 -* 事务数据量过大时易导致内存暴涨。 - -## 事务大小 - -对于 TiDB 乐观事务而言,事务太大或者太小,都会影响事务性能。为了克服上述事务在处理过程中的不足,在实际应用中可以根据事务大小进行针对性处理。 - -### 小事务 - -在自动提交状态 (`autocommit = 1`) 下,下面三条语句各为一个事务: - -```sql -# 使用自动提交的原始版本。 -UPDATE my_table SET a ='new_value' WHERE id = 1; -UPDATE my_table SET a ='newer_value' WHERE id = 2; -UPDATE my_table SET a ='newest_value' WHERE id = 3; -``` - -此时每一条语句都需要经过两阶段提交,频繁的网络交互致使小事务延迟率高。为提升事务执行效率,可以选择使用显式事务,即在一个事务内执行三条语句: - -```sql -# 优化后版本。 -START TRANSACTION; -UPDATE my_table SET a ='new_value' WHERE id = 1; -UPDATE my_table SET a ='newer_value' WHERE id = 2; -UPDATE my_table SET a ='newest_value' WHERE id = 3; -COMMIT; -``` - -同理,执行 `INSERT` 语句时,建议使用显式事务。 - -### 大事务 - -通过分析两阶段提交的过程,可以发现单个事务过大时会存在以下问题: - -* 客户端在提交之前,数据都写在内存中,而数据量过多时易导致 OOM (Out of Memory) 错误。 -* 在第一阶段写入数据时,与其他事务出现冲突的概率会指数级增长,使事务之间相互阻塞影响。 -* 最终导致事务完成提交的耗时增加。 - -因此,TiDB 特意对事务的大小做了一些限制: - -* 单个事务包含的 SQL 语句不超过 5000 条(默认) -* 每个键值对不超过 6 MB -* 键值对的总数不超过 300000 -* 键值对的总大小不超过 100 MB - -为了使性能达到最优,建议每 100~500 行写入一个事务。 - -## 事务冲突 - -事务的冲突,主要指事务并发执行时对相同的 Key 进行了读写操作。冲突主要有两种形式: - -* 读写冲突:部分事务进行读操作时,有事务在同一时间对相同的 Key 进行写操作。 -* 写写冲突:不同事务同时对相同的 Key 进行写操作。 - -在 TiDB 的乐观锁机制中,只有在客户端执行 `commit` 时,才会触发两阶段提交并检测是否存在写写冲突。也就是说,在乐观事务下,如果存在写写冲突,在事务提交阶段就会暴露出来,因而更容易被用户感知。 - -### 默认冲突行为 - -乐观事务下,默认在最终提交时才会进行冲突检测。当两个事务同时更新同一行数据,即并发事务存在冲突时,不同时间点的执行结果如下: - -![并发事务冲突流程](/media/best-practices/optimistic-transaction-table1.png) - -根据乐观锁检测写写冲突的设定,该实例的执行逻辑分析如下: - -![并发事务冲突逻辑](/media/best-practices/optimistic-transaction-case1.png) - -1. 如上图,事务 A 在时间点 `t1` 开始,事务 B 在 `t2` 开始。 - -2. 事务 A、事务 B 同时更新同一行数据。 - -3. `t4` 时,事务 A 更新 `id = 1` 的同一行数据。 虽然 `t3` 时,事务 B 已经更新了这一行数据,但是乐观事务只有在事务 commit 时才检测冲突,因此 `t4` 的操作执行成功了。 - -4. `t5` 时,事务 B 成功提交,数据落盘。 - -5. `t6` 时,事务 A 尝试提交,检测冲突时发现 `t1` 之后有新的数据写入,因此返回错误,提示客户端重试,事务 A 提交失败。 - -### 重试机制 - -TiDB 中默认使用乐观事务模型,因而在高冲突率的场景中,事务很容易提交失败。而 MySQL 内部使用的是悲观事务模型,对应到上面的实例中,事务 A 在 `t4` 时就会返回错误,提示客户端根据需求去重试。 - -换言之,MySQL 在执行 SQL 语句的过程中进行冲突检测,所以提交时很难出现异常。由于 TiDB 使用乐观锁机制造成了两边行为不一致,要兼容 MySQL 的悲观事务行为,需要在客户端修改大量的代码。为了便于广大 MySQL 用户使用,TiDB 提供了重试机制。当事务提交后,如果发现冲突,TiDB 内部重新执行包含写操作的 SQL 语句。你可以通过设置 `tidb_disable_txn_auto_retry` 和 `tidb_retry_limit` 开启自动重试: - -```toml -# 用于设置是否禁用自动重试,默认不重试。 -tidb_disable_txn_auto_retry = on -# 用来控制重试次数。只有自动重试启用时该参数才会生效。 -# 当 “tidb_retry_limit= 0” 时,也会禁用自动重试。 -tidb_retry_limit = 10 -``` - -推荐通过以下两种方式进行参数设置: - -1. Session 级别设置: - - {{< copyable "sql" >}} - - ```sql - set @@tidb_disable_txn_auto_retry = off; - set @@tidb_retry_limit = 10; - ``` - -2. Global 级别设置: - - {{< copyable "sql" >}} - - ```sql - set @@global.tidb_disable_txn_auto_retry = off; - set @@global.tidb_retry_limit = 10; - ``` - -### 重试的局限性 - -基于重试机制的原理,可将重试过程概括为以下三个步骤: - -1. 重新获取 `start_ts`。 - -2. 重新执行包含写操作的 SQL 语句。 - -3. 两阶段提交。 - -根据第二步,重试时仅重新执行包含写操作的 SQL 语句,并不涉及读操作的 SQL 语句。这会引发以下问题: - -1. `start_ts` 发生了变更。当前事务中,读到数据的时间与事务真正开始的时间发生了变化。同理,写入的版本变成了重试时获取的 `start_ts` 而非事务一开始时获取的 `start_ts`。 - -2. 当前事务中,如果存在依赖查询结果来更新的语句,结果将变得不可控。 - -以下实例来具体说明了重试的局限性。开启自动重试后,当同时更新同一行数据时,Session A 和 Session B 在不同时间点的执行结果如下: - -![自动重试流程](/media/best-practices/optimistic-transaction-table2.png) - -该实例的执行逻辑分析如下: - -![自动重试逻辑](/media/best-practices/optimistic-transaction-case2.png) - -1. 如图,Session B 在 `t2` 时开始事务 2,`t5` 时提交成功。Session A 的事务 1 在事务 2 之前开始,在事务 2 提交完成后提交。 - -2. 事务 1、事务 2 同时更新同一行数据。 - -3. Session A 提交事务 1 时发现冲突,TiDB 内部重试事务 1。 - 1. 重新取得新的 `start_ts` 为 `t8’`。 - 2. 重新执行更新语句 `update tidb set name='pd' where id =1 and status=1`。 - 1. 发现当前版本 `t8’` 下并不存在符合条件的语句,不需要更新。 - 2. 没有数据更新,返回上层成功。 - -4. TiDB 认为事务 1 重试成功,返回客户端成功。 - -5. Session A 认为事务执行成功。如果在不存在其他更新,此时查询结果会发现数据与预想的不一致。 - -由上述分析可知,对于重试事务,当事务中更新语句需要依赖查询结果时,会重新取版本号作为 `start_ts`,所以无法保证事务原本可重复读的隔离级别,最终可能导致结果与预期出现不一致。 - -因此,如果存在依赖查询结果来更新 SQL 语句的事务,建议不要打开 TiDB 乐观锁的重试机制。 - -### 冲突预检 - -由上文可以知道,检测底层数据是否存在写写冲突是一个很重要的操作。具体而言,TiKV 在 prewrite 阶段就需要读取数据进行检测。为了优化这一块性能,TiDB 集群会在内存里面进行一次冲突预检测。 - -作为一个分布式系统,TiDB 在内存中的冲突检测主要在两个模块进行: - -* TiDB 层。如果发现 TiDB 实例本身就存在写写冲突,那么第一个写入发出后,后面的写入已经清楚地知道自己冲突了,无需再往下层 TiKV 发送请求去检测冲突。 -* TiKV 层。主要发生在 prewrite 阶段。因为 TiDB 集群是一个分布式系统,TiDB 实例本身无状态,实例之间无法感知到彼此的存在,也就无法确认自己的写入与别的 TiDB 实例是否存在冲突,所以会在 TiKV 这一层检测具体的数据是否有冲突。 - -其中 TiDB 层的冲突检测可以选择关闭,具体配置项如下: - -```toml -# 事务内存锁相关配置,当本地事务冲突比较多时建议开启。 -[txn-local-latches] -# 是否开启内存锁,默认为关闭。 -enabled = false -# Hash 对应的 slot 数,会自动向上调整为 2 的指数倍。 -# 每个 slot 占 32 Bytes 内存。当写入数据的范围比较广时(如导数据), -# 设置过小会导致变慢,性能下降。(默认为 2048000) -capacity = 2048000 -``` - -配置项 `capacity` 主要影响到冲突判断的正确性。在实现冲突检测时,不可能把所有的 Key 都存到内存里,所以真正存下来的是每个 Key 的 Hash 值。有 Hash 算法就有碰撞也就是误判的概率,这里可以通过配置 `capacity` 来控制 Hash 取模的值: - -* `capacity` 值越小,占用内存小,误判概率越大。 -* `capacity` 值越大,占用内存大,误判概率越小。 - -实际应用时,如果业务场景能够预判断写入不存在冲突(如导入数据操作),建议关闭冲突检测。 - -相应地,在 TiKV 层检测内存中是否存在冲突也有类似的机制。不同的是,TiKV 层的检测会更严格且不允许关闭,仅支持对 Hash 取模值进行配置: - -```toml -# scheduler 内置一个内存锁机制,防止同时对一个 Key 进行操作。 -# 每个 Key hash 到不同的 slot。(默认为 2048000) -scheduler-concurrency = 2048000 -``` - -此外,TiKV 支持监控等待 latch 的时间: - -![Scheduler latch wait duration](/media/best-practices/optimistic-transaction-metric.png) - -当 `Scheduler latch wait duration` 的值特别高时,说明大量时间消耗在等待锁的请求上。如果不存在底层写入慢的问题,基本上可以判断该段时间内冲突比较多。 diff --git a/v2.1/reference/configuration/tidb-server/tidb-specific-variables.md b/v2.1/reference/configuration/tidb-server/tidb-specific-variables.md index 11bb30384a71..eaccd0a77121 100644 --- a/v2.1/reference/configuration/tidb-server/tidb-specific-variables.md +++ b/v2.1/reference/configuration/tidb-server/tidb-specific-variables.md @@ -283,7 +283,7 @@ set @@global.tidb_distsql_scan_concurrency = 10 默认值:10 -这个变量用来设置最多可重试次数,即在一个事务执行中遇到可重试的错误(例如事务冲突、事务提交过慢或表结构变更)时,这个事务可以被重新执行,这个变量值表明最多可重试的次数。 +这个变量用来设置最大重试次数。一个事务执行中遇到可重试的错误(例如事务冲突、事务提交过慢或表结构变更)时,会根据该变量的设置进行重试。注意当 `tidb_retry_limit = 0` 时,也会禁用自动重试。 ### tidb_disable_txn_auto_retry @@ -297,7 +297,7 @@ set @@global.tidb_distsql_scan_concurrency = 10 这个变量不会影响自动提交的隐式事务和 TiDB 内部执行的事务,它们依旧会根据 `tidb_retry_limit` 的值来决定最大重试次数。 -是否需要禁用自动重试,请参考[事务自动重试及带来的异常](/v2.1/reference/transactions/transaction-isolation.md#事务自动重试及带来的异常)。 +是否需要禁用自动重试,请参考[重试的局限性](/v2.1/reference/transactions/transaction-optimistic.md#重试的局限性)。 ### tidb_backoff_weight diff --git a/v2.1/reference/mysql-compatibility.md b/v2.1/reference/mysql-compatibility.md index 1c22ce9a56c0..1c3167efbe86 100644 --- a/v2.1/reference/mysql-compatibility.md +++ b/v2.1/reference/mysql-compatibility.md @@ -13,7 +13,7 @@ TiDB 支持 MySQL 传输协议及其绝大多数的语法。这意味着您现 > **注意:** > -> 本页内容仅涉及 MySQL 与 TiDB 的总体差异。关于[安全特性](/v2.1/reference/security/compatibility.md)及[事务模型](/v2.1/reference/transactions/transaction-model.md)的兼容信息请查看各自具体页面。 +> 本页内容仅涉及 MySQL 与 TiDB 的总体差异。关于[安全特性](/v2.1/reference/security/compatibility.md)的兼容信息请查看各自具体页面。 ## 不支持的特性 diff --git a/v2.1/reference/sql/statements/load-data.md b/v2.1/reference/sql/statements/load-data.md index 4a7f3452540d..65c848c85537 100644 --- a/v2.1/reference/sql/statements/load-data.md +++ b/v2.1/reference/sql/statements/load-data.md @@ -48,4 +48,4 @@ Records: 815264 Deleted: 0 Skipped: 0 Warnings: 0 ## 另请参阅 * [INSERT](/v2.1/reference/sql/statements/insert.md) -* [Transaction Model](/v2.1/reference/transactions/transaction-model.md) +* [乐观事务模型](/v2.1/reference/transactions/transaction-optimistic.md) diff --git a/v2.1/reference/sql/statements/select.md b/v2.1/reference/sql/statements/select.md index a8041459fb19..6ad448008338 100644 --- a/v2.1/reference/sql/statements/select.md +++ b/v2.1/reference/sql/statements/select.md @@ -74,7 +74,7 @@ category: reference |`HAVING where_condition` | Having 子句与 Where 子句作用类似,Having 子句可以让过滤 GroupBy 后的各种数据,Where 子句用于在聚合前过滤记录。| |`ORDER BY` | OrderBy 子句用于指定结果排序顺序,可以按照列、表达式或者是 `select_expr` 列表中某个位置的字段进行排序。| |`LIMIT` | Limit 子句用于限制结果条数。Limit 接受一个或两个数字参数,如果只有一个参数,那么表示返回数据的最大行数;如果是两个参数,那么第一个参数表示返回数据的第一行的偏移量(第一行数据的偏移量是 0),第二个参数指定返回数据的最大条目数。| -|`FOR UPDATE` | 对查询结果集所有行上锁(对于在查询条件内,但是不在结果集的行,将不会加锁,如事务启动后由其他事务写入的行),以监测其他事务对这些的并发修改。TiDB 使用[乐观事务模型](/v2.1/reference/transactions/transaction-model.md#事务模型)在语句执行期间不会检测锁,因此,不会像 PostgreSQL 之类的数据库一样,在当前事务结束前阻止其他事务执行 UPDATE、DELETE 和 SELECT FOR UPDATE。在事务的提交阶段 SELECT FOR UPDATE 读到的行,也会进行两阶段提交,因此,它们也可以参与事务冲突检测。如发生写入冲突,那么包含 SELECT FOR UPDATE 语句的事务会提交失败。如果没有冲突,事务将成功提交,当提交结束时,这些被加锁的行,会产生一个新版本,可以让其他尚未提交的事务,在将来提交时发现写入冲突。| +|`FOR UPDATE` | 对查询结果集所有行上锁(对于在查询条件内,但是不在结果集的行,将不会加锁,如事务启动后由其他事务写入的行),以监测其他事务对这些的并发修改。使用[乐观事务模型](/v2.1/reference/transactions/transaction-optimistic.md)时,语句执行期间不会检测锁,因此,不会像 PostgreSQL 之类的数据库一样,在当前事务结束前阻止其他事务执行 `UPDATE`、`DELETE` 和 `SELECT FOR UPDATE`。在事务的提交阶段 `SELECT FOR UPDATE` 读到的行,也会进行两阶段提交,因此,它们也可以参与事务冲突检测。如发生写入冲突,那么包含 `SELECT FOR UPDATE` 语句的事务会提交失败。如果没有冲突,事务将成功提交,当提交结束时,这些被加锁的行,会产生一个新版本,可以让其他尚未提交的事务,在将来提交时发现写入冲突。 | |`LOCK IN SHARE MODE` | TiDB 出于兼容性解析这个语法,但是不做任何处理| ## 示例 diff --git a/v2.1/reference/transactions/overview.md b/v2.1/reference/transactions/overview.md index be42c7e901e5..5be9dcddf356 100644 --- a/v2.1/reference/transactions/overview.md +++ b/v2.1/reference/transactions/overview.md @@ -1,15 +1,16 @@ --- title: TiDB 事务概览 +summary: 了解 TiDB 中的事务。 category: reference --- # TiDB 事务概览 -TiDB 支持完整的分布式事务。本文主要介绍涉及到事务的语句、显式/隐式事务以及事务的隔离级别和惰性检查。 +TiDB 支持完整的分布式事务,使用[乐观事务模型](/v2.1/reference/transactions/transaction-optimistic.md)。本文主要介绍涉及到事务的语句、显式/隐式事务、事务的隔离级别和惰性检查,以及事务大小的限制。 -常用的变量包括 `autocommit`、[`tidb_disable_txn_auto_retry`](/v2.1/reference/configuration/tidb-server/tidb-specific-variables.md#tidb_disable_txn_auto_retry) 以及 [`tidb_retry_limit`](/v2.1/reference/configuration/tidb-server/tidb-specific-variables.md#tidb_retry_limit)。 +常用的变量包括 [`autocommit`](#自动提交)、[`tidb_disable_txn_auto_retry`](/v2.1/reference/configuration/tidb-server/tidb-specific-variables.md#tidb_disable_txn_auto_retry) 以及 [`tidb_retry_limit`](/v2.1/reference/configuration/tidb-server/tidb-specific-variables.md#tidb_retry_limit)。 -## 事务常用语句 +## 常用事务语句 ### `BEGIN` 和 `START TRANSACTION` @@ -69,13 +70,11 @@ ROLLBACK; SET autocommit = {0 | 1} ``` -当 `autocommit = 1` 时(默认),当前的 Session 为自动提交状态。设置 `autocommit = 0` 时将更改当前 Session 为非自动提交状态。 - -自动提交状态下,每条语句运行后,TiDB 会自动将修改提交到数据库中。非自动提交状态下,通过执行 `COMMIT` 语句来手动提交事务。 +当 `autocommit = 1` 时(默认),当前的 Session 为自动提交状态,即每条语句运行后,TiDB 会自动将修改提交到数据库中。设置 `autocommit = 0` 时更改当前 Session 更改为非自动提交状态,通过执行 `COMMIT` 语句来手动提交事务。 > **注意:** > -> 某些语句执行后会导致隐式提交。例如,执行 `[BEGIN|START TRANCATION]` 语句的时候,TiDB 会试图提交上一个事务,并开启一个新的事务。详情参见 [implicit commit](https://dev.mysql.com/doc/refman/8.0/en/implicit-commit.html)。 +> 某些语句执行后会导致隐式提交。例如,执行 `[BEGIN|START TRANCATION]` 语句时,TiDB 会试图提交上一个事务,并开启一个新的事务。详情参见 [implicit commit](https://dev.mysql.com/doc/refman/8.0/en/implicit-commit.html)。 另外,`autocommit` 也是一个系统变量,你可以通过变量赋值语句修改当前 Session 或 Global 的值。 @@ -93,7 +92,7 @@ SET @@GLOBAL.autocommit = {0 | 1}; ## 显式事务和隐式事务 -TiDB 可以显式地使用事务 (`[BEGIN|START TRANSACTION]`/`COMMIT`) 或者隐式地使用事务 (`SET autocommit = 1`)。 +TiDB 可以显式地使用事务(通过 `[BEGIN|START TRANSACTION]`/`COMMIT` 语句定义事务的开始和结束) 或者隐式地使用事务 (`SET autocommit = 1`)。 在自动提交状态下,使用 `[BEGIN|START TRANSACTION]` 语句会显式地开启一个事务,同时也会禁用自动提交,使隐式事务变成显式事务。直到执行 `COMMIT` 或 `ROLLBACK` 语句时才会恢复到此前默认的自动提交状态。 @@ -109,7 +108,7 @@ TiDB **只支持** `SNAPSHOT ISOLATION`,可以通过下面的语句将当前 S SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; ``` -## 事务的惰性检查 +## 惰性检查 TiDB 中,对于普通的 `INSERT` 语句写入的值,会进行惰性检查。惰性检查的含义是,不在 `INSERT` 语句执行时进行唯一约束的检查,而在事务提交时进行唯一约束的检查。 @@ -159,4 +158,57 @@ insert into test values (3); rollback; ``` -以上例子中,第二条语句执行失败。由于调用了 `rollback`,因此事务不会将任何数据写入数据库。 +以上例子中,第二条语句执行失败。由于调用了 `ROLLBACK`,因此事务不会将任何数据写入数据库。 + +## 事务大小 + +对于 TiDB 事务而言,事务太大或太小,都会影响事务的执行效率。 + +### 小事务 + +以如下 query 为例,当 `autocommit = 1` 时,下面三条语句各为一个事务: + +{{< copyable "sql" >}} + +```sql +UPDATE my_table SET a ='new_value' WHERE id = 1; +UPDATE my_table SET a ='newer_value' WHERE id = 2; +UPDATE my_table SET a ='newest_value' WHERE id = 3; +``` + +此时每一条语句都需要经过两阶段提交,频繁的网络交互致使延迟率高。为提升事务执行效率,可以选择使用显式事务,即在一个事务内执行三条语句。 + +优化后版本: + +{{< copyable "sql" >}} + +```sql +START TRANSACTION; +UPDATE my_table SET a ='new_value' WHERE id = 1; +UPDATE my_table SET a ='newer_value' WHERE id = 2; +UPDATE my_table SET a ='newest_value' WHERE id = 3; +COMMIT; +``` + +同理,执行 `INSERT` 语句时,建议使用显式事务。 + +> **注意:** +> +> 由于 TiDB 中的资源是分布式的,TiDB 中单线程 workload 可能不会很好地利用分布式资源,因此性能相比于单实例部署的 MySQL 较低。这与 TiDB 中的事务延迟较高的情況类似。 + +### 大事务 + +由于 TiDB 两阶段提交的要求,修改数据的单个事务过大时会存在以下问题: + +* 客户端在提交之前,数据都写在内存中,而数据量过多时易导致 OOM (Out of Memory) 错误。 +* 在第一阶段写入数据耗时增加,与其他事务出现写冲突的概率会指数级增长。 +* 最终导致事务完成提交的耗时增加。 + +因此,TiDB 对事务做了一些限制: + +* 单个事务包含的 SQL 语句不超过 5000 条(默认) +* 每个键值对不超过 6 MB +* 键值对的总数不超过 300000 +* 键值对的总大小不超过 100 MB + +为了使性能达到最优,建议每 100~500 行写入一个事务。 \ No newline at end of file diff --git a/v2.1/reference/transactions/transaction-isolation.md b/v2.1/reference/transactions/transaction-isolation.md index 85bcffbdc6f5..6f291c5bc07a 100644 --- a/v2.1/reference/transactions/transaction-isolation.md +++ b/v2.1/reference/transactions/transaction-isolation.md @@ -1,13 +1,14 @@ --- title: TiDB 事务隔离级别 +summary: 了解 TiDB 事务的隔离级别。 category: reference --- # TiDB 事务隔离级别 -事务隔离级别是数据库事务处理的基础,ACID 中 I,即 Isolation,指的就是事务的隔离性。 +事务隔离级别是数据库事务处理的基础,[ACID](/v2.1/glossary.md#acid) 中的 “I”,即 Isolation,指的就是事务的隔离性。 -SQL 92 标准定义了 4 种隔离级别:读未提交、读已提交、可重复读、串行化。详见下表: +SQL-92 标准定义了 4 种隔离级别:读未提交 (READ UNCOMMITTED)、读已提交 (READ COMMITTED)、可重复读 (REPEATABLE READ)、串行化 (SERIALIZABLE)。详见下表: | Isolation Level | Dirty Write | Dirty Read | Fuzzy Read | Phantom | | ---------------- | ------------ | ------------ | ------------ | ------------ | @@ -16,13 +17,11 @@ SQL 92 标准定义了 4 种隔离级别:读未提交、读已提交、可重 | REPEATABLE READ | Not Possible | Not possible | Not possible | Possible | | SERIALIZABLE | Not Possible | Not possible | Not possible | Not possible | -TiDB 实现了快照隔离 (Snapshot Isolation) 级别的一致性。为与 MySQL 保持一致,又称其为“可重复读”。该隔离级别不同于 [ANSI 可重复读隔离级别](#与-ansi-可重复读隔离级别的区别)和 [MySQL 可重复读隔离级别](#与-mysql-可重复读隔离级别的区别)。 +TiDB 实现了快照隔离 (Snapshot Isolation, SI) 级别的一致性。为与 MySQL 保持一致,又称其为“可重复读”。该隔离级别不同于 [ANSI 可重复读隔离级别](#与-ansi-可重复读隔离级别的区别)和 [MySQL 可重复读隔离级别](#与-mysql-可重复读隔离级别的区别)。 > **注意:** > -> 在 2.1 默认设置中,事务的自动重试功能默认开启。关于该项功能对隔离级别的影响以及如何开启该项功能,请参考[事务自动重试及带来的异常](#事务自动重试及带来的异常)。 - -TiDB 使用 [Percolator 事务模型](https://research.google.com/pubs/pub36726.html),当事务启动时会获取全局读时间戳,事务提交时也会获取全局提交时间戳,并以此确定事务的执行顺序,如果想了解 TiDB 事务模型的实现可以详细阅读以下两篇文章:[TiKV 的 MVCC (Multi-Version Concurrency Control) 机制](https://pingcap.com/blog-cn/mvcc-in-tikv/),[Percolator 和 TiDB 事务算法](https://pingcap.com/blog-cn/percolator-and-txn/)。 +> 在 TiDB 2.1 中,事务的自动重试功能默认开启。关于该项功能对隔离级别的影响以及如何开启该项功能,请参考[事务重试](/v2.1/reference/transactions/transaction-optimistic.md#重试机制)。 ## 可重复读 @@ -45,56 +44,12 @@ commit; | ### 与 ANSI 可重复读隔离级别的区别 -尽管名称是可重复读隔离级别,但是 TiDB 中可重复读隔离级别和 ANSI 可重复隔离级别是不同的。按照 [A Critique of ANSI SQL Isolation Levels](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/tr-95-51.pdf) 论文中的标准,TiDB 实现的是论文中的 Snapshot 隔离级别 (SI)。该隔离级别不会出现狭义上的幻读 (A3),但不会阻止广义上的幻读 (P3),同时,SI 还会出现写偏斜,而 ANSI 可重复读隔离级别不会出现写偏斜,会出现幻读。 +尽管名称是可重复读隔离级别,但是 TiDB 中可重复读隔离级别和 ANSI 可重复隔离级别是不同的。按照 [A Critique of ANSI SQL Isolation Levels](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/tr-95-51.pdf) 论文中的标准,TiDB 实现的是论文中的快照隔离级别。该隔离级别不会出现狭义上的幻读 (A3),但不会阻止广义上的幻读 (P3),同时,SI 还会出现写偏斜,而 ANSI 可重复读隔离级别不会出现写偏斜,会出现幻读。 ### 与 MySQL 可重复读隔离级别的区别 -MySQL 可重复读隔离级别在更新时并不检验当前版本是否可见,也就是说,即使该行在事务启动后被更新过,同样可以继续更新。这种情况在 TiDB 会导致事务回滚,导致事务最终失败,而 MySQL 是可以更新成功的。MySQL 的可重复读隔离级别并非 Snapshot 隔离级别,MySQL 可重复读隔离级别的一致性要弱于 Snapshot 隔离级别,也弱于 TiDB 的可重复读隔离级别。 - -## 事务自动重试及带来的异常 - -TiDB 默认进行事务自动重试,重试事务可能会导致更新丢失,从而破坏快照隔离。如果业务可以容忍事务重试导致的异常,或并不关注事务是否以快照隔离级别来执行,则可以开启自动重试。通过设置 `tidb_disable_txn_auto_retry = off` 可开启该项功能。需注意 `tidb_retry_limit` 的值不能为 `0`,否则会禁用自动重试。开启自动重试以后,事务遇到提交出错的可能性会降低。 - -开启自动重试后,显式事务遇到冲突可能会导致最终结果不符合预期。 - -比如下面这两个例子: - -| Session1 | Session2 | -| ---------------- | ------------ | -| `begin;` | `begin;` | -| `select balance from t where id = 1;` | `update t set balance = balance -100 where id = 1;` | -| | `update t set balance = balance -100 where id = 2;` | -| // 使用 select 的结果决定后续的逻辑 | `commit;` | -| `if balance > 100 {` | | -| `update t set balance = balance + 100 where id = 2;` | | -| `}` | | -| `commit;` // 自动重试 | | - -| Session1 | Session2 | -| ---------------- | ------------ | -| `begin;` | `begin;` | -| `update t set balance = balance - 100 where id = 1;` | `delete from t where id = 1;` | -| | `commit;` | -| // 使用 affected_rows 的结果决定后续的逻辑 | | -| `if affected_rows > 0 {` | | -| `update t set balance = balance + 100 where id = 2;` | | -| `}` | | -| `commit;` // 自动重试 | | - -因为 TiDB 自动重试机制会把事务第一次执行的所有语句重新执行一遍,当一个事务里的后续语句是否执行取决于前面语句执行结果的时候,自动重试会违反快照隔离,导致更新丢失。这种情况下,需要在应用层重试整个事务。 - -通过配置 `tidb_disable_txn_auto_retry = on` 变量可以关掉显示事务的重试。 - -{{< copyable "sql" >}} - -```sql -SET GLOBAL tidb_disable_txn_auto_retry = on; -``` - -改变 `tidb_disable_txn_auto_retry` 变量不会影响 `autocommit = 1` 的单语句的隐式事务,因为该语句的自动重试,不会造成丢失更新等异常,即不会破坏事务的隔离性。 - -关掉显式事务重试后,如果出现事务冲突,commit 语句会返回错误,错误信息会包含 `try again later` 这个字符串,应用层可以用来判断遇到的错误是否是可以重试的。 +MySQL 可重复读隔离级别在更新时并不检验当前版本是否可见,也就是说,即使该行在事务启动后被更新过,同样可以继续更新。这种情况在 TiDB 会导致事务回滚,导致事务最终失败,而 MySQL 是可以更新成功的。MySQL 的可重复读隔离级别并非快照隔离级别,MySQL 可重复读隔离级别的一致性要弱于快照隔离级别,也弱于 TiDB 的可重复读隔离级别。 -如果事务执行过程中包含了应用层的逻辑,建议在应用层添加显式事务的重试,并关闭自动重试。 +## 更多阅读 -`tidb_retry_limit` 变量决定了事务重试的最大次数,默认值为 10,当它被设置为 0 时,所有事务都不会自动重试,包括自动提交的单语句隐式事务。这是彻底禁用 TiDB 中自动重试机制的方法。当用户相比于事务隔离性,更关心事务执行的延迟时,可以将它设置为 0,所有冲突的事务都会以最快的方式上报失败给应用层。 +- [TiKV 的 MVCC (Multi-Version Concurrency Control) 机制](https://pingcap.com/blog-cn/mvcc-in-tikv/) \ No newline at end of file diff --git a/v2.1/reference/transactions/transaction-model.md b/v2.1/reference/transactions/transaction-model.md deleted file mode 100644 index 5b72a91d5718..000000000000 --- a/v2.1/reference/transactions/transaction-model.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -title: 事务模型 -category: reference ---- - -# 事务模型 - -TiDB 默认使用乐观事务模型。也就是说,在执行 `UPDATE`、`INSERT`、`DELETE` 等语句时,只有在提交过程中才会检查写写冲突,而不是像 MySQL 一样使用行锁来避免写写冲突。类似的,诸如 `GET_LOCK()` 和 `RELEASE_LOCK()` 等函数以及 `SELECT .. FOR UPDATE` 之类的语句在 TiDB 和 MySQL 中的执行方式并不相同。所以业务端在执行 SQL 语句后,需要注意检查 `COMMIT` 的返回值,即使执行时没有出错,`COMMIT` 的时候也可能会出错。 - -## 事务限制 - -由于 TiDB 分布式两阶段提交的要求,修改数据的大事务可能会出现一些问题。因此,TiDB 特意对事务大小设置了一些限制以减少这种影响: - -* 单个事务包含的 SQL 语句不超过 5000 条(默认) -* 每个键值对不超过 6MB -* 键值对的总大小不超过 100MB - -## 基于事务模型的优化实践 - -由于 TiDB 中的每个事务都需要跟 PD leader 进行两次 round trip,TiDB 中的事务相比于 MySQL 中的事务延迟更高。以如下的 query 为例,用显式事务代替 `autocommit`,可优化该 query 的性能。 - -使用 `autocommit` 的原始版本: - -{{< copyable "sql" >}} - -```sql -UPDATE my_table SET a='new_value' WHERE id = 1; -UPDATE my_table SET a='newer_value' WHERE id = 2; -UPDATE my_table SET a='newest_value' WHERE id = 3; -``` - -优化后的版本: - -{{< copyable "sql" >}} - -```sql -START TRANSACTION; -UPDATE my_table SET a='new_value' WHERE id = 1; -UPDATE my_table SET a='newer_value' WHERE id = 2; -UPDATE my_table SET a='newest_value' WHERE id = 3; -COMMIT; -``` - -> **注意:** -> -> 由于 TiDB 中的资源是分布式的,TiDB 中单线程 workload 可能不会很好地利用分布式资源,因此性能相比于单实例部署的 MySQL 较低。这与 TiDB 中的事务延迟较高的情況类似。 diff --git a/v2.1/reference/transactions/transaction-optimistic.md b/v2.1/reference/transactions/transaction-optimistic.md new file mode 100644 index 000000000000..92f25c752e4d --- /dev/null +++ b/v2.1/reference/transactions/transaction-optimistic.md @@ -0,0 +1,174 @@ +--- +title: TiDB 乐观事务模型 +summary: 了解 TiDB 的乐观事务模型。 +category: reference +aliases: ['/docs-cn/v2.1/reference/transactions/transaction-model/'] +--- + +# TiDB 乐观事务模型 + +本文介绍 TiDB 乐观事务的原理,以及相关特性。本文假定你对 [TiDB 的整体架构](/v2.1/architecture.md#tidb-整体架构)、[Percolator](https://www.usenix.org/legacy/event/osdi10/tech/full_papers/Peng.pdf) 事务模型以及事务的 [ACID 特性](/v2.1/glossary.md#acid)都有一定了解。 + +TiDB 默认使用乐观事务模型,不会出现读写冲突,所有的读操作都不会被写操作阻塞。对于写写冲突,只有在客户端执行 `COMMIT` 时,才会触发两阶段提交并检测是否存在写写冲突。 + +## 乐观事务原理 + +TiDB 中事务使用两阶段提交,流程如下: + +![TiDB 中的两阶段提交](/media/2pc-in-tidb.png) + +1. 客户端开始一个事务。 + + TiDB 从 PD 获取一个全局唯一递增的版本号作为当前事务的开始版本号,这里定义为该事务的 `start_ts` 版本。 + +2. 客户端发起读请求。 + + 1. TiDB 从 PD 获取数据路由信息,即数据具体存在哪个 TiKV 节点上。 + 2. TiDB 从 TiKV 获取 `start_ts` 版本下对应的数据信息。 + +3. 客户端发起写请求。 + + TiDB 校验写入数据是否符合一致性约束(如数据类型是否正确、是否符合唯一索引约束等)。**校验通过的数据将存放在内存里。** + +4. 客户端发起 commit。 + +5. TiDB 开始两阶段提交,保证分布式事务的原子性,让数据真正落盘。 + + 1. TiDB 从当前要写入的数据中选择一个 Key 作为当前事务的 Primary Key。 + 2. TiDB 从 PD 获取所有数据的写入路由信息,并将所有的 Key 按照所有的路由进行分类。 + 3. TiDB 并发地向所有涉及的 TiKV 发起 prewrite 请求。TiKV 收到 prewrite 数据后,检查数据版本信息是否存在冲突或已过期。符合条件的数据会被加锁。 + 4. TiDB 收到所有 prewrite 响应且所有 prewrite 都成功。 + 5. TiDB 向 PD 获取第二个全局唯一递增版本号,定义为本次事务的 `commit_ts`。 + 6. TiDB 向 Primary Key 所在 TiKV 发起第二阶段提交。TiKV 收到 commit 操作后,检查数据合法性,清理 prewrite 阶段留下的锁。 + 7. TiDB 收到两阶段提交成功的信息。 + +6. TiDB 向客户端返回事务提交成功的信息。 + +7. TiDB 异步清理本次事务遗留的锁信息。 + +## 优缺点分析 + +通过分析 TiDB 中事务的处理流程,可以发现 TiDB 事务有如下优点: + +* 实现原理简单,易于理解。 +* 基于单实例事务实现了跨节点事务。 +* 锁管理实现了去中心化。 + +但 TiDB 事务也存在以下缺点: + +* 两阶段提交使网络交互增多。 +* 需要一个中心化的版本管理服务。 +* 事务数据量过大时易导致内存暴涨。 + +实际应用中,你可以[根据事务的大小进行针对性处理](/v2.1/reference/transactions/overview.md#事务大小),以提高事务的执行效率。 + +## 事务的重试 + +使用乐观事务模型时,在高冲突率的场景中,事务很容易提交失败。而 MySQL 内部使用的是悲观事务模型,在执行 SQL 语句的过程中进行冲突检测,所以提交时很难出现异常。为了兼容 MySQL 的悲观事务行为,TiDB 提供了重试机制。 + +### 重试机制 + +当事务提交后,如果发现冲突,TiDB 内部重新执行包含写操作的 SQL 语句。你可以通过设置 `tidb_disable_txn_auto_retry = off` 开启自动重试,并通过 `tidb_retry_limit` 设置重试次数: + +```sql +# 设置是否禁用自动重试,默认为 “on”,即不重试。 +tidb_disable_txn_auto_retry = off +# 控制重试次数,默认为 “10”。只有自动重试启用时该参数才会生效。 +# 当 “tidb_retry_limit= 0” 时,也会禁用自动重试。 +tidb_retry_limit = 10 +``` + +你也可以修改当前 Session 或 Global 的值: + +- Session 级别设置: + + {{< copyable "sql" >}} + + ```sql + set @@tidb_disable_txn_auto_retry = off; + ``` + + {{< copyable "sql" >}} + + ```sql + set @@tidb_retry_limit = 10; + ``` + +- Global 级别设置: + + {{< copyable "sql" >}} + + ```sql + set @@global.tidb_disable_txn_auto_retry = off; + ``` + + {{< copyable "sql" >}} + + ```sql + set @@global.tidb_retry_limit = 10; + ``` + +> **注意:** +> +> `tidb_retry_limit` 变量决定了事务重试的最大次数。当它被设置为 0 时,所有事务都不会自动重试,包括自动提交的单语句隐式事务。这是彻底禁用 TiDB 中自动重试机制的方法。禁用自动重试后,所有冲突的事务都会以最快的方式上报失败信息 (`try again later`) 给应用层。 + +### 重试的局限性 + +TiDB 默认不进行事务重试,因为重试事务可能会导致更新丢失,从而破坏[可重复读的隔离级别](/v2.1/reference/transactions/transaction-isolation.md)。 + +事务重试的局限性与其原理有关。事务重试可概括为以下三个步骤: + +1. 重新获取 `start_ts`。 +2. 重新执行包含写操作的 SQL 语句。 +3. 再次进行两阶段提交。 + +第二步中,重试时仅重新执行包含写操作的 SQL 语句,并不涉及读操作的 SQL 语句。但是当前事务中读到数据的时间与事务真正开始的时间发生了变化,写入的版本变成了重试时获取的 `start_ts` 而非事务一开始时获取的 `start_ts`。因此,当事务中存在依赖查询结果来更新的语句时,重试将无法保证事务原本可重复读的隔离级别,最终可能导致结果与预期出现不一致。 + +如果业务可以容忍事务重试导致的异常,或并不关注事务是否以可重复读的隔离级别来执行,则可以开启自动重试。 + +## 冲突检测 + +乐观事务下,检测底层数据是否存在写写冲突是一个很重要的操作。具体而言,TiKV 在 prewrite 阶段就需要读取数据进行检测。为了优化这一块性能,TiDB 集群会在内存里面进行一次冲突预检测。 + +作为一个分布式系统,TiDB 在内存中的冲突检测主要在两个模块进行: + +- TiDB 层。如果发现 TiDB 实例本身就存在写写冲突,那么第一个写入发出后,后面的写入已经清楚地知道自己冲突了,无需再往下层 TiKV 发送请求去检测冲突。 +- TiKV 层。主要发生在 prewrite 阶段。因为 TiDB 集群是一个分布式系统,TiDB 实例本身无状态,实例之间无法感知到彼此的存在,也就无法确认自己的写入与别的 TiDB 实例是否存在冲突,所以会在 TiKV 这一层检测具体的数据是否有冲突。 + +其中 TiDB 层的冲突检测可以根据场景需要选择打开或关闭,具体配置项如下: + +```toml +# 事务内存锁相关配置,当本地事务冲突比较多时建议开启。 +[txn-local-latches] +# 是否开启内存锁,默认为 false,即不开启。 +enabled = false +# Hash 对应的 slot 数,会自动向上调整为 2 的指数倍。 +# 每个 slot 占 32 Bytes 内存。当写入数据的范围比较广时(如导数据), +# 设置过小会导致变慢,性能下降。(默认为 2048000) +capacity = 2048000 +``` + +配置项 `capacity` 主要影响到冲突判断的正确性。在实现冲突检测时,不可能把所有的 Key 都存到内存里,所以真正存下来的是每个 Key 的 Hash 值。有 Hash 算法就有碰撞也就是误判的概率,这里可以通过配置 `capacity` 来控制 Hash 取模的值: + +* `capacity` 值越小,占用内存小,误判概率越大。 +* `capacity` 值越大,占用内存大,误判概率越小。 + +实际应用时,如果业务场景能够预判断写入不存在冲突(如导入数据操作),建议关闭冲突检测。 + +相应地,在 TiKV 层检测内存中是否存在冲突也有类似的机制。不同的是,TiKV 层的检测会更严格且不允许关闭,仅支持对 Hash 取模值进行配置: + +```toml +# scheduler 内置一个内存锁机制,防止同时对一个 Key 进行操作。 +# 每个 Key hash 到不同的 slot。(默认为 2048000) +scheduler-concurrency = 2048000 +``` + +此外,TiKV 支持监控等待 latch 的时间: + +![Scheduler latch wait duration](/media/optimistic-transaction-metric.png) + +当 `Scheduler latch wait duration` 的值特别高时,说明大量时间消耗在等待锁的请求上。如果不存在底层写入慢的问题,基本上可以判断该段时间内冲突比较多。 + +## 更多阅读 + +- [Percolator 和 TiDB 事务算法](https://pingcap.com/blog-cn/percolator-and-txn/) \ No newline at end of file diff --git a/v3.0/TOC.md b/v3.0/TOC.md index 441093df0161..526b0650d0dd 100644 --- a/v3.0/TOC.md +++ b/v3.0/TOC.md @@ -233,9 +233,9 @@ - [基于角色的访问控制](/v3.0/reference/security/role-based-access-control.md) - [TiDB 证书鉴权使用指南](/v3.0/reference/security/cert-based-authentication.md) + 事务 - - [事务语句](/v3.0/reference/transactions/overview.md) - - [事务模型](/v3.0/reference/transactions/transaction-model.md) + - [事务概览](/v3.0/reference/transactions/overview.md) - [隔离级别](/v3.0/reference/transactions/transaction-isolation.md) + - [乐观事务](/v3.0/reference/transactions/transaction-optimistic.md) - [悲观事务](/v3.0/reference/transactions/transaction-pessimistic.md) + 系统数据库 - [`mysql`](/v3.0/reference/system-databases/mysql.md) @@ -268,7 +268,6 @@ - [Grafana 监控最佳实践](/v3.0/reference/best-practices/grafana-monitor.md) - [PD 调度策略最佳实践](/v3.0/reference/best-practices/pd-scheduling.md) - [海量 Region 集群调优最佳实践](/v3.0/reference/best-practices/massive-regions.md) - - [乐观锁事务最佳实践](/v3.0/reference/best-practices/optimistic-transaction.md) + [TiSpark 使用指南](/v3.0/reference/tispark.md) + TiDB Binlog - [概述](/v3.0/reference/tidb-binlog/overview.md) diff --git a/v3.0/glossary.md b/v3.0/glossary.md index dc91ff18aa8b..3b31e69e5d20 100644 --- a/v3.0/glossary.md +++ b/v3.0/glossary.md @@ -1,6 +1,6 @@ --- title: 术语表 -summary: 学习 TiDB 相关术语。 +summary: 了解 TiDB 相关术语。 category: glossary --- @@ -13,29 +13,16 @@ category: glossary ACID 是指数据库管理系统在写入或更新资料的过程中,为保证[事务](#事务)是正确可靠的,所必须具备的四个特性:原子性 (atomicity)、一致性 (consistency)、隔离性(isolation)以及持久性(durability)。 * 原子性 (atomicity) 指一个事务中的所有操作,或者全部完成,或者全部不完成,不会结束在中间某个环节。TiDB 通过 Primary Key 所在 [Region](#regionpeerraft-group) 的原子性来保证分布式事务的原子性。 - * 一致性 (consistency) 指在事务开始之前和结束以后,数据库的完整性没有被破坏。TiDB 在写入数据之前,会校验数据的一致性,校验通过才会写入内存并返回成功。 - * 隔离性 (isolation) 指数据库允许多个并发事务同时对其数据进行读写和修改的能力。隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致,主要用于处理并发场景。TiDB 目前只支持一种隔离级别,即可重复读。 - * 持久性 (durability) 指事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。在 TiDB 中,事务一旦提交成功,数据全部持久化存储到 TiKV,此时即使 TiDB 服务器宕机也不会出现数据丢失。 -## B - -### 悲观事务 - -悲观事务假定并发事务会发生冲突,所以每一条 SQL 语句执行后都会检测冲突,只有在确保事务一定能够执行成功后,才开始提交。另有[乐观事务](#乐观事务)。 - ## L ### Leader/Follower/Learner 它们分别对应 [Peer](#regionpeerraft-group) 的三种角色。其中 Leader 负责响应客户端的读写请求;Follower 被动地从 Leader 同步数据,当 Leader 失效时会进行选举产生新的 Leader;Learner 是一种特殊的角色,它只参与同步 raft log 而不参与投票,在目前的实现中只短暂存在于添加副本的中间步骤。 -### 乐观事务 - -乐观事务假定不会发生并发冲突,只有在事务最终提交时才会检测冲突。另有[悲观事务](#悲观事务)。 - ## O ### Operator @@ -86,16 +73,6 @@ Scheduler(调度器)是 PD 中生成调度的组件。PD 中每个调度器 - `hot-region-scheduler`:保持不同节点的读写热点 Region 均衡。 - `evict-leader-{store-id}`:驱逐某个节点的所有 Leader。(常用于滚动升级) -### 事务 - -事务指一系列有限的数据库操作序列。TiDB 中的事务具备 [ACID](#ACID) 四个特性。 - ### Store PD 中的 Store 指的是集群中的存储节点,也就是 tikv-server 实例。Store 与 TiKV 实例是严格一一对应的,即使在同一主机甚至同一块磁盘部署多个 TiKV 实例,这些实例也对会对应不同的 Store。 - -## X - -### 显式事务/隐式事务 - -由事务控制语句定义开始和结束的事务为显式事务。无需定义开始的事务为隐式事务。 \ No newline at end of file diff --git a/v3.0/reference/best-practices/optimistic-transaction.md b/v3.0/reference/best-practices/optimistic-transaction.md deleted file mode 100644 index 8d88ac025ebb..000000000000 --- a/v3.0/reference/best-practices/optimistic-transaction.md +++ /dev/null @@ -1,264 +0,0 @@ ---- -title: 乐观锁事务最佳实践 -summary: 了解 TiDB 的乐观事务模型。 -category: reference ---- - -# 乐观锁事务最佳实践 - -本文介绍 TiDB 乐观锁机制的实现原理,并通过分析乐观锁在多种场景下的应用为业务提供最佳实践。本文假定你对 [TiDB 的整体架构](/v3.0/architecture.md#tidb-整体架构)和 [Percolator](https://www.usenix.org/legacy/event/osdi10/tech/full_papers/Peng.pdf) 事务模型都有一定了解,相关核心概念如下: - -- [ACID](/v3.0/glossary.md#acid) -- [事务](/v3.0/glossary.md#事务) -- [乐观事务](/v3.0/glossary.md#乐观事务) -- [悲观事务](/v3.0/glossary.md#悲观事务) -- [显式事务/隐式事务](/v3.0/glossary.md#显式事务隐式事务) - -## 乐观事务原理 - -TiDB 中事务使用两阶段提交,流程如下: - -![TiDB 中的两阶段提交](/media/best-practices/2pc-in-tidb.png) - -1. 客户端开始一个事务。 - - TiDB 从 PD 获取一个全局唯一递增的版本号作为当前事务的开始版本号,这里定义为该事务的 `start_ts` 版本。 - -2. 客户端发起读请求。 - - a. TiDB 从 PD 获取数据路由信息,即数据具体存在哪个 TiKV 节点上。 - - b. TiDB 从 TiKV 获取 `start_ts` 版本下对应的数据信息。 - -3. 客户端发起写请求。 - - TiDB 校验写入数据是否符合一致性约束(如数据类型是否正确、是否符合唯一索引约束等)。**校验通过的数据将存放在内存里。** - -4. 客户端发起 commit。 - -5. TiDB 开始两阶段提交,保证分布式事务的原子性,让数据真正落盘。 - - a. TiDB 从当前要写入的数据中选择一个 Key 作为当前事务的 Primary Key。 - - b. TiDB 从 PD 获取所有数据的写入路由信息,并将所有的 Key 按照所有的路由进行分类。 - - c. TiDB 并发地向所有涉及的 TiKV 发起 prewrite 请求。TiKV 收到 prewrite 数据后,检查数据版本信息是否存在冲突或已过期。符合条件的数据会被加锁。 - - d. TiDB 成功收到所有 prewrite 请求。 - - e. TiDB 向 PD 获取第二个全局唯一递增版本号,定义为本次事务的 `commit_ts`。 - - f. TiDB 向 Primary Key 所在 TiKV 发起第二阶段提交。TiKV 收到 commit 操作后,检查数据合法性,清理 prewrite 阶段留下的锁。 - - g. TiDB 收到 f 成功信息。 - -6. TiDB 向客户端返回事务提交成功的信息。 - -7. TiDB 异步清理本次事务遗留的锁信息。 - -## 优缺点分析 - -通过分析 TiDB 中事务的处理流程,可以发现 TiDB 事务有如下优点: - -* 实现原理简单,易于理解。 -* 基于单实例事务实现了跨节点事务。 -* 锁管理实现了去中心化。 - -但 TiDB 事务也存在以下缺点: - -* 两阶段提交使网络交互增多。 -* 缺少一个中心化的版本管理服务。 -* 事务数据量过大时易导致内存暴涨。 - -## 事务大小 - -对于 TiDB 乐观事务而言,事务太大或者太小,都会影响事务性能。为了克服上述事务在处理过程中的不足,在实际应用中可以根据事务大小进行针对性处理。 - -### 小事务 - -在自动提交状态 (`autocommit = 1`) 下,下面三条语句各为一个事务: - -```sql -# 使用自动提交的原始版本。 -UPDATE my_table SET a ='new_value' WHERE id = 1; -UPDATE my_table SET a ='newer_value' WHERE id = 2; -UPDATE my_table SET a ='newest_value' WHERE id = 3; -``` - -此时每一条语句都需要经过两阶段提交,频繁的网络交互致使小事务延迟率高。为提升事务执行效率,可以选择使用显式事务,即在一个事务内执行三条语句: - -```sql -# 优化后版本。 -START TRANSACTION; -UPDATE my_table SET a ='new_value' WHERE id = 1; -UPDATE my_table SET a ='newer_value' WHERE id = 2; -UPDATE my_table SET a ='newest_value' WHERE id = 3; -COMMIT; -``` - -同理,执行 `INSERT` 语句时,建议使用显式事务。 - -### 大事务 - -通过分析两阶段提交的过程,可以发现单个事务过大时会存在以下问题: - -* 客户端在提交之前,数据都写在内存中,而数据量过多时易导致 OOM (Out of Memory) 错误。 -* 在第一阶段写入数据时,与其他事务出现冲突的概率会指数级增长,使事务之间相互阻塞影响。 -* 最终导致事务完成提交的耗时增加。 - -因此,TiDB 特意对事务的大小做了一些限制: - -* 单个事务包含的 SQL 语句不超过 5000 条(默认) -* 每个键值对不超过 6 MB -* 键值对的总数不超过 300000 -* 键值对的总大小不超过 100 MB - -为了使性能达到最优,建议每 100~500 行写入一个事务。 - -## 事务冲突 - -事务的冲突,主要指事务并发执行时对相同的 Key 进行了读写操作。冲突主要有两种形式: - -* 读写冲突:部分事务进行读操作时,有事务在同一时间对相同的 Key 进行写操作。 -* 写写冲突:不同事务同时对相同的 Key 进行写操作。 - -在 TiDB 的乐观锁机制中,只有在客户端执行 `commit` 时,才会触发两阶段提交并检测是否存在写写冲突。也就是说,在乐观事务下,如果存在写写冲突,在事务提交阶段就会暴露出来,因而更容易被用户感知。 - -### 默认冲突行为 - -乐观事务下,默认在最终提交时才会进行冲突检测。当两个事务同时更新同一行数据,即并发事务存在冲突时,不同时间点的执行结果如下: - -![并发事务冲突流程](/media/best-practices/optimistic-transaction-table1.png) - -根据乐观锁检测写写冲突的设定,该实例的执行逻辑分析如下: - -![并发事务冲突逻辑](/media/best-practices/optimistic-transaction-case1.png) - -1. 如上图,事务 A 在时间点 `t1` 开始,事务 B 在 `t2` 开始。 - -2. 事务 A、事务 B 同时更新同一行数据。 - -3. `t4` 时,事务 A 更新 `id = 1` 的同一行数据。 虽然 `t3` 时,事务 B 已经更新了这一行数据,但是乐观事务只有在事务 commit 时才检测冲突,因此 `t4` 的操作执行成功了。 - -4. `t5` 时,事务 B 成功提交,数据落盘。 - -5. `t6` 时,事务 A 尝试提交,检测冲突时发现 `t1` 之后有新的数据写入,因此返回错误,提示客户端重试,事务 A 提交失败。 - -### 重试机制 - -TiDB 中默认使用乐观事务模型,因而在高冲突率的场景中,事务很容易提交失败。而 MySQL 内部使用的是悲观事务模型,对应到上面的实例中,事务 A 在 `t4` 时就会返回错误,提示客户端根据需求去重试。 - -换言之,MySQL 在执行 SQL 语句的过程中进行冲突检测,所以提交时很难出现异常。由于 TiDB 使用乐观锁机制造成了两边行为不一致,要兼容 MySQL 的悲观事务行为,需要在客户端修改大量的代码。为了便于广大 MySQL 用户使用,TiDB 提供了重试机制。当事务提交后,如果发现冲突,TiDB 内部重新执行包含写操作的 SQL 语句。你可以通过设置 `tidb_disable_txn_auto_retry` 和 `tidb_retry_limit` 开启自动重试: - -```toml -# 用于设置是否禁用自动重试,默认不重试。 -tidb_disable_txn_auto_retry = on -# 用来控制重试次数。只有自动重试启用时该参数才会生效。 -# 当 “tidb_retry_limit= 0” 时,也会禁用自动重试。 -tidb_retry_limit = 10 -``` - -推荐通过以下两种方式进行参数设置: - -1. Session 级别设置: - - {{< copyable "sql" >}} - - ```sql - set @@tidb_disable_txn_auto_retry = off; - set @@tidb_retry_limit = 10; - ``` - -2. Global 级别设置: - - {{< copyable "sql" >}} - - ```sql - set @@global.tidb_disable_txn_auto_retry = off; - set @@global.tidb_retry_limit = 10; - ``` - -### 重试的局限性 - -基于重试机制的原理,可将重试过程概括为以下三个步骤: - -1. 重新获取 `start_ts`。 - -2. 重新执行包含写操作的 SQL 语句。 - -3. 两阶段提交。 - -根据第二步,重试时仅重新执行包含写操作的 SQL 语句,并不涉及读操作的 SQL 语句。这会引发以下问题: - -1. `start_ts` 发生了变更。当前事务中,读到数据的时间与事务真正开始的时间发生了变化。同理,写入的版本变成了重试时获取的 `start_ts` 而非事务一开始时获取的 `start_ts`。 - -2. 当前事务中,如果存在依赖查询结果来更新的语句,结果将变得不可控。 - -以下实例来具体说明了重试的局限性。开启自动重试后,当同时更新同一行数据时,Session A 和 Session B 在不同时间点的执行结果如下: - -![自动重试流程](/media/best-practices/optimistic-transaction-table2.png) - -该实例的执行逻辑分析如下: - -![自动重试逻辑](/media/best-practices/optimistic-transaction-case2.png) - -1. 如图,Session B 在 `t2` 时开始事务 2,`t5` 时提交成功。Session A 的事务 1 在事务 2 之前开始,在事务 2 提交完成后提交。 - -2. 事务 1、事务 2 同时更新同一行数据。 - -3. Session A 提交事务 1 时发现冲突,TiDB 内部重试事务 1。 - 1. 重新取得新的 `start_ts` 为 `t8’`。 - 2. 重新执行更新语句 `update tidb set name='pd' where id =1 and status=1`。 - 1. 发现当前版本 `t8’` 下并不存在符合条件的语句,不需要更新。 - 2. 没有数据更新,返回上层成功。 - -4. TiDB 认为事务 1 重试成功,返回客户端成功。 - -5. Session A 认为事务执行成功。如果在不存在其他更新,此时查询结果会发现数据与预想的不一致。 - -由上述分析可知,对于重试事务,当事务中更新语句需要依赖查询结果时,会重新取版本号作为 `start_ts`,所以无法保证事务原本可重复读的隔离级别,最终可能导致结果与预期出现不一致。 - -因此,如果存在依赖查询结果来更新 SQL 语句的事务,建议不要打开 TiDB 乐观锁的重试机制。 - -### 冲突预检 - -由上文可以知道,检测底层数据是否存在写写冲突是一个很重要的操作。具体而言,TiKV 在 prewrite 阶段就需要读取数据进行检测。为了优化这一块性能,TiDB 集群会在内存里面进行一次冲突预检测。 - -作为一个分布式系统,TiDB 在内存中的冲突检测主要在两个模块进行: - -* TiDB 层。如果发现 TiDB 实例本身就存在写写冲突,那么第一个写入发出后,后面的写入已经清楚地知道自己冲突了,无需再往下层 TiKV 发送请求去检测冲突。 -* TiKV 层。主要发生在 prewrite 阶段。因为 TiDB 集群是一个分布式系统,TiDB 实例本身无状态,实例之间无法感知到彼此的存在,也就无法确认自己的写入与别的 TiDB 实例是否存在冲突,所以会在 TiKV 这一层检测具体的数据是否有冲突。 - -其中 TiDB 层的冲突检测可以选择关闭,具体配置项如下: - -```toml -# 事务内存锁相关配置,当本地事务冲突比较多时建议开启。 -[txn-local-latches] -# 是否开启内存锁,默认为关闭。 -enabled = false -# Hash 对应的 slot 数,会自动向上调整为 2 的指数倍。 -# 每个 slot 占 32 Bytes 内存。当写入数据的范围比较广时(如导数据), -# 设置过小会导致变慢,性能下降。(默认为 1024000) -capacity = 1024000 -``` - -配置项 `capacity` 主要影响到冲突判断的正确性。在实现冲突检测时,不可能把所有的 Key 都存到内存里,所以真正存下来的是每个 Key 的 Hash 值。有 Hash 算法就有碰撞也就是误判的概率,这里可以通过配置 `capacity` 来控制 Hash 取模的值: - -* `capacity` 值越小,占用内存小,误判概率越大。 -* `capacity` 值越大,占用内存大,误判概率越小。 - -实际应用时,如果业务场景能够预判断写入不存在冲突(如导入数据操作),建议关闭冲突检测。 - -相应地,在 TiKV 层检测内存中是否存在冲突也有类似的机制。不同的是,TiKV 层的检测会更严格且不允许关闭,仅支持对 Hash 取模值进行配置: - -```toml -# scheduler 内置一个内存锁机制,防止同时对一个 Key 进行操作。 -# 每个 Key hash 到不同的 slot。(默认为 2048000) -scheduler-concurrency = 2048000 -``` - -此外,TiKV 支持监控等待 latch 的时间: - -![Scheduler latch wait duration](/media/best-practices/optimistic-transaction-metric.png) - -当 `Scheduler latch wait duration` 的值特别高时,说明大量时间消耗在等待锁的请求上。如果不存在底层写入慢的问题,基本上可以判断该段时间内冲突比较多。 diff --git a/v3.0/reference/configuration/tidb-server/tidb-specific-variables.md b/v3.0/reference/configuration/tidb-server/tidb-specific-variables.md index 72ea5c33fd71..538f625e3938 100644 --- a/v3.0/reference/configuration/tidb-server/tidb-specific-variables.md +++ b/v3.0/reference/configuration/tidb-server/tidb-specific-variables.md @@ -313,7 +313,7 @@ set @@global.tidb_distsql_scan_concurrency = 10; 默认值:10 -这个变量用来设置最多可重试次数,即在一个事务执行中遇到可重试的错误(例如事务冲突、事务提交过慢或表结构变更)时,这个事务可以被重新执行,这个变量值表明最多可重试的次数。 +这个变量用来设置最大重试次数。一个事务执行中遇到可重试的错误(例如事务冲突、事务提交过慢或表结构变更)时,会根据该变量的设置进行重试。注意当 `tidb_retry_limit = 0` 时,也会禁用自动重试。 ### tidb_disable_txn_auto_retry @@ -327,7 +327,7 @@ set @@global.tidb_distsql_scan_concurrency = 10; 这个变量不会影响自动提交的隐式事务和 TiDB 内部执行的事务,它们依旧会根据 `tidb_retry_limit` 的值来决定最大重试次数。 -是否需要禁用自动重试,请参考[事务自动重试及带来的异常](/v3.0/reference/transactions/transaction-isolation.md#事务自动重试及带来的异常)。 +是否需要禁用自动重试,请参考[重试的局限性](/v3.0/reference/transactions/transaction-optimistic.md#重试的局限性)。 ### tidb_backoff_weight diff --git a/v3.0/reference/mysql-compatibility.md b/v3.0/reference/mysql-compatibility.md index 1bd4b14a8aeb..9076720e31ba 100644 --- a/v3.0/reference/mysql-compatibility.md +++ b/v3.0/reference/mysql-compatibility.md @@ -13,7 +13,7 @@ TiDB 支持 MySQL 传输协议及其绝大多数的语法。这意味着您现 > **注意:** > -> 本页内容仅涉及 MySQL 与 TiDB 的总体差异。关于[安全特性](/v3.0/reference/security/compatibility.md)及[事务模型](/v3.0/reference/transactions/transaction-model.md)的兼容信息请查看各自具体页面。 +> 本页内容仅涉及 MySQL 与 TiDB 的总体差异。关于[安全特性](/v3.0/reference/security/compatibility.md)、[悲观事务模型](/v3.0/reference/transactions/transaction-pessimistic.md#和-mysql-innodb-的差异)的兼容信息请查看各自具体页面。 ## 不支持的特性 diff --git a/v3.0/reference/sql/statements/load-data.md b/v3.0/reference/sql/statements/load-data.md index 7c47cd082672..ab249aa40db4 100644 --- a/v3.0/reference/sql/statements/load-data.md +++ b/v3.0/reference/sql/statements/load-data.md @@ -59,4 +59,4 @@ Records: 815264 Deleted: 0 Skipped: 0 Warnings: 0 ## 另请参阅 * [INSERT](/v3.0/reference/sql/statements/insert.md) -* [Transaction Model](/v3.0/reference/transactions/transaction-model.md) +* [乐观事务模型](/v3.0/reference/transactions/transaction-optimistic.md) diff --git a/v3.0/reference/sql/statements/select.md b/v3.0/reference/sql/statements/select.md index 5fdfc9dcad07..ddb41b9bfbbd 100644 --- a/v3.0/reference/sql/statements/select.md +++ b/v3.0/reference/sql/statements/select.md @@ -75,7 +75,7 @@ aliases: ['/docs-cn/sql/dml/'] |`HAVING where_condition` | Having 子句与 Where 子句作用类似,Having 子句可以让过滤 GroupBy 后的各种数据,Where 子句用于在聚合前过滤记录。| |`ORDER BY` | OrderBy 子句用于指定结果排序顺序,可以按照列、表达式或者是 `select_expr` 列表中某个位置的字段进行排序。| |`LIMIT` | Limit 子句用于限制结果条数。Limit 接受一个或两个数字参数,如果只有一个参数,那么表示返回数据的最大行数;如果是两个参数,那么第一个参数表示返回数据的第一行的偏移量(第一行数据的偏移量是 0),第二个参数指定返回数据的最大条目数。| -|`FOR UPDATE` | 对查询结果集所有行上锁(对于在查询条件内,但是不在结果集的行,将不会加锁,如事务启动后由其他事务写入的行),以监测其他事务对这些的并发修改。TiDB 使用[乐观事务模型](/v3.0/reference/transactions/transaction-model.md#事务模型)在语句执行期间不会检测锁,因此,不会像 PostgreSQL 之类的数据库一样,在当前事务结束前阻止其他事务执行 UPDATE、DELETE 和 SELECT FOR UPDATE。在事务的提交阶段 SELECT FOR UPDATE 读到的行,也会进行两阶段提交,因此,它们也可以参与事务冲突检测。如发生写入冲突,那么包含 SELECT FOR UPDATE 语句的事务会提交失败。如果没有冲突,事务将成功提交,当提交结束时,这些被加锁的行,会产生一个新版本,可以让其他尚未提交的事务,在将来提交时发现写入冲突。| +|`FOR UPDATE` | 对查询结果集所有行上锁(对于在查询条件内,但是不在结果集的行,将不会加锁,如事务启动后由其他事务写入的行),以监测其他事务对这些的并发修改。使用[乐观事务模型](/v3.0/reference/transactions/transaction-optimistic.md)时,语句执行期间不会检测锁,因此,不会像 PostgreSQL 之类的数据库一样,在当前事务结束前阻止其他事务执行 `UPDATE`、`DELETE` 和 `SELECT FOR UPDATE`。在事务的提交阶段 `SELECT FOR UPDATE` 读到的行,也会进行两阶段提交,因此,它们也可以参与事务冲突检测。如发生写入冲突,那么包含 `SELECT FOR UPDATE` 语句的事务会提交失败。如果没有冲突,事务将成功提交,当提交结束时,这些被加锁的行,会产生一个新版本,可以让其他尚未提交的事务,在将来提交时发现写入冲突。若使用悲观事务,则行为与其他数据库基本相同,不一致之处参考[和 MySQL InnoDB 的差异](/v3.0/reference/transactions/transaction-pessimistic.md#和-mysql-innodb-的差异)。 | |`LOCK IN SHARE MODE` | TiDB 出于兼容性解析这个语法,但是不做任何处理| ## 示例 diff --git a/v3.0/reference/transactions/overview.md b/v3.0/reference/transactions/overview.md index 91a795eae20f..d1020e90bb00 100644 --- a/v3.0/reference/transactions/overview.md +++ b/v3.0/reference/transactions/overview.md @@ -1,15 +1,16 @@ --- title: TiDB 事务概览 +summary: 了解 TiDB 中的事务。 category: reference --- # TiDB 事务概览 -TiDB 支持完整的分布式事务。本文主要介绍涉及到事务的语句、显式/隐式事务以及事务的隔离级别和惰性检查。 +TiDB 支持完整的分布式事务,提供[乐观事务](/v3.0/reference/transactions/transaction-optimistic.md)与[悲观事务](/v3.0/reference/transactions/transaction-pessimistic.md)(TiDB 3.0 中引入)两种事务模型。本文主要介绍涉及到事务的语句、显式/隐式事务、事务的隔离级别和惰性检查,以及事务大小的限制。 -常用的变量包括 `autocommit`、[`tidb_disable_txn_auto_retry`](/v3.0/reference/configuration/tidb-server/tidb-specific-variables.md#tidb_disable_txn_auto_retry) 以及 [`tidb_retry_limit`](/v3.0/reference/configuration/tidb-server/tidb-specific-variables.md#tidb_retry_limit)。 +常用的变量包括 [`autocommit`](#自动提交)、[`tidb_disable_txn_auto_retry`](/v3.0/reference/configuration/tidb-server/tidb-specific-variables.md#tidb_disable_txn_auto_retry) 以及 [`tidb_retry_limit`](/v3.0/reference/configuration/tidb-server/tidb-specific-variables.md#tidb_retry_limit)。 -## 事务常用语句 +## 常用事务语句 ### `BEGIN` 和 `START TRANSACTION` @@ -69,13 +70,11 @@ ROLLBACK; SET autocommit = {0 | 1} ``` -当 `autocommit = 1` 时(默认),当前的 Session 为自动提交状态。设置 `autocommit = 0` 时将更改当前 Session 为非自动提交状态。 - -自动提交状态下,每条语句运行后,TiDB 会自动将修改提交到数据库中。非自动提交状态下,通过执行 `COMMIT` 语句来手动提交事务。 +当 `autocommit = 1` 时(默认),当前的 Session 为自动提交状态,即每条语句运行后,TiDB 会自动将修改提交到数据库中。设置 `autocommit = 0` 时更改当前 Session 更改为非自动提交状态,通过执行 `COMMIT` 语句来手动提交事务。 > **注意:** > -> 某些语句执行后会导致隐式提交。例如,执行 `[BEGIN|START TRANCATION]` 语句的时候,TiDB 会试图提交上一个事务,并开启一个新的事务。详情参见 [implicit commit](https://dev.mysql.com/doc/refman/8.0/en/implicit-commit.html)。 +> 某些语句执行后会导致隐式提交。例如,执行 `[BEGIN|START TRANCATION]` 语句时,TiDB 会试图提交上一个事务,并开启一个新的事务。详情参见 [implicit commit](https://dev.mysql.com/doc/refman/8.0/en/implicit-commit.html)。 另外,`autocommit` 也是一个系统变量,你可以通过变量赋值语句修改当前 Session 或 Global 的值。 @@ -93,7 +92,7 @@ SET @@GLOBAL.autocommit = {0 | 1}; ## 显式事务和隐式事务 -TiDB 可以显式地使用事务 (`[BEGIN|START TRANSACTION]`/`COMMIT`) 或者隐式地使用事务 (`SET autocommit = 1`)。 +TiDB 可以显式地使用事务(通过 `[BEGIN|START TRANSACTION]`/`COMMIT` 语句定义事务的开始和结束) 或者隐式地使用事务 (`SET autocommit = 1`)。 在自动提交状态下,使用 `[BEGIN|START TRANSACTION]` 语句会显式地开启一个事务,同时也会禁用自动提交,使隐式事务变成显式事务。直到执行 `COMMIT` 或 `ROLLBACK` 语句时才会恢复到此前默认的自动提交状态。 @@ -109,7 +108,7 @@ TiDB **只支持** `SNAPSHOT ISOLATION`,可以通过下面的语句将当前 S SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; ``` -## 事务的惰性检查 +## 惰性检查 TiDB 中,对于普通的 `INSERT` 语句写入的值,会进行惰性检查。惰性检查的含义是,不在 `INSERT` 语句执行时进行唯一约束的检查,而在事务提交时进行唯一约束的检查。 @@ -159,4 +158,57 @@ insert into test values (3); rollback; ``` -以上例子中,第二条语句执行失败。由于调用了 `rollback`,因此事务不会将任何数据写入数据库。 +以上例子中,第二条语句执行失败。由于调用了 `ROLLBACK`,因此事务不会将任何数据写入数据库。 + +## 事务大小 + +对于 TiDB 事务而言,事务太大或太小,都会影响事务的执行效率。 + +### 小事务 + +以如下 query 为例,当 `autocommit = 1` 时,下面三条语句各为一个事务: + +{{< copyable "sql" >}} + +```sql +UPDATE my_table SET a ='new_value' WHERE id = 1; +UPDATE my_table SET a ='newer_value' WHERE id = 2; +UPDATE my_table SET a ='newest_value' WHERE id = 3; +``` + +此时每一条语句都需要经过两阶段提交,频繁的网络交互致使延迟率高。为提升事务执行效率,可以选择使用显式事务,即在一个事务内执行三条语句。 + +优化后版本: + +{{< copyable "sql" >}} + +```sql +START TRANSACTION; +UPDATE my_table SET a ='new_value' WHERE id = 1; +UPDATE my_table SET a ='newer_value' WHERE id = 2; +UPDATE my_table SET a ='newest_value' WHERE id = 3; +COMMIT; +``` + +同理,执行 `INSERT` 语句时,建议使用显式事务。 + +> **注意:** +> +> 由于 TiDB 中的资源是分布式的,TiDB 中单线程 workload 可能不会很好地利用分布式资源,因此性能相比于单实例部署的 MySQL 较低。这与 TiDB 中的事务延迟较高的情況类似。 + +### 大事务 + +由于 TiDB 两阶段提交的要求,修改数据的单个事务过大时会存在以下问题: + +* 客户端在提交之前,数据都写在内存中,而数据量过多时易导致 OOM (Out of Memory) 错误。 +* 在第一阶段写入数据耗时增加,与其他事务出现写冲突的概率会指数级增长。 +* 最终导致事务完成提交的耗时增加。 + +因此,TiDB 对事务做了一些限制: + +* 单个事务包含的 SQL 语句不超过 5000 条(默认) +* 每个键值对不超过 6 MB +* 键值对的总数不超过 300000 +* 键值对的总大小不超过 100 MB + +为了使性能达到最优,建议每 100~500 行写入一个事务。 \ No newline at end of file diff --git a/v3.0/reference/transactions/transaction-isolation.md b/v3.0/reference/transactions/transaction-isolation.md index 69a0fc863579..e2c4c2823eb6 100644 --- a/v3.0/reference/transactions/transaction-isolation.md +++ b/v3.0/reference/transactions/transaction-isolation.md @@ -1,13 +1,14 @@ --- title: TiDB 事务隔离级别 +summary: 了解 TiDB 事务的隔离级别。 category: reference --- # TiDB 事务隔离级别 -事务隔离级别是数据库事务处理的基础,ACID 中 I,即 Isolation,指的就是事务的隔离性。 +事务隔离级别是数据库事务处理的基础,[ACID](/v3.0/glossary.md#acid) 中的 “I”,即 Isolation,指的就是事务的隔离性。 -SQL 92 标准定义了 4 种隔离级别:读未提交、读已提交、可重复读、串行化。详见下表: +SQL-92 标准定义了 4 种隔离级别:读未提交 (READ UNCOMMITTED)、读已提交 (READ COMMITTED)、可重复读 (REPEATABLE READ)、串行化 (SERIALIZABLE)。详见下表: | Isolation Level | Dirty Write | Dirty Read | Fuzzy Read | Phantom | | ---------------- | ------------ | ------------ | ------------ | ------------ | @@ -16,13 +17,11 @@ SQL 92 标准定义了 4 种隔离级别:读未提交、读已提交、可重 | REPEATABLE READ | Not Possible | Not possible | Not possible | Possible | | SERIALIZABLE | Not Possible | Not possible | Not possible | Not possible | -TiDB 实现了快照隔离 (Snapshot Isolation) 级别的一致性。为与 MySQL 保持一致,又称其为“可重复读”。该隔离级别不同于 [ANSI 可重复读隔离级别](#与-ansi-可重复读隔离级别的区别)和 [MySQL 可重复读隔离级别](#与-mysql-可重复读隔离级别的区别)。 +TiDB 实现了快照隔离 (Snapshot Isolation, SI) 级别的一致性。为与 MySQL 保持一致,又称其为“可重复读”。该隔离级别不同于 [ANSI 可重复读隔离级别](#与-ansi-可重复读隔离级别的区别)和 [MySQL 可重复读隔离级别](#与-mysql-可重复读隔离级别的区别)。 > **注意:** > -> 在 TiDB v3.0 的默认设置中,事务的自动重试功能已经关闭。关于该项功能对隔离级别的影响以及如何开启该项功能,请参考[事务自动重试及带来的异常](#事务自动重试及带来的异常)。 - -TiDB 使用 [Percolator 事务模型](https://research.google.com/pubs/pub36726.html),当事务启动时会获取全局读时间戳,事务提交时也会获取全局提交时间戳,并以此确定事务的执行顺序,如果想了解 TiDB 事务模型的实现可以详细阅读以下两篇文章:[TiKV 的 MVCC (Multi-Version Concurrency Control) 机制](https://pingcap.com/blog-cn/mvcc-in-tikv/),[Percolator 和 TiDB 事务算法](https://pingcap.com/blog-cn/percolator-and-txn/)。 +> 在 TiDB v3.0 中,事务的自动重试功能默认为禁用状态。关于该项功能对隔离级别的影响以及如何开启该项功能,请参考[事务重试](/v3.0/reference/transactions/transaction-optimistic.md#重试机制)。 ## 可重复读 @@ -45,56 +44,12 @@ commit; | ### 与 ANSI 可重复读隔离级别的区别 -尽管名称是可重复读隔离级别,但是 TiDB 中可重复读隔离级别和 ANSI 可重复隔离级别是不同的。按照 [A Critique of ANSI SQL Isolation Levels](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/tr-95-51.pdf) 论文中的标准,TiDB 实现的是论文中的 Snapshot 隔离级别 (SI)。该隔离级别不会出现狭义上的幻读 (A3),但不会阻止广义上的幻读 (P3),同时,SI 还会出现写偏斜,而 ANSI 可重复读隔离级别不会出现写偏斜,会出现幻读。 +尽管名称是可重复读隔离级别,但是 TiDB 中可重复读隔离级别和 ANSI 可重复隔离级别是不同的。按照 [A Critique of ANSI SQL Isolation Levels](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/tr-95-51.pdf) 论文中的标准,TiDB 实现的是论文中的快照隔离级别。该隔离级别不会出现狭义上的幻读 (A3),但不会阻止广义上的幻读 (P3),同时,SI 还会出现写偏斜,而 ANSI 可重复读隔离级别不会出现写偏斜,会出现幻读。 ### 与 MySQL 可重复读隔离级别的区别 -MySQL 可重复读隔离级别在更新时并不检验当前版本是否可见,也就是说,即使该行在事务启动后被更新过,同样可以继续更新。这种情况在 TiDB 会导致事务回滚,导致事务最终失败,而 MySQL 是可以更新成功的。MySQL 的可重复读隔离级别并非 Snapshot 隔离级别,MySQL 可重复读隔离级别的一致性要弱于 Snapshot 隔离级别,也弱于 TiDB 的可重复读隔离级别。 - -## 事务自动重试及带来的异常 - -TiDB 默认不进行事务重试,因为重试事务可能会导致更新丢失,从而破坏快照隔离。如果业务可以容忍事务重试导致的异常,或并不关注事务是否以快照隔离级别来执行,则可以开启自动重试。通过设置 `tidb_disable_txn_auto_retry = off` 可开启该项功能。需注意 `tidb_retry_limit` 的值不能为 `0`,否则会禁用自动重试。开启自动重试以后,事务遇到提交出错的可能性会降低。 - -开启自动重试后,显式事务遇到冲突可能会导致最终结果不符合预期。 - -比如下面这两个例子: - -| Session1 | Session2 | -| ---------------- | ------------ | -| `begin;` | `begin;` | -| `select balance from t where id = 1;` | `update t set balance = balance -100 where id = 1;` | -| | `update t set balance = balance -100 where id = 2;` | -| // 使用 select 的结果决定后续的逻辑 | `commit;` | -| `if balance > 100 {` | | -| `update t set balance = balance + 100 where id = 2;` | | -| `}` | | -| `commit;` // 自动重试 | | - -| Session1 | Session2 | -| ---------------- | ------------ | -| `begin;` | `begin;` | -| `update t set balance = balance - 100 where id = 1;` | `delete from t where id = 1;` | -| | `commit;` | -| // 使用 affected_rows 的结果决定后续的逻辑 | | -| `if affected_rows > 0 {` | | -| `update t set balance = balance + 100 where id = 2;` | | -| `}` | | -| `commit;` // 自动重试 | | - -因为 TiDB 自动重试机制会把事务第一次执行的所有语句重新执行一遍,当一个事务里的后续语句是否执行取决于前面语句执行结果的时候,自动重试会违反快照隔离,导致更新丢失。这种情况下,需要在应用层重试整个事务。 - -通过配置 `tidb_disable_txn_auto_retry = on` 变量可以关掉显示事务的重试。 - -{{< copyable "sql" >}} - -```sql -SET GLOBAL tidb_disable_txn_auto_retry = on; -``` - -改变 `tidb_disable_txn_auto_retry` 变量不会影响 `autocommit = 1` 的单语句的隐式事务,因为该语句的自动重试,不会造成丢失更新等异常,即不会破坏事务的隔离性。 - -关掉显式事务重试后,如果出现事务冲突,commit 语句会返回错误,错误信息会包含 `try again later` 这个字符串,应用层可以用来判断遇到的错误是否是可以重试的。 +MySQL 可重复读隔离级别在更新时并不检验当前版本是否可见,也就是说,即使该行在事务启动后被更新过,同样可以继续更新。这种情况在 TiDB 会导致事务回滚,导致事务最终失败,而 MySQL 是可以更新成功的。MySQL 的可重复读隔离级别并非快照隔离级别,MySQL 可重复读隔离级别的一致性要弱于快照隔离级别,也弱于 TiDB 的可重复读隔离级别。 -如果事务执行过程中包含了应用层的逻辑,建议在应用层添加显式事务的重试,并关闭自动重试。 +## 更多阅读 -`tidb_retry_limit` 变量决定了事务重试的最大次数,默认值为 10,当它被设置为 0 时,所有事务都不会自动重试,包括自动提交的单语句隐式事务。这是彻底禁用 TiDB 中自动重试机制的方法。当用户相比于事务隔离性,更关心事务执行的延迟时,可以将它设置为 0,所有冲突的事务都会以最快的方式上报失败给应用层。 +- [TiKV 的 MVCC (Multi-Version Concurrency Control) 机制](https://pingcap.com/blog-cn/mvcc-in-tikv/) \ No newline at end of file diff --git a/v3.0/reference/transactions/transaction-model.md b/v3.0/reference/transactions/transaction-model.md deleted file mode 100644 index 5b72a91d5718..000000000000 --- a/v3.0/reference/transactions/transaction-model.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -title: 事务模型 -category: reference ---- - -# 事务模型 - -TiDB 默认使用乐观事务模型。也就是说,在执行 `UPDATE`、`INSERT`、`DELETE` 等语句时,只有在提交过程中才会检查写写冲突,而不是像 MySQL 一样使用行锁来避免写写冲突。类似的,诸如 `GET_LOCK()` 和 `RELEASE_LOCK()` 等函数以及 `SELECT .. FOR UPDATE` 之类的语句在 TiDB 和 MySQL 中的执行方式并不相同。所以业务端在执行 SQL 语句后,需要注意检查 `COMMIT` 的返回值,即使执行时没有出错,`COMMIT` 的时候也可能会出错。 - -## 事务限制 - -由于 TiDB 分布式两阶段提交的要求,修改数据的大事务可能会出现一些问题。因此,TiDB 特意对事务大小设置了一些限制以减少这种影响: - -* 单个事务包含的 SQL 语句不超过 5000 条(默认) -* 每个键值对不超过 6MB -* 键值对的总大小不超过 100MB - -## 基于事务模型的优化实践 - -由于 TiDB 中的每个事务都需要跟 PD leader 进行两次 round trip,TiDB 中的事务相比于 MySQL 中的事务延迟更高。以如下的 query 为例,用显式事务代替 `autocommit`,可优化该 query 的性能。 - -使用 `autocommit` 的原始版本: - -{{< copyable "sql" >}} - -```sql -UPDATE my_table SET a='new_value' WHERE id = 1; -UPDATE my_table SET a='newer_value' WHERE id = 2; -UPDATE my_table SET a='newest_value' WHERE id = 3; -``` - -优化后的版本: - -{{< copyable "sql" >}} - -```sql -START TRANSACTION; -UPDATE my_table SET a='new_value' WHERE id = 1; -UPDATE my_table SET a='newer_value' WHERE id = 2; -UPDATE my_table SET a='newest_value' WHERE id = 3; -COMMIT; -``` - -> **注意:** -> -> 由于 TiDB 中的资源是分布式的,TiDB 中单线程 workload 可能不会很好地利用分布式资源,因此性能相比于单实例部署的 MySQL 较低。这与 TiDB 中的事务延迟较高的情況类似。 diff --git a/v3.0/reference/transactions/transaction-optimistic.md b/v3.0/reference/transactions/transaction-optimistic.md new file mode 100644 index 000000000000..b1fe521b673a --- /dev/null +++ b/v3.0/reference/transactions/transaction-optimistic.md @@ -0,0 +1,178 @@ +--- +title: TiDB 乐观事务模型 +summary: 了解 TiDB 的乐观事务模型。 +category: reference +aliases: ['/docs-cn/v3.0/reference/transactions/transaction-model/'] +--- + +# TiDB 乐观事务模型 + +本文介绍 TiDB 乐观事务的原理,以及相关特性。本文假定你对 [TiDB 的整体架构](/v3.0/architecture.md#tidb-整体架构)、[Percolator](https://www.usenix.org/legacy/event/osdi10/tech/full_papers/Peng.pdf) 事务模型以及事务的 [ACID 特性](/v3.0/glossary.md#acid)都有一定了解。 + +TiDB 默认使用乐观事务模型,不会出现读写冲突,所有的读操作都不会被写操作阻塞。对于写写冲突,只有在客户端执行 `COMMIT` 时,才会触发两阶段提交并检测是否存在写写冲突。 + +> **注意:** +> +> 自 v3.0.8 开始,TiDB 默认使用[悲观事务模型](/v3.0/reference/transactions/transaction-pessimistic.md)。但如果从 3.0.7 及之前的版本升级到 >= 3.0.8 的版本,不会改变默认事务模型,即**只有新创建的集群才会默认使用悲观事务模型**。 + +## 乐观事务原理 + +TiDB 中事务使用两阶段提交,流程如下: + +![TiDB 中的两阶段提交](/media/2pc-in-tidb.png) + +1. 客户端开始一个事务。 + + TiDB 从 PD 获取一个全局唯一递增的版本号作为当前事务的开始版本号,这里定义为该事务的 `start_ts` 版本。 + +2. 客户端发起读请求。 + + 1. TiDB 从 PD 获取数据路由信息,即数据具体存在哪个 TiKV 节点上。 + 2. TiDB 从 TiKV 获取 `start_ts` 版本下对应的数据信息。 + +3. 客户端发起写请求。 + + TiDB 校验写入数据是否符合一致性约束(如数据类型是否正确、是否符合唯一索引约束等)。**校验通过的数据将存放在内存里。** + +4. 客户端发起 commit。 + +5. TiDB 开始两阶段提交,保证分布式事务的原子性,让数据真正落盘。 + + 1. TiDB 从当前要写入的数据中选择一个 Key 作为当前事务的 Primary Key。 + 2. TiDB 从 PD 获取所有数据的写入路由信息,并将所有的 Key 按照所有的路由进行分类。 + 3. TiDB 并发地向所有涉及的 TiKV 发起 prewrite 请求。TiKV 收到 prewrite 数据后,检查数据版本信息是否存在冲突或已过期。符合条件的数据会被加锁。 + 4. TiDB 收到所有 prewrite 响应且所有 prewrite 都成功。 + 5. TiDB 向 PD 获取第二个全局唯一递增版本号,定义为本次事务的 `commit_ts`。 + 6. TiDB 向 Primary Key 所在 TiKV 发起第二阶段提交。TiKV 收到 commit 操作后,检查数据合法性,清理 prewrite 阶段留下的锁。 + 7. TiDB 收到两阶段提交成功的信息。 + +6. TiDB 向客户端返回事务提交成功的信息。 + +7. TiDB 异步清理本次事务遗留的锁信息。 + +## 优缺点分析 + +通过分析 TiDB 中事务的处理流程,可以发现 TiDB 事务有如下优点: + +* 实现原理简单,易于理解。 +* 基于单实例事务实现了跨节点事务。 +* 锁管理实现了去中心化。 + +但 TiDB 事务也存在以下缺点: + +* 两阶段提交使网络交互增多。 +* 需要一个中心化的版本管理服务。 +* 事务数据量过大时易导致内存暴涨。 + +实际应用中,你可以[根据事务的大小进行针对性处理](/v3.0/reference/transactions/overview.md#事务大小),以提高事务的执行效率。 + +## 事务的重试 + +使用乐观事务模型时,在高冲突率的场景中,事务很容易提交失败。而 MySQL 内部使用的是悲观事务模型,在执行 SQL 语句的过程中进行冲突检测,所以提交时很难出现异常。为了兼容 MySQL 的悲观事务行为,TiDB 提供了重试机制。 + +### 重试机制 + +当事务提交后,如果发现冲突,TiDB 内部重新执行包含写操作的 SQL 语句。你可以通过设置 `tidb_disable_txn_auto_retry = off` 开启自动重试,并通过 `tidb_retry_limit` 设置重试次数: + +```sql +# 设置是否禁用自动重试,默认为 “on”,即不重试。 +tidb_disable_txn_auto_retry = off +# 控制重试次数,默认为 “10”。只有自动重试启用时该参数才会生效。 +# 当 “tidb_retry_limit= 0” 时,也会禁用自动重试。 +tidb_retry_limit = 10 +``` + +你也可以修改当前 Session 或 Global 的值: + +- Session 级别设置: + + {{< copyable "sql" >}} + + ```sql + set @@tidb_disable_txn_auto_retry = off; + ``` + + {{< copyable "sql" >}} + + ```sql + set @@tidb_retry_limit = 10; + ``` + +- Global 级别设置: + + {{< copyable "sql" >}} + + ```sql + set @@global.tidb_disable_txn_auto_retry = off; + ``` + + {{< copyable "sql" >}} + + ```sql + set @@global.tidb_retry_limit = 10; + ``` + +> **注意:** +> +> `tidb_retry_limit` 变量决定了事务重试的最大次数。当它被设置为 0 时,所有事务都不会自动重试,包括自动提交的单语句隐式事务。这是彻底禁用 TiDB 中自动重试机制的方法。禁用自动重试后,所有冲突的事务都会以最快的方式上报失败信息 (`try again later`) 给应用层。 + +### 重试的局限性 + +TiDB 默认不进行事务重试,因为重试事务可能会导致更新丢失,从而破坏[可重复读的隔离级别](/v3.0/reference/transactions/transaction-isolation.md)。 + +事务重试的局限性与其原理有关。事务重试可概括为以下三个步骤: + +1. 重新获取 `start_ts`。 +2. 重新执行包含写操作的 SQL 语句。 +3. 再次进行两阶段提交。 + +第二步中,重试时仅重新执行包含写操作的 SQL 语句,并不涉及读操作的 SQL 语句。但是当前事务中读到数据的时间与事务真正开始的时间发生了变化,写入的版本变成了重试时获取的 `start_ts` 而非事务一开始时获取的 `start_ts`。因此,当事务中存在依赖查询结果来更新的语句时,重试将无法保证事务原本可重复读的隔离级别,最终可能导致结果与预期出现不一致。 + +如果业务可以容忍事务重试导致的异常,或并不关注事务是否以可重复读的隔离级别来执行,则可以开启自动重试。 + +## 冲突检测 + +乐观事务下,检测底层数据是否存在写写冲突是一个很重要的操作。具体而言,TiKV 在 prewrite 阶段就需要读取数据进行检测。为了优化这一块性能,TiDB 集群会在内存里面进行一次冲突预检测。 + +作为一个分布式系统,TiDB 在内存中的冲突检测主要在两个模块进行: + +- TiDB 层。如果发现 TiDB 实例本身就存在写写冲突,那么第一个写入发出后,后面的写入已经清楚地知道自己冲突了,无需再往下层 TiKV 发送请求去检测冲突。 +- TiKV 层。主要发生在 prewrite 阶段。因为 TiDB 集群是一个分布式系统,TiDB 实例本身无状态,实例之间无法感知到彼此的存在,也就无法确认自己的写入与别的 TiDB 实例是否存在冲突,所以会在 TiKV 这一层检测具体的数据是否有冲突。 + +其中 TiDB 层的冲突检测可以根据场景需要选择打开或关闭,具体配置项如下: + +```toml +# 事务内存锁相关配置,当本地事务冲突比较多时建议开启。 +[txn-local-latches] +# 是否开启内存锁,默认为 false,即不开启。 +enabled = false +# Hash 对应的 slot 数,会自动向上调整为 2 的指数倍。 +# 每个 slot 占 32 Bytes 内存。当写入数据的范围比较广时(如导数据), +# 设置过小会导致变慢,性能下降。(默认为 2048000) +capacity = 2048000 +``` + +配置项 `capacity` 主要影响到冲突判断的正确性。在实现冲突检测时,不可能把所有的 Key 都存到内存里,所以真正存下来的是每个 Key 的 Hash 值。有 Hash 算法就有碰撞也就是误判的概率,这里可以通过配置 `capacity` 来控制 Hash 取模的值: + +* `capacity` 值越小,占用内存小,误判概率越大。 +* `capacity` 值越大,占用内存大,误判概率越小。 + +实际应用时,如果业务场景能够预判断写入不存在冲突(如导入数据操作),建议关闭冲突检测。 + +相应地,在 TiKV 层检测内存中是否存在冲突也有类似的机制。不同的是,TiKV 层的检测会更严格且不允许关闭,仅支持对 Hash 取模值进行配置: + +```toml +# scheduler 内置一个内存锁机制,防止同时对一个 Key 进行操作。 +# 每个 Key hash 到不同的 slot。(默认为 2048000) +scheduler-concurrency = 2048000 +``` + +此外,TiKV 支持监控等待 latch 的时间: + +![Scheduler latch wait duration](/media/optimistic-transaction-metric.png) + +当 `Scheduler latch wait duration` 的值特别高时,说明大量时间消耗在等待锁的请求上。如果不存在底层写入慢的问题,基本上可以判断该段时间内冲突比较多。 + +## 更多阅读 + +- [Percolator 和 TiDB 事务算法](https://pingcap.com/blog-cn/percolator-and-txn/) \ No newline at end of file diff --git a/v3.0/reference/transactions/transaction-pessimistic.md b/v3.0/reference/transactions/transaction-pessimistic.md index 005f49aa4d19..99d5e6d4498f 100644 --- a/v3.0/reference/transactions/transaction-pessimistic.md +++ b/v3.0/reference/transactions/transaction-pessimistic.md @@ -1,12 +1,12 @@ --- -title: TiDB 悲观事务模式 +title: TiDB 悲观事务模型 +summary: 了解 TiDB 的悲观事务模型。 category: reference --- -# TiDB 悲观事务模式 +# TiDB 悲观事务模型 -TiDB 默认使用乐观事务模式,存在事务提交时因为冲突而失败的问题。为了保证事务的成功率,需要修改应用程序,加上重试的逻辑。 -悲观事务模式可以避免这个问题,应用程序无需添加重试逻辑,就可以正常执行。 +TiDB 的乐观事务模型会导致事务提交时因为冲突而失败。为了保证事务的成功率,需要修改应用程序,加上重试的逻辑。悲观事务模式可以避免这个问题,应用程序无需添加重试逻辑,就可以正常执行。 ## 悲观事务模式的行为 diff --git a/v3.1/TOC.md b/v3.1/TOC.md index 8e818cf69e63..f0505d11345c 100644 --- a/v3.1/TOC.md +++ b/v3.1/TOC.md @@ -236,9 +236,9 @@ - [基于角色的访问控制](/v3.1/reference/security/role-based-access-control.md) - [TiDB 证书鉴权使用指南](/v3.1/reference/security/cert-based-authentication.md) + 事务 - - [事务语句](/v3.1/reference/transactions/overview.md) - - [事务模型](/v3.1/reference/transactions/transaction-model.md) + - [事务概览](/v3.1/reference/transactions/overview.md) - [隔离级别](/v3.1/reference/transactions/transaction-isolation.md) + - [乐观事务](/v3.1/reference/transactions/transaction-optimistic.md) - [悲观事务](/v3.1/reference/transactions/transaction-pessimistic.md) + 系统数据库 - [`mysql`](/v3.1/reference/system-databases/mysql.md) @@ -272,7 +272,6 @@ - [Grafana 监控最佳实践](/v3.1/reference/best-practices/grafana-monitor.md) - [PD 调度策略最佳实践](/v3.1/reference/best-practices/pd-scheduling.md) - [海量 Region 集群调优最佳实践](/v3.1/reference/best-practices/massive-regions.md) - - [乐观锁事务最佳实践](/v3.1/reference/best-practices/optimistic-transaction.md) + [TiSpark 使用指南](/v3.1/reference/tispark.md) + TiDB Binlog - [概述](/v3.1/reference/tidb-binlog/overview.md) diff --git a/v3.1/glossary.md b/v3.1/glossary.md index dc91ff18aa8b..3b31e69e5d20 100644 --- a/v3.1/glossary.md +++ b/v3.1/glossary.md @@ -1,6 +1,6 @@ --- title: 术语表 -summary: 学习 TiDB 相关术语。 +summary: 了解 TiDB 相关术语。 category: glossary --- @@ -13,29 +13,16 @@ category: glossary ACID 是指数据库管理系统在写入或更新资料的过程中,为保证[事务](#事务)是正确可靠的,所必须具备的四个特性:原子性 (atomicity)、一致性 (consistency)、隔离性(isolation)以及持久性(durability)。 * 原子性 (atomicity) 指一个事务中的所有操作,或者全部完成,或者全部不完成,不会结束在中间某个环节。TiDB 通过 Primary Key 所在 [Region](#regionpeerraft-group) 的原子性来保证分布式事务的原子性。 - * 一致性 (consistency) 指在事务开始之前和结束以后,数据库的完整性没有被破坏。TiDB 在写入数据之前,会校验数据的一致性,校验通过才会写入内存并返回成功。 - * 隔离性 (isolation) 指数据库允许多个并发事务同时对其数据进行读写和修改的能力。隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致,主要用于处理并发场景。TiDB 目前只支持一种隔离级别,即可重复读。 - * 持久性 (durability) 指事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。在 TiDB 中,事务一旦提交成功,数据全部持久化存储到 TiKV,此时即使 TiDB 服务器宕机也不会出现数据丢失。 -## B - -### 悲观事务 - -悲观事务假定并发事务会发生冲突,所以每一条 SQL 语句执行后都会检测冲突,只有在确保事务一定能够执行成功后,才开始提交。另有[乐观事务](#乐观事务)。 - ## L ### Leader/Follower/Learner 它们分别对应 [Peer](#regionpeerraft-group) 的三种角色。其中 Leader 负责响应客户端的读写请求;Follower 被动地从 Leader 同步数据,当 Leader 失效时会进行选举产生新的 Leader;Learner 是一种特殊的角色,它只参与同步 raft log 而不参与投票,在目前的实现中只短暂存在于添加副本的中间步骤。 -### 乐观事务 - -乐观事务假定不会发生并发冲突,只有在事务最终提交时才会检测冲突。另有[悲观事务](#悲观事务)。 - ## O ### Operator @@ -86,16 +73,6 @@ Scheduler(调度器)是 PD 中生成调度的组件。PD 中每个调度器 - `hot-region-scheduler`:保持不同节点的读写热点 Region 均衡。 - `evict-leader-{store-id}`:驱逐某个节点的所有 Leader。(常用于滚动升级) -### 事务 - -事务指一系列有限的数据库操作序列。TiDB 中的事务具备 [ACID](#ACID) 四个特性。 - ### Store PD 中的 Store 指的是集群中的存储节点,也就是 tikv-server 实例。Store 与 TiKV 实例是严格一一对应的,即使在同一主机甚至同一块磁盘部署多个 TiKV 实例,这些实例也对会对应不同的 Store。 - -## X - -### 显式事务/隐式事务 - -由事务控制语句定义开始和结束的事务为显式事务。无需定义开始的事务为隐式事务。 \ No newline at end of file diff --git a/v3.1/reference/best-practices/optimistic-transaction.md b/v3.1/reference/best-practices/optimistic-transaction.md deleted file mode 100644 index 68daf4b59f5d..000000000000 --- a/v3.1/reference/best-practices/optimistic-transaction.md +++ /dev/null @@ -1,264 +0,0 @@ ---- -title: 乐观锁事务最佳实践 -summary: 了解 TiDB 的乐观事务模型。 -category: reference ---- - -# 乐观锁事务最佳实践 - -本文介绍 TiDB 乐观锁机制的实现原理,并通过分析乐观锁在多种场景下的应用为业务提供最佳实践。本文假定你对 [TiDB 的整体架构](/v3.1/architecture.md#tidb-整体架构)和 [Percolator](https://www.usenix.org/legacy/event/osdi10/tech/full_papers/Peng.pdf) 事务模型都有一定了解,相关核心概念如下: - -- [ACID](/v3.1/glossary.md#acid) -- [事务](/v3.1/glossary.md#事务) -- [乐观事务](/v3.1/glossary.md#乐观事务) -- [悲观事务](/v3.1/glossary.md#悲观事务) -- [显式事务/隐式事务](/v3.1/glossary.md#显式事务隐式事务) - -## 乐观事务原理 - -TiDB 中事务使用两阶段提交,流程如下: - -![TiDB 中的两阶段提交](/media/best-practices/2pc-in-tidb.png) - -1. 客户端开始一个事务。 - - TiDB 从 PD 获取一个全局唯一递增的版本号作为当前事务的开始版本号,这里定义为该事务的 `start_ts` 版本。 - -2. 客户端发起读请求。 - - a. TiDB 从 PD 获取数据路由信息,即数据具体存在哪个 TiKV 节点上。 - - b. TiDB 从 TiKV 获取 `start_ts` 版本下对应的数据信息。 - -3. 客户端发起写请求。 - - TiDB 校验写入数据是否符合一致性约束(如数据类型是否正确、是否符合唯一索引约束等)。**校验通过的数据将存放在内存里。** - -4. 客户端发起 commit。 - -5. TiDB 开始两阶段提交,保证分布式事务的原子性,让数据真正落盘。 - - a. TiDB 从当前要写入的数据中选择一个 Key 作为当前事务的 Primary Key。 - - b. TiDB 从 PD 获取所有数据的写入路由信息,并将所有的 Key 按照所有的路由进行分类。 - - c. TiDB 并发地向所有涉及的 TiKV 发起 prewrite 请求。TiKV 收到 prewrite 数据后,检查数据版本信息是否存在冲突或已过期。符合条件的数据会被加锁。 - - d. TiDB 成功收到所有 prewrite 请求。 - - e. TiDB 向 PD 获取第二个全局唯一递增版本号,定义为本次事务的 `commit_ts`。 - - f. TiDB 向 Primary Key 所在 TiKV 发起第二阶段提交。TiKV 收到 commit 操作后,检查数据合法性,清理 prewrite 阶段留下的锁。 - - g. TiDB 收到 f 成功信息。 - -6. TiDB 向客户端返回事务提交成功的信息。 - -7. TiDB 异步清理本次事务遗留的锁信息。 - -## 优缺点分析 - -通过分析 TiDB 中事务的处理流程,可以发现 TiDB 事务有如下优点: - -* 实现原理简单,易于理解。 -* 基于单实例事务实现了跨节点事务。 -* 锁管理实现了去中心化。 - -但 TiDB 事务也存在以下缺点: - -* 两阶段提交使网络交互增多。 -* 缺少一个中心化的版本管理服务。 -* 事务数据量过大时易导致内存暴涨。 - -## 事务大小 - -对于 TiDB 乐观事务而言,事务太大或者太小,都会影响事务性能。为了克服上述事务在处理过程中的不足,在实际应用中可以根据事务大小进行针对性处理。 - -### 小事务 - -在自动提交状态 (`autocommit = 1`) 下,下面三条语句各为一个事务: - -```sql -# 使用自动提交的原始版本。 -UPDATE my_table SET a ='new_value' WHERE id = 1; -UPDATE my_table SET a ='newer_value' WHERE id = 2; -UPDATE my_table SET a ='newest_value' WHERE id = 3; -``` - -此时每一条语句都需要经过两阶段提交,频繁的网络交互致使小事务延迟率高。为提升事务执行效率,可以选择使用显式事务,即在一个事务内执行三条语句: - -```sql -# 优化后版本。 -START TRANSACTION; -UPDATE my_table SET a ='new_value' WHERE id = 1; -UPDATE my_table SET a ='newer_value' WHERE id = 2; -UPDATE my_table SET a ='newest_value' WHERE id = 3; -COMMIT; -``` - -同理,执行 `INSERT` 语句时,建议使用显式事务。 - -### 大事务 - -通过分析两阶段提交的过程,可以发现单个事务过大时会存在以下问题: - -* 客户端在提交之前,数据都写在内存中,而数据量过多时易导致 OOM (Out of Memory) 错误。 -* 在第一阶段写入数据时,与其他事务出现冲突的概率会指数级增长,使事务之间相互阻塞影响。 -* 最终导致事务完成提交的耗时增加。 - -因此,TiDB 特意对事务的大小做了一些限制: - -* 单个事务包含的 SQL 语句不超过 5000 条(默认) -* 每个键值对不超过 6 MB -* 键值对的总数不超过 300000 -* 键值对的总大小不超过 100 MB - -为了使性能达到最优,建议每 100~500 行写入一个事务。 - -## 事务冲突 - -事务的冲突,主要指事务并发执行时对相同的 Key 进行了读写操作。冲突主要有两种形式: - -* 读写冲突:部分事务进行读操作时,有事务在同一时间对相同的 Key 进行写操作。 -* 写写冲突:不同事务同时对相同的 Key 进行写操作。 - -在 TiDB 的乐观锁机制中,只有在客户端执行 `commit` 时,才会触发两阶段提交并检测是否存在写写冲突。也就是说,在乐观事务下,如果存在写写冲突,在事务提交阶段就会暴露出来,因而更容易被用户感知。 - -### 默认冲突行为 - -乐观事务下,默认在最终提交时才会进行冲突检测。当两个事务同时更新同一行数据,即并发事务存在冲突时,不同时间点的执行结果如下: - -![并发事务冲突流程](/media/best-practices/optimistic-transaction-table1.png) - -根据乐观锁检测写写冲突的设定,该实例的执行逻辑分析如下: - -![并发事务冲突逻辑](/media/best-practices/optimistic-transaction-case1.png) - -1. 如上图,事务 A 在时间点 `t1` 开始,事务 B 在 `t2` 开始。 - -2. 事务 A、事务 B 同时更新同一行数据。 - -3. `t4` 时,事务 A 更新 `id = 1` 的同一行数据。 虽然 `t3` 时,事务 B 已经更新了这一行数据,但是乐观事务只有在事务 commit 时才检测冲突,因此 `t4` 的操作执行成功了。 - -4. `t5` 时,事务 B 成功提交,数据落盘。 - -5. `t6` 时,事务 A 尝试提交,检测冲突时发现 `t1` 之后有新的数据写入,因此返回错误,提示客户端重试,事务 A 提交失败。 - -### 重试机制 - -TiDB 中默认使用乐观事务模型,因而在高冲突率的场景中,事务很容易提交失败。而 MySQL 内部使用的是悲观事务模型,对应到上面的实例中,事务 A 在 `t4` 时就会返回错误,提示客户端根据需求去重试。 - -换言之,MySQL 在执行 SQL 语句的过程中进行冲突检测,所以提交时很难出现异常。由于 TiDB 使用乐观锁机制造成了两边行为不一致,要兼容 MySQL 的悲观事务行为,需要在客户端修改大量的代码。为了便于广大 MySQL 用户使用,TiDB 提供了重试机制。当事务提交后,如果发现冲突,TiDB 内部重新执行包含写操作的 SQL 语句。你可以通过设置 `tidb_disable_txn_auto_retry` 和 `tidb_retry_limit` 开启自动重试: - -```toml -# 用于设置是否禁用自动重试,默认不重试。 -tidb_disable_txn_auto_retry = on -# 用来控制重试次数。只有自动重试启用时该参数才会生效。 -# 当 “tidb_retry_limit= 0” 时,也会禁用自动重试。 -tidb_retry_limit = 10 -``` - -推荐通过以下两种方式进行参数设置: - -1. Session 级别设置: - - {{< copyable "sql" >}} - - ```sql - set @@tidb_disable_txn_auto_retry = off; - set @@tidb_retry_limit = 10; - ``` - -2. Global 级别设置: - - {{< copyable "sql" >}} - - ```sql - set @@global.tidb_disable_txn_auto_retry = off; - set @@global.tidb_retry_limit = 10; - ``` - -### 重试的局限性 - -基于重试机制的原理,可将重试过程概括为以下三个步骤: - -1. 重新获取 `start_ts`。 - -2. 重新执行包含写操作的 SQL 语句。 - -3. 两阶段提交。 - -根据第二步,重试时仅重新执行包含写操作的 SQL 语句,并不涉及读操作的 SQL 语句。这会引发以下问题: - -1. `start_ts` 发生了变更。当前事务中,读到数据的时间与事务真正开始的时间发生了变化。同理,写入的版本变成了重试时获取的 `start_ts` 而非事务一开始时获取的 `start_ts`。 - -2. 当前事务中,如果存在依赖查询结果来更新的语句,结果将变得不可控。 - -以下实例来具体说明了重试的局限性。开启自动重试后,当同时更新同一行数据时,Session A 和 Session B 在不同时间点的执行结果如下: - -![自动重试流程](/media/best-practices/optimistic-transaction-table2.png) - -该实例的执行逻辑分析如下: - -![自动重试逻辑](/media/best-practices/optimistic-transaction-case2.png) - -1. 如图,Session B 在 `t2` 时开始事务 2,`t5` 时提交成功。Session A 的事务 1 在事务 2 之前开始,在事务 2 提交完成后提交。 - -2. 事务 1、事务 2 同时更新同一行数据。 - -3. Session A 提交事务 1 时发现冲突,TiDB 内部重试事务 1。 - 1. 重新取得新的 `start_ts` 为 `t8’`。 - 2. 重新执行更新语句 `update tidb set name='pd' where id =1 and status=1`。 - 1. 发现当前版本 `t8’` 下并不存在符合条件的语句,不需要更新。 - 2. 没有数据更新,返回上层成功。 - -4. TiDB 认为事务 1 重试成功,返回客户端成功。 - -5. Session A 认为事务执行成功。如果在不存在其他更新,此时查询结果会发现数据与预想的不一致。 - -由上述分析可知,对于重试事务,当事务中更新语句需要依赖查询结果时,会重新取版本号作为 `start_ts`,所以无法保证事务原本可重复读的隔离级别,最终可能导致结果与预期出现不一致。 - -因此,如果存在依赖查询结果来更新 SQL 语句的事务,建议不要打开 TiDB 乐观锁的重试机制。 - -### 冲突预检 - -由上文可以知道,检测底层数据是否存在写写冲突是一个很重要的操作。具体而言,TiKV 在 prewrite 阶段就需要读取数据进行检测。为了优化这一块性能,TiDB 集群会在内存里面进行一次冲突预检测。 - -作为一个分布式系统,TiDB 在内存中的冲突检测主要在两个模块进行: - -* TiDB 层。如果发现 TiDB 实例本身就存在写写冲突,那么第一个写入发出后,后面的写入已经清楚地知道自己冲突了,无需再往下层 TiKV 发送请求去检测冲突。 -* TiKV 层。主要发生在 prewrite 阶段。因为 TiDB 集群是一个分布式系统,TiDB 实例本身无状态,实例之间无法感知到彼此的存在,也就无法确认自己的写入与别的 TiDB 实例是否存在冲突,所以会在 TiKV 这一层检测具体的数据是否有冲突。 - -其中 TiDB 层的冲突检测可以选择关闭,具体配置项如下: - -```toml -# 事务内存锁相关配置,当本地事务冲突比较多时建议开启。 -[txn-local-latches] -# 是否开启内存锁,默认为关闭。 -enabled = false -# Hash 对应的 slot 数,会自动向上调整为 2 的指数倍。 -# 每个 slot 占 32 Bytes 内存。当写入数据的范围比较广时(如导数据), -# 设置过小会导致变慢,性能下降。(默认为 2048000) -capacity = 2048000 -``` - -配置项 `capacity` 主要影响到冲突判断的正确性。在实现冲突检测时,不可能把所有的 Key 都存到内存里,所以真正存下来的是每个 Key 的 Hash 值。有 Hash 算法就有碰撞也就是误判的概率,这里可以通过配置 `capacity` 来控制 Hash 取模的值: - -* `capacity` 值越小,占用内存小,误判概率越大。 -* `capacity` 值越大,占用内存大,误判概率越小。 - -实际应用时,如果业务场景能够预判断写入不存在冲突(如导入数据操作),建议关闭冲突检测。 - -相应地,在 TiKV 层检测内存中是否存在冲突也有类似的机制。不同的是,TiKV 层的检测会更严格且不允许关闭,仅支持对 Hash 取模值进行配置: - -```toml -# scheduler 内置一个内存锁机制,防止同时对一个 Key 进行操作。 -# 每个 Key hash 到不同的 slot。(默认为 2048000) -scheduler-concurrency = 2048000 -``` - -此外,TiKV 支持监控等待 latch 的时间: - -![Scheduler latch wait duration](/media/best-practices/optimistic-transaction-metric.png) - -当 `Scheduler latch wait duration` 的值特别高时,说明大量时间消耗在等待锁的请求上。如果不存在底层写入慢的问题,基本上可以判断该段时间内冲突比较多。 diff --git a/v3.1/reference/configuration/tidb-server/tidb-specific-variables.md b/v3.1/reference/configuration/tidb-server/tidb-specific-variables.md index 786a8a61b541..2dbd0f51f787 100644 --- a/v3.1/reference/configuration/tidb-server/tidb-specific-variables.md +++ b/v3.1/reference/configuration/tidb-server/tidb-specific-variables.md @@ -312,7 +312,7 @@ set @@global.tidb_distsql_scan_concurrency = 10; 默认值:10 -这个变量用来设置最多可重试次数,即在一个事务执行中遇到可重试的错误(例如事务冲突、事务提交过慢或表结构变更)时,这个事务可以被重新执行,这个变量值表明最多可重试的次数。 +这个变量用来设置最大重试次数。一个事务执行中遇到可重试的错误(例如事务冲突、事务提交过慢或表结构变更)时,会根据该变量的设置进行重试。注意当 `tidb_retry_limit = 0` 时,也会禁用自动重试。 ### tidb_disable_txn_auto_retry @@ -326,7 +326,7 @@ set @@global.tidb_distsql_scan_concurrency = 10; 这个变量不会影响自动提交的隐式事务和 TiDB 内部执行的事务,它们依旧会根据 `tidb_retry_limit` 的值来决定最大重试次数。 -是否需要禁用自动重试,请参考[事务自动重试及带来的异常](/v3.1/reference/transactions/transaction-isolation.md#事务自动重试及带来的异常)。 +是否需要禁用自动重试,请参考[重试的局限性](/v3.1/reference/transactions/transaction-optimistic.md#重试的局限性)。 ### tidb_backoff_weight diff --git a/v3.1/reference/mysql-compatibility.md b/v3.1/reference/mysql-compatibility.md index f398c4b2e9b9..acaf0dd61eba 100644 --- a/v3.1/reference/mysql-compatibility.md +++ b/v3.1/reference/mysql-compatibility.md @@ -13,7 +13,7 @@ TiDB 支持 MySQL 传输协议及其绝大多数的语法。这意味着您现 > **注意:** > -> 本页内容仅涉及 MySQL 与 TiDB 的总体差异。关于[安全特性](/v3.1/reference/security/compatibility.md)及[事务模型](/v3.1/reference/transactions/transaction-model.md)的兼容信息请查看各自具体页面。 +> 本页内容仅涉及 MySQL 与 TiDB 的总体差异。关于[安全特性](/v3.1/reference/security/compatibility.md)、[悲观事务模型](/v3.1/reference/transactions/transaction-pessimistic.md#和-mysql-innodb-的差异)的兼容信息请查看各自具体页面。 ## 不支持的特性 diff --git a/v3.1/reference/sql/statements/load-data.md b/v3.1/reference/sql/statements/load-data.md index 9cadfe59cb85..69667d91515d 100644 --- a/v3.1/reference/sql/statements/load-data.md +++ b/v3.1/reference/sql/statements/load-data.md @@ -59,4 +59,4 @@ Records: 815264 Deleted: 0 Skipped: 0 Warnings: 0 ## 另请参阅 * [INSERT](/v3.1/reference/sql/statements/insert.md) -* [Transaction Model](/v3.1/reference/transactions/transaction-model.md) +* [乐观事务模型](/v3.1/reference/transactions/transaction-optimistic.md) diff --git a/v3.1/reference/sql/statements/select.md b/v3.1/reference/sql/statements/select.md index 51e3e0f09002..e29649208502 100644 --- a/v3.1/reference/sql/statements/select.md +++ b/v3.1/reference/sql/statements/select.md @@ -74,7 +74,7 @@ category: reference |`HAVING where_condition` | Having 子句与 Where 子句作用类似,Having 子句可以让过滤 GroupBy 后的各种数据,Where 子句用于在聚合前过滤记录。| |`ORDER BY` | OrderBy 子句用于指定结果排序顺序,可以按照列、表达式或者是 `select_expr` 列表中某个位置的字段进行排序。| |`LIMIT` | Limit 子句用于限制结果条数。Limit 接受一个或两个数字参数,如果只有一个参数,那么表示返回数据的最大行数;如果是两个参数,那么第一个参数表示返回数据的第一行的偏移量(第一行数据的偏移量是 0),第二个参数指定返回数据的最大条目数。| -|`FOR UPDATE` | 对查询结果集所有行上锁(对于在查询条件内,但是不在结果集的行,将不会加锁,如事务启动后由其他事务写入的行),以监测其他事务对这些的并发修改。TiDB 使用[乐观事务模型](/v3.1/reference/transactions/transaction-model.md#事务模型)在语句执行期间不会检测锁,因此,不会像 PostgreSQL 之类的数据库一样,在当前事务结束前阻止其他事务执行 UPDATE、DELETE 和 SELECT FOR UPDATE。在事务的提交阶段 SELECT FOR UPDATE 读到的行,也会进行两阶段提交,因此,它们也可以参与事务冲突检测。如发生写入冲突,那么包含 SELECT FOR UPDATE 语句的事务会提交失败。如果没有冲突,事务将成功提交,当提交结束时,这些被加锁的行,会产生一个新版本,可以让其他尚未提交的事务,在将来提交时发现写入冲突。| +|`FOR UPDATE` | 对查询结果集所有行上锁(对于在查询条件内,但是不在结果集的行,将不会加锁,如事务启动后由其他事务写入的行),以监测其他事务对这些的并发修改。使用[乐观事务模型](/v3.1/reference/transactions/transaction-optimistic.md)时,语句执行期间不会检测锁,因此,不会像 PostgreSQL 之类的数据库一样,在当前事务结束前阻止其他事务执行 `UPDATE`、`DELETE` 和 `SELECT FOR UPDATE`。在事务的提交阶段 `SELECT FOR UPDATE` 读到的行,也会进行两阶段提交,因此,它们也可以参与事务冲突检测。如发生写入冲突,那么包含 `SELECT FOR UPDATE` 语句的事务会提交失败。如果没有冲突,事务将成功提交,当提交结束时,这些被加锁的行,会产生一个新版本,可以让其他尚未提交的事务,在将来提交时发现写入冲突。若使用悲观事务,则行为与其他数据库基本相同,不一致之处参考[和 MySQL InnoDB 的差异](/v3.1/reference/transactions/transaction-pessimistic.md#和-mysql-innodb-的差异)。 | |`LOCK IN SHARE MODE` | TiDB 出于兼容性解析这个语法,但是不做任何处理| ## 示例 diff --git a/v3.1/reference/transactions/overview.md b/v3.1/reference/transactions/overview.md index 4487f3f817db..a3775339ba58 100644 --- a/v3.1/reference/transactions/overview.md +++ b/v3.1/reference/transactions/overview.md @@ -1,15 +1,16 @@ --- title: TiDB 事务概览 +summary: 了解 TiDB 中的事务。 category: reference --- # TiDB 事务概览 -TiDB 支持完整的分布式事务。本文主要介绍涉及到事务的语句、显式/隐式事务以及事务的隔离级别和惰性检查。 +TiDB 支持完整的分布式事务,提供[乐观事务](/v3.1/reference/transactions/transaction-optimistic.md)与[悲观事务](/v3.1/reference/transactions/transaction-pessimistic.md)(TiDB 3.0 中引入)两种事务模型。本文主要介绍涉及到事务的语句、显式/隐式事务、事务的隔离级别和惰性检查,以及事务大小的限制。 -常用的变量包括 `autocommit`、[`tidb_disable_txn_auto_retry`](/v3.1/reference/configuration/tidb-server/tidb-specific-variables.md#tidb_disable_txn_auto_retry) 以及 [`tidb_retry_limit`](/v3.1/reference/configuration/tidb-server/tidb-specific-variables.md#tidb_retry_limit)。 +常用的变量包括 [`autocommit`](#自动提交)、[`tidb_disable_txn_auto_retry`](/v3.1/reference/configuration/tidb-server/tidb-specific-variables.md#tidb_disable_txn_auto_retry) 以及 [`tidb_retry_limit`](/v3.1/reference/configuration/tidb-server/tidb-specific-variables.md#tidb_retry_limit)。 -## 事务常用语句 +## 常用事务语句 ### `BEGIN` 和 `START TRANSACTION` @@ -69,13 +70,11 @@ ROLLBACK; SET autocommit = {0 | 1} ``` -当 `autocommit = 1` 时(默认),当前的 Session 为自动提交状态。设置 `autocommit = 0` 时将更改当前 Session 为非自动提交状态。 - -自动提交状态下,每条语句运行后,TiDB 会自动将修改提交到数据库中。非自动提交状态下,通过执行 `COMMIT` 语句来手动提交事务。 +当 `autocommit = 1` 时(默认),当前的 Session 为自动提交状态,即每条语句运行后,TiDB 会自动将修改提交到数据库中。设置 `autocommit = 0` 时更改当前 Session 更改为非自动提交状态,通过执行 `COMMIT` 语句来手动提交事务。 > **注意:** > -> 某些语句执行后会导致隐式提交。例如,执行 `[BEGIN|START TRANCATION]` 语句的时候,TiDB 会试图提交上一个事务,并开启一个新的事务。详情参见 [implicit commit](https://dev.mysql.com/doc/refman/8.0/en/implicit-commit.html)。 +> 某些语句执行后会导致隐式提交。例如,执行 `[BEGIN|START TRANCATION]` 语句时,TiDB 会试图提交上一个事务,并开启一个新的事务。详情参见 [implicit commit](https://dev.mysql.com/doc/refman/8.0/en/implicit-commit.html)。 另外,`autocommit` 也是一个系统变量,你可以通过变量赋值语句修改当前 Session 或 Global 的值。 @@ -93,7 +92,7 @@ SET @@GLOBAL.autocommit = {0 | 1}; ## 显式事务和隐式事务 -TiDB 可以显式地使用事务 (`[BEGIN|START TRANSACTION]`/`COMMIT`) 或者隐式地使用事务 (`SET autocommit = 1`)。 +TiDB 可以显式地使用事务(通过 `[BEGIN|START TRANSACTION]`/`COMMIT` 语句定义事务的开始和结束) 或者隐式地使用事务 (`SET autocommit = 1`)。 在自动提交状态下,使用 `[BEGIN|START TRANSACTION]` 语句会显式地开启一个事务,同时也会禁用自动提交,使隐式事务变成显式事务。直到执行 `COMMIT` 或 `ROLLBACK` 语句时才会恢复到此前默认的自动提交状态。 @@ -109,7 +108,7 @@ TiDB **只支持** `SNAPSHOT ISOLATION`,可以通过下面的语句将当前 S SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; ``` -## 事务的惰性检查 +## 惰性检查 TiDB 中,对于普通的 `INSERT` 语句写入的值,会进行惰性检查。惰性检查的含义是,不在 `INSERT` 语句执行时进行唯一约束的检查,而在事务提交时进行唯一约束的检查。 @@ -159,4 +158,57 @@ insert into test values (3); rollback; ``` -以上例子中,第二条语句执行失败。由于调用了 `rollback`,因此事务不会将任何数据写入数据库。 +以上例子中,第二条语句执行失败。由于调用了 `ROLLBACK`,因此事务不会将任何数据写入数据库。 + +## 事务大小 + +对于 TiDB 事务而言,事务太大或太小,都会影响事务的执行效率。 + +### 小事务 + +以如下 query 为例,当 `autocommit = 1` 时,下面三条语句各为一个事务: + +{{< copyable "sql" >}} + +```sql +UPDATE my_table SET a ='new_value' WHERE id = 1; +UPDATE my_table SET a ='newer_value' WHERE id = 2; +UPDATE my_table SET a ='newest_value' WHERE id = 3; +``` + +此时每一条语句都需要经过两阶段提交,频繁的网络交互致使延迟率高。为提升事务执行效率,可以选择使用显式事务,即在一个事务内执行三条语句。 + +优化后版本: + +{{< copyable "sql" >}} + +```sql +START TRANSACTION; +UPDATE my_table SET a ='new_value' WHERE id = 1; +UPDATE my_table SET a ='newer_value' WHERE id = 2; +UPDATE my_table SET a ='newest_value' WHERE id = 3; +COMMIT; +``` + +同理,执行 `INSERT` 语句时,建议使用显式事务。 + +> **注意:** +> +> 由于 TiDB 中的资源是分布式的,TiDB 中单线程 workload 可能不会很好地利用分布式资源,因此性能相比于单实例部署的 MySQL 较低。这与 TiDB 中的事务延迟较高的情況类似。 + +### 大事务 + +由于 TiDB 两阶段提交的要求,修改数据的单个事务过大时会存在以下问题: + +* 客户端在提交之前,数据都写在内存中,而数据量过多时易导致 OOM (Out of Memory) 错误。 +* 在第一阶段写入数据耗时增加,与其他事务出现写冲突的概率会指数级增长。 +* 最终导致事务完成提交的耗时增加。 + +因此,TiDB 对事务做了一些限制: + +* 单个事务包含的 SQL 语句不超过 5000 条(默认) +* 每个键值对不超过 6 MB +* 键值对的总数不超过 300000 +* 键值对的总大小不超过 100 MB + +为了使性能达到最优,建议每 100~500 行写入一个事务。 \ No newline at end of file diff --git a/v3.1/reference/transactions/transaction-isolation.md b/v3.1/reference/transactions/transaction-isolation.md index 69a0fc863579..4530591f1eb4 100644 --- a/v3.1/reference/transactions/transaction-isolation.md +++ b/v3.1/reference/transactions/transaction-isolation.md @@ -1,13 +1,14 @@ --- title: TiDB 事务隔离级别 +summary: 了解 TiDB 事务的隔离级别。 category: reference --- # TiDB 事务隔离级别 -事务隔离级别是数据库事务处理的基础,ACID 中 I,即 Isolation,指的就是事务的隔离性。 +事务隔离级别是数据库事务处理的基础,[ACID](/v3.1/glossary.md#acid) 中的 “I”,即 Isolation,指的就是事务的隔离性。 -SQL 92 标准定义了 4 种隔离级别:读未提交、读已提交、可重复读、串行化。详见下表: +SQL-92 标准定义了 4 种隔离级别:读未提交 (READ UNCOMMITTED)、读已提交 (READ COMMITTED)、可重复读 (REPEATABLE READ)、串行化 (SERIALIZABLE)。详见下表: | Isolation Level | Dirty Write | Dirty Read | Fuzzy Read | Phantom | | ---------------- | ------------ | ------------ | ------------ | ------------ | @@ -16,13 +17,11 @@ SQL 92 标准定义了 4 种隔离级别:读未提交、读已提交、可重 | REPEATABLE READ | Not Possible | Not possible | Not possible | Possible | | SERIALIZABLE | Not Possible | Not possible | Not possible | Not possible | -TiDB 实现了快照隔离 (Snapshot Isolation) 级别的一致性。为与 MySQL 保持一致,又称其为“可重复读”。该隔离级别不同于 [ANSI 可重复读隔离级别](#与-ansi-可重复读隔离级别的区别)和 [MySQL 可重复读隔离级别](#与-mysql-可重复读隔离级别的区别)。 +TiDB 实现了快照隔离 (Snapshot Isolation, SI) 级别的一致性。为与 MySQL 保持一致,又称其为“可重复读”。该隔离级别不同于 [ANSI 可重复读隔离级别](#与-ansi-可重复读隔离级别的区别)和 [MySQL 可重复读隔离级别](#与-mysql-可重复读隔离级别的区别)。 > **注意:** > -> 在 TiDB v3.0 的默认设置中,事务的自动重试功能已经关闭。关于该项功能对隔离级别的影响以及如何开启该项功能,请参考[事务自动重试及带来的异常](#事务自动重试及带来的异常)。 - -TiDB 使用 [Percolator 事务模型](https://research.google.com/pubs/pub36726.html),当事务启动时会获取全局读时间戳,事务提交时也会获取全局提交时间戳,并以此确定事务的执行顺序,如果想了解 TiDB 事务模型的实现可以详细阅读以下两篇文章:[TiKV 的 MVCC (Multi-Version Concurrency Control) 机制](https://pingcap.com/blog-cn/mvcc-in-tikv/),[Percolator 和 TiDB 事务算法](https://pingcap.com/blog-cn/percolator-and-txn/)。 +> 在 TiDB v3.0 中,事务的自动重试功能默认为禁用状态。关于该项功能对隔离级别的影响以及如何开启该项功能,请参考[事务重试](/v3.1/reference/transactions/transaction-optimistic.md#重试机制)。 ## 可重复读 @@ -45,56 +44,12 @@ commit; | ### 与 ANSI 可重复读隔离级别的区别 -尽管名称是可重复读隔离级别,但是 TiDB 中可重复读隔离级别和 ANSI 可重复隔离级别是不同的。按照 [A Critique of ANSI SQL Isolation Levels](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/tr-95-51.pdf) 论文中的标准,TiDB 实现的是论文中的 Snapshot 隔离级别 (SI)。该隔离级别不会出现狭义上的幻读 (A3),但不会阻止广义上的幻读 (P3),同时,SI 还会出现写偏斜,而 ANSI 可重复读隔离级别不会出现写偏斜,会出现幻读。 +尽管名称是可重复读隔离级别,但是 TiDB 中可重复读隔离级别和 ANSI 可重复隔离级别是不同的。按照 [A Critique of ANSI SQL Isolation Levels](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/tr-95-51.pdf) 论文中的标准,TiDB 实现的是论文中的快照隔离级别。该隔离级别不会出现狭义上的幻读 (A3),但不会阻止广义上的幻读 (P3),同时,SI 还会出现写偏斜,而 ANSI 可重复读隔离级别不会出现写偏斜,会出现幻读。 ### 与 MySQL 可重复读隔离级别的区别 -MySQL 可重复读隔离级别在更新时并不检验当前版本是否可见,也就是说,即使该行在事务启动后被更新过,同样可以继续更新。这种情况在 TiDB 会导致事务回滚,导致事务最终失败,而 MySQL 是可以更新成功的。MySQL 的可重复读隔离级别并非 Snapshot 隔离级别,MySQL 可重复读隔离级别的一致性要弱于 Snapshot 隔离级别,也弱于 TiDB 的可重复读隔离级别。 - -## 事务自动重试及带来的异常 - -TiDB 默认不进行事务重试,因为重试事务可能会导致更新丢失,从而破坏快照隔离。如果业务可以容忍事务重试导致的异常,或并不关注事务是否以快照隔离级别来执行,则可以开启自动重试。通过设置 `tidb_disable_txn_auto_retry = off` 可开启该项功能。需注意 `tidb_retry_limit` 的值不能为 `0`,否则会禁用自动重试。开启自动重试以后,事务遇到提交出错的可能性会降低。 - -开启自动重试后,显式事务遇到冲突可能会导致最终结果不符合预期。 - -比如下面这两个例子: - -| Session1 | Session2 | -| ---------------- | ------------ | -| `begin;` | `begin;` | -| `select balance from t where id = 1;` | `update t set balance = balance -100 where id = 1;` | -| | `update t set balance = balance -100 where id = 2;` | -| // 使用 select 的结果决定后续的逻辑 | `commit;` | -| `if balance > 100 {` | | -| `update t set balance = balance + 100 where id = 2;` | | -| `}` | | -| `commit;` // 自动重试 | | - -| Session1 | Session2 | -| ---------------- | ------------ | -| `begin;` | `begin;` | -| `update t set balance = balance - 100 where id = 1;` | `delete from t where id = 1;` | -| | `commit;` | -| // 使用 affected_rows 的结果决定后续的逻辑 | | -| `if affected_rows > 0 {` | | -| `update t set balance = balance + 100 where id = 2;` | | -| `}` | | -| `commit;` // 自动重试 | | - -因为 TiDB 自动重试机制会把事务第一次执行的所有语句重新执行一遍,当一个事务里的后续语句是否执行取决于前面语句执行结果的时候,自动重试会违反快照隔离,导致更新丢失。这种情况下,需要在应用层重试整个事务。 - -通过配置 `tidb_disable_txn_auto_retry = on` 变量可以关掉显示事务的重试。 - -{{< copyable "sql" >}} - -```sql -SET GLOBAL tidb_disable_txn_auto_retry = on; -``` - -改变 `tidb_disable_txn_auto_retry` 变量不会影响 `autocommit = 1` 的单语句的隐式事务,因为该语句的自动重试,不会造成丢失更新等异常,即不会破坏事务的隔离性。 - -关掉显式事务重试后,如果出现事务冲突,commit 语句会返回错误,错误信息会包含 `try again later` 这个字符串,应用层可以用来判断遇到的错误是否是可以重试的。 +MySQL 可重复读隔离级别在更新时并不检验当前版本是否可见,也就是说,即使该行在事务启动后被更新过,同样可以继续更新。这种情况在 TiDB 会导致事务回滚,导致事务最终失败,而 MySQL 是可以更新成功的。MySQL 的可重复读隔离级别并非快照隔离级别,MySQL 可重复读隔离级别的一致性要弱于快照隔离级别,也弱于 TiDB 的可重复读隔离级别。 -如果事务执行过程中包含了应用层的逻辑,建议在应用层添加显式事务的重试,并关闭自动重试。 +## 更多阅读 -`tidb_retry_limit` 变量决定了事务重试的最大次数,默认值为 10,当它被设置为 0 时,所有事务都不会自动重试,包括自动提交的单语句隐式事务。这是彻底禁用 TiDB 中自动重试机制的方法。当用户相比于事务隔离性,更关心事务执行的延迟时,可以将它设置为 0,所有冲突的事务都会以最快的方式上报失败给应用层。 +- [TiKV 的 MVCC (Multi-Version Concurrency Control) 机制](https://pingcap.com/blog-cn/mvcc-in-tikv/) \ No newline at end of file diff --git a/v3.1/reference/transactions/transaction-model.md b/v3.1/reference/transactions/transaction-model.md deleted file mode 100644 index 5b72a91d5718..000000000000 --- a/v3.1/reference/transactions/transaction-model.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -title: 事务模型 -category: reference ---- - -# 事务模型 - -TiDB 默认使用乐观事务模型。也就是说,在执行 `UPDATE`、`INSERT`、`DELETE` 等语句时,只有在提交过程中才会检查写写冲突,而不是像 MySQL 一样使用行锁来避免写写冲突。类似的,诸如 `GET_LOCK()` 和 `RELEASE_LOCK()` 等函数以及 `SELECT .. FOR UPDATE` 之类的语句在 TiDB 和 MySQL 中的执行方式并不相同。所以业务端在执行 SQL 语句后,需要注意检查 `COMMIT` 的返回值,即使执行时没有出错,`COMMIT` 的时候也可能会出错。 - -## 事务限制 - -由于 TiDB 分布式两阶段提交的要求,修改数据的大事务可能会出现一些问题。因此,TiDB 特意对事务大小设置了一些限制以减少这种影响: - -* 单个事务包含的 SQL 语句不超过 5000 条(默认) -* 每个键值对不超过 6MB -* 键值对的总大小不超过 100MB - -## 基于事务模型的优化实践 - -由于 TiDB 中的每个事务都需要跟 PD leader 进行两次 round trip,TiDB 中的事务相比于 MySQL 中的事务延迟更高。以如下的 query 为例,用显式事务代替 `autocommit`,可优化该 query 的性能。 - -使用 `autocommit` 的原始版本: - -{{< copyable "sql" >}} - -```sql -UPDATE my_table SET a='new_value' WHERE id = 1; -UPDATE my_table SET a='newer_value' WHERE id = 2; -UPDATE my_table SET a='newest_value' WHERE id = 3; -``` - -优化后的版本: - -{{< copyable "sql" >}} - -```sql -START TRANSACTION; -UPDATE my_table SET a='new_value' WHERE id = 1; -UPDATE my_table SET a='newer_value' WHERE id = 2; -UPDATE my_table SET a='newest_value' WHERE id = 3; -COMMIT; -``` - -> **注意:** -> -> 由于 TiDB 中的资源是分布式的,TiDB 中单线程 workload 可能不会很好地利用分布式资源,因此性能相比于单实例部署的 MySQL 较低。这与 TiDB 中的事务延迟较高的情況类似。 diff --git a/v3.1/reference/transactions/transaction-optimistic.md b/v3.1/reference/transactions/transaction-optimistic.md new file mode 100644 index 000000000000..d84194c481ce --- /dev/null +++ b/v3.1/reference/transactions/transaction-optimistic.md @@ -0,0 +1,178 @@ +--- +title: TiDB 乐观事务模型 +summary: 了解 TiDB 的乐观事务模型。 +category: reference +aliases: ['/docs-cn/v3.1/reference/transactions/transaction-model/'] +--- + +# TiDB 乐观事务模型 + +本文介绍 TiDB 乐观事务的原理,以及相关特性。本文假定你对 [TiDB 的整体架构](/v3.1/architecture.md#tidb-整体架构)、[Percolator](https://www.usenix.org/legacy/event/osdi10/tech/full_papers/Peng.pdf) 事务模型以及事务的 [ACID 特性](/v3.1/glossary.md#acid)都有一定了解。 + +TiDB 默认使用乐观事务模型,不会出现读写冲突,所有的读操作都不会被写操作阻塞。对于写写冲突,只有在客户端执行 `COMMIT` 时,才会触发两阶段提交并检测是否存在写写冲突。 + +> **注意:** +> +> 自 v3.0.8 开始,TiDB 默认使用[悲观事务模型](/v3.1/reference/transactions/transaction-pessimistic.md)。但如果从 3.0.7 及之前的版本升级到 >= 3.0.8 的版本,不会改变默认事务模型,即**只有新创建的集群才会默认使用悲观事务模型**。 + +## 乐观事务原理 + +TiDB 中事务使用两阶段提交,流程如下: + +![TiDB 中的两阶段提交](/media/2pc-in-tidb.png) + +1. 客户端开始一个事务。 + + TiDB 从 PD 获取一个全局唯一递增的版本号作为当前事务的开始版本号,这里定义为该事务的 `start_ts` 版本。 + +2. 客户端发起读请求。 + + 1. TiDB 从 PD 获取数据路由信息,即数据具体存在哪个 TiKV 节点上。 + 2. TiDB 从 TiKV 获取 `start_ts` 版本下对应的数据信息。 + +3. 客户端发起写请求。 + + TiDB 校验写入数据是否符合一致性约束(如数据类型是否正确、是否符合唯一索引约束等)。**校验通过的数据将存放在内存里。** + +4. 客户端发起 commit。 + +5. TiDB 开始两阶段提交,保证分布式事务的原子性,让数据真正落盘。 + + 1. TiDB 从当前要写入的数据中选择一个 Key 作为当前事务的 Primary Key。 + 2. TiDB 从 PD 获取所有数据的写入路由信息,并将所有的 Key 按照所有的路由进行分类。 + 3. TiDB 并发地向所有涉及的 TiKV 发起 prewrite 请求。TiKV 收到 prewrite 数据后,检查数据版本信息是否存在冲突或已过期。符合条件的数据会被加锁。 + 4. TiDB 收到所有 prewrite 响应且所有 prewrite 都成功。 + 5. TiDB 向 PD 获取第二个全局唯一递增版本号,定义为本次事务的 `commit_ts`。 + 6. TiDB 向 Primary Key 所在 TiKV 发起第二阶段提交。TiKV 收到 commit 操作后,检查数据合法性,清理 prewrite 阶段留下的锁。 + 7. TiDB 收到两阶段提交成功的信息。 + +6. TiDB 向客户端返回事务提交成功的信息。 + +7. TiDB 异步清理本次事务遗留的锁信息。 + +## 优缺点分析 + +通过分析 TiDB 中事务的处理流程,可以发现 TiDB 事务有如下优点: + +* 实现原理简单,易于理解。 +* 基于单实例事务实现了跨节点事务。 +* 锁管理实现了去中心化。 + +但 TiDB 事务也存在以下缺点: + +* 两阶段提交使网络交互增多。 +* 需要一个中心化的版本管理服务。 +* 事务数据量过大时易导致内存暴涨。 + +实际应用中,你可以[根据事务的大小进行针对性处理](/v3.1/reference/transactions/overview.md#事务大小),以提高事务的执行效率。 + +## 事务的重试 + +使用乐观事务模型时,在高冲突率的场景中,事务很容易提交失败。而 MySQL 内部使用的是悲观事务模型,在执行 SQL 语句的过程中进行冲突检测,所以提交时很难出现异常。为了兼容 MySQL 的悲观事务行为,TiDB 提供了重试机制。 + +### 重试机制 + +当事务提交后,如果发现冲突,TiDB 内部重新执行包含写操作的 SQL 语句。你可以通过设置 `tidb_disable_txn_auto_retry = off` 开启自动重试,并通过 `tidb_retry_limit` 设置重试次数: + +```sql +# 设置是否禁用自动重试,默认为 “on”,即不重试。 +tidb_disable_txn_auto_retry = off +# 控制重试次数,默认为 “10”。只有自动重试启用时该参数才会生效。 +# 当 “tidb_retry_limit= 0” 时,也会禁用自动重试。 +tidb_retry_limit = 10 +``` + +你也可以修改当前 Session 或 Global 的值: + +- Session 级别设置: + + {{< copyable "sql" >}} + + ```sql + set @@tidb_disable_txn_auto_retry = off; + ``` + + {{< copyable "sql" >}} + + ```sql + set @@tidb_retry_limit = 10; + ``` + +- Global 级别设置: + + {{< copyable "sql" >}} + + ```sql + set @@global.tidb_disable_txn_auto_retry = off; + ``` + + {{< copyable "sql" >}} + + ```sql + set @@global.tidb_retry_limit = 10; + ``` + +> **注意:** +> +> `tidb_retry_limit` 变量决定了事务重试的最大次数。当它被设置为 0 时,所有事务都不会自动重试,包括自动提交的单语句隐式事务。这是彻底禁用 TiDB 中自动重试机制的方法。禁用自动重试后,所有冲突的事务都会以最快的方式上报失败信息 (`try again later`) 给应用层。 + +### 重试的局限性 + +TiDB 默认不进行事务重试,因为重试事务可能会导致更新丢失,从而破坏[可重复读的隔离级别](/v3.1/reference/transactions/transaction-isolation.md)。 + +事务重试的局限性与其原理有关。事务重试可概括为以下三个步骤: + +1. 重新获取 `start_ts`。 +2. 重新执行包含写操作的 SQL 语句。 +3. 再次进行两阶段提交。 + +第二步中,重试时仅重新执行包含写操作的 SQL 语句,并不涉及读操作的 SQL 语句。但是当前事务中读到数据的时间与事务真正开始的时间发生了变化,写入的版本变成了重试时获取的 `start_ts` 而非事务一开始时获取的 `start_ts`。因此,当事务中存在依赖查询结果来更新的语句时,重试将无法保证事务原本可重复读的隔离级别,最终可能导致结果与预期出现不一致。 + +如果业务可以容忍事务重试导致的异常,或并不关注事务是否以可重复读的隔离级别来执行,则可以开启自动重试。 + +## 冲突检测 + +乐观事务下,检测底层数据是否存在写写冲突是一个很重要的操作。具体而言,TiKV 在 prewrite 阶段就需要读取数据进行检测。为了优化这一块性能,TiDB 集群会在内存里面进行一次冲突预检测。 + +作为一个分布式系统,TiDB 在内存中的冲突检测主要在两个模块进行: + +- TiDB 层。如果发现 TiDB 实例本身就存在写写冲突,那么第一个写入发出后,后面的写入已经清楚地知道自己冲突了,无需再往下层 TiKV 发送请求去检测冲突。 +- TiKV 层。主要发生在 prewrite 阶段。因为 TiDB 集群是一个分布式系统,TiDB 实例本身无状态,实例之间无法感知到彼此的存在,也就无法确认自己的写入与别的 TiDB 实例是否存在冲突,所以会在 TiKV 这一层检测具体的数据是否有冲突。 + +其中 TiDB 层的冲突检测可以根据场景需要选择打开或关闭,具体配置项如下: + +```toml +# 事务内存锁相关配置,当本地事务冲突比较多时建议开启。 +[txn-local-latches] +# 是否开启内存锁,默认为 false,即不开启。 +enabled = false +# Hash 对应的 slot 数,会自动向上调整为 2 的指数倍。 +# 每个 slot 占 32 Bytes 内存。当写入数据的范围比较广时(如导数据), +# 设置过小会导致变慢,性能下降。(默认为 2048000) +capacity = 2048000 +``` + +配置项 `capacity` 主要影响到冲突判断的正确性。在实现冲突检测时,不可能把所有的 Key 都存到内存里,所以真正存下来的是每个 Key 的 Hash 值。有 Hash 算法就有碰撞也就是误判的概率,这里可以通过配置 `capacity` 来控制 Hash 取模的值: + +* `capacity` 值越小,占用内存小,误判概率越大。 +* `capacity` 值越大,占用内存大,误判概率越小。 + +实际应用时,如果业务场景能够预判断写入不存在冲突(如导入数据操作),建议关闭冲突检测。 + +相应地,在 TiKV 层检测内存中是否存在冲突也有类似的机制。不同的是,TiKV 层的检测会更严格且不允许关闭,仅支持对 Hash 取模值进行配置: + +```toml +# scheduler 内置一个内存锁机制,防止同时对一个 Key 进行操作。 +# 每个 Key hash 到不同的 slot。(默认为 2048000) +scheduler-concurrency = 2048000 +``` + +此外,TiKV 支持监控等待 latch 的时间: + +![Scheduler latch wait duration](/media/optimistic-transaction-metric.png) + +当 `Scheduler latch wait duration` 的值特别高时,说明大量时间消耗在等待锁的请求上。如果不存在底层写入慢的问题,基本上可以判断该段时间内冲突比较多。 + +## 更多阅读 + +- [Percolator 和 TiDB 事务算法](https://pingcap.com/blog-cn/percolator-and-txn/) \ No newline at end of file diff --git a/v3.1/reference/transactions/transaction-pessimistic.md b/v3.1/reference/transactions/transaction-pessimistic.md index dcba48a52a52..2bebc1a600e7 100644 --- a/v3.1/reference/transactions/transaction-pessimistic.md +++ b/v3.1/reference/transactions/transaction-pessimistic.md @@ -1,12 +1,12 @@ --- -title: TiDB 悲观事务模式 +title: TiDB 悲观事务模型 +summary: 了解 TiDB 的悲观事务模型。 category: reference --- -# TiDB 悲观事务模式 +# TiDB 悲观事务模型 -TiDB 默认使用乐观事务模式,存在事务提交时因为冲突而失败的问题。为了保证事务的成功率,需要修改应用程序,加上重试的逻辑。 -悲观事务模式可以避免这个问题,应用程序无需添加重试逻辑,就可以正常执行。 +TiDB 的乐观事务模型会导致事务提交时因为冲突而失败。为了保证事务的成功率,需要修改应用程序,加上重试的逻辑。悲观事务模式可以避免这个问题,应用程序无需添加重试逻辑,就可以正常执行。 ## 悲观事务模式的行为