MySQL-索引和锁


三、索引

索引的出现其实就是为了提高数据查询的效率,就像书的目录一样。

3.1 InnoDB的索引模型

  • 在 InnoDB 中,表都是根据主键顺序以索引的形式存放的,这种存储方式的表称为索引组织表
  • InnoDB 使用了 B+ 树索引模型,所以数据都是存储在 B+ 树的。
  • 每一个索引在 InnoDB 里面对应一棵 B+ 树。
  • B+ 树能够很好地配合磁盘的读写特性,减少单次查询的磁盘访问次数。
  • 根据叶子节点的内容,索引类型分为主键索引和非主键索引。
    • 主键索引的叶子节点存的是整行数据。在 InnoDB 里,主键索引也被称为聚簇索引(clustered index)。
    • 非主键索引的叶子节点内容是主键的值。在 InnoDB 里,非主键索引也被称为二级索引(secondary index)。

实际上,一般实际使用过程中,B+树几乎不会出现超过四层的情况,因为我们知道,一个数据页有16KB,即使假设每个记录需要160字节,一个数据页可以存放100条数据,而目录页需要存放的数据大小更小,假设一个目录页可以存放1000个数据。那么四层B+数可以存放的记录数就是:100 * 1000 * 1000 * 1000 = 1000,0000,0000 。

1000亿条记录!!!这就意味着,我们查找一次数据,最多需要4次磁盘I/O操作(加载4个数据页)。

基于主键索引和普通索引的查询有什么区别?

  • 如果语句是 select * from T where ID = 500,即主键查询方式,则只需要搜索 ID 这棵 B+ 树。
  • 如果语句是 select * from T where k = 5,即普通索引查询方式,则需要先搜索 k 索引树,得到 ID 的值为 500,再到 ID 索引树搜索一次。这个过程称为回表。
  • 基于主键索引和普通索引的查询有什么区别?
    • 如果语句是 select * from T where ID = 500,即主键查询方式,则只需要搜索 ID 这棵 B+ 树;
    • 如果语句是 select * from T where k = 5,即普通索引查询方式,则需要先搜索 k 索引树,得到 ID 的值为 500,再到 ID 索引树搜索一次。这个过程称为回表。
  • 也就是说,基于非主键索引的查询需要多扫描一棵索引树。因此,在应用中应该尽量使用主键查询。

3.2 索引维护

  • B+ 树为了维护索引有序性,在插入新值或删除旧值的时候需要做必要的维护。
  • 插入新值时,若是插入在中间的数据,需要在上挪动后面的数据,空出位置。如果数据页满了的话,就会申请新的数据页,挪动部分数据过去,这个过程称为页分裂。这个过程性能会受到影响,而且原本放在一个页的数据,现在分到两个页中,整体空间利用率降低大约 50%。
  • 删除旧值时,若相邻两个也由于删除了数据,利用率很低了之后,会对数据页做合并处理,也会影响性能。

这就引入一个问题:为什么要尽量用自增主键?

  • 插入新数据时不会影响其它记录。自增主键的插入数据模式,正符合了我们前面提到的递增插入的场景。每次插入一条新记录,都是追加操作,都不涉及到挪动其他记录,也不会触发叶子节点的分裂。
  • 有业务逻辑的字段做主键,则往往不容易保证有序插入,这样写数据成本相对较高。
  • 主键长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小

什么场景适合使用业务字段直接做主键呢?

  • 只有一个索引,该索引必须是唯一索引。
  • 这就是典型的 KV 场景。由于没有其他索引,所以也就不用考虑其他索引的叶子节点大小的问题。
  • 因为要尽量使用主键查询,直接将这个索引设置为主键,可以避免每次查询需要搜索两棵树。

3.3 最左前缀原则

B+ 树这种索引结构,可以利用索引的“最左前缀”,来定位记录。

例子:

拿市民信息表为里,增加一个(姓名,年龄)的联合索引。

可以看到,索引项是按照索引定义里面出现的字段顺序排序的。

当逻辑需求是查到所有名字是“张三”的人时,可以快速定位到 ID4,然后向后遍历得到所有需要的结果。

如果要查的是所有名字第一个字是“张”的人,SQL 语句的条件是 “where name like ‘张%’”。这时,你也能够用上这个索引,查找到第一个符合条件的记录是 ID3,然后向后遍历,直到不满足条件为止。

可以看到,不只是索引的全部定义,只要满足最左前缀,就可以利用索引来加速检索。这个最左前缀可以是联合索引的最左 N 个字段,也可以是字符串索引的最左 M 个字符。

在建立联合索引的时候,如何安排索引内的字段顺序?

  • 索引的复用能力。因为可以支持最左前缀,所以当已经有了 (a, b) 这个联合索引后,一般就不需要单独在 a 上建立索引了。
  • 可以减少维护的索引。第一原则是,如果通过调整顺序,可以少维护一个索引,那么这个顺序往往就是需要优先考虑采用的。
  • 高频需求。如果有根据某个字段查询联合索引中的另一个字段的高频需求,自然是条件需要的字段在左边。
  • 空间。比如上面这个市民表的情况,姓名字段是比年龄字段大的 ,那我就建议你创建一个(姓名,年龄)的联合索引和一个(年龄)的单字段索引。

3.4 索引下推

索引下推通过减少回表的次数,来提高数据库的查询效率。

例子:

还是以市民表的联合索引 (name, age) 为例。如果现在有一个需求:检索出表中“名字第一个字是张,而且年龄是 10 岁的所有男孩”。那么,SQL 语句是这么写的:

select * from tuser where name like '张%' and age=10 and ismale=1;

执行流程:

  1. 根据前缀索引规则,所以这个语句在搜索索引树的时候,只能用 “张”,找到第一个满足条件的记录 ID3 。当然,这还不错,总比全表扫描要好。
  2. 然后判断其它条件是否满足。
  3. 在 MySQL 5.6 之前,只能从 ID3 开始一个个回表。到主键索引上找出数据行,再对比字段值。

而 MySQL 5.6 引入的索引下推优化(index condition pushdown),可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。

在图中每一个虚线箭头表示回表一次。

在 MySQL 5.6 之前的图中,在这个过程 InnoDB 并不会去看 age 的值,只是按顺序把 name 第一个字是张的记录一条条取出来回表。因此,需要回表 4 次。

在 MySQL 5.6 引入的索引下推优化的图中,InnoDB 在 (name,age) 索引内部就判断了 age 是否等于 10 ,对于不等于 10 的记录,直接判断并跳过。在我们的这个例子中,只需要对 ID4、ID5 这两条记录回表取数据判断,就只需要回表 2 次。

3.5 普通索引和唯一索引的选择

3.5.1 两种索引的查询比较例子

create table T (
  id int primary key,
  k int not null,
  name varchar(16),
   index (k)
) engine=InnoDB;

假设字段 k 上的值都不重复, 执行查询的语句是 select id from T where k = 5。

  • 这个查询语句在索引树上查找的过程,先是通过 B+ 树从树根开始,按层搜索到叶子节点,也就是图中右下角的这个数据页,然后可以认为数据页内部通过二分法来定位记录。
  • 对于普通索引来说,查找到满足条件的第一个记录 (5, 500) 后,需要查找下一个记录,直到碰到第一个不满足 k = 5条件的记录。
  • 对于唯一索引来说,由于索引定义了唯一性,查找到第一个满足条件的记录后,就会停止继续检索。

InnoDB 的数据是按数据页为单位来读写的。也就是说,当需要读一条记录的时候,并不是将这个记录本身从磁盘读出来,而是以页为单位,将其整体读入内存。在 InnoDB 中,每个数据页的大小默认是 16 KB。

因为引擎是按页读写的,所以说,当找到 k = 5 的记录的时候,它所在的数据页就都在内存里了。那么,对于普通索引来说,要多做的那一次“查找和判断下一条记录”的操作,就只需要一次指针寻找和一次计算。

当然,如果 k = 5 这个记录刚好是这个数据页的最后一个记录,那么要取下一个记录,必须读取下一个数据页,这个操作会稍微复杂一些。

这个不同索引带来的性能差距微乎其微。

3.5.2 两种索引的更新比较

change buffer

  • 当需要更新一个数据页时,如果数据页在内存中就直接更新,而如果这个数据页还没有在内存中的话,在不影响数据一致性的前提下,InooDB 会将这些更新操作缓存在 change buffer 中,这样就不需要从磁盘中读入这个数据页了。
  • 在下次查询需要访问这个数据页的时候,将数据页读入内存,然后执行 change buffer 中与这个页有关的操作。通过这种方式就能保证这个数据逻辑的正确性。
  • 需要说明的是,虽然名字叫作 change buffer,实际上它是可以持久化的数据。也就是说,change buffer 在内存中有拷贝,也会被写入到磁盘上。
  • 将 change buffer 中的操作应用到原数据页,得到最新结果的过程称为 merge。除了访问这个数据页会触发 merge 外,系统有后台线程会定期 merge。在数据库正常关闭(shutdown)的过程中,也会执行 merge 操作。
  • 显然,如果能够将更新操作先记录在 change buffer,减少读磁盘,语句的执行速度会得到明显的提升。而且,数据读入内存是需要占用 buffer pool 的,所以这种方式还能够避免占用内存,提高内存利用率。
  • change buffer 用的是 buffer pool 里的内存,因此不能无限增大。change buffer 的大小,可以通过参数 innodb_change_buffer_max_size 来动态设置。这个参数设置为 50 的时候,表示 change buffer 的大小最多只能占用 buffer pool 的 50%。
  • 对于唯一索引来说,所有的更新操作都要先判断这个操作是否违反唯一性约束,而这必须要将数据页读入内存才能判断。如果都已经读入到内存了,那直接更新内存会更快,就没必要使用 change buffer 了。
  • 因此,唯一索引的更新就不能使用 change buffer,除了主键之外的非唯一索引才能用。

例子:

在这张表中插入一个新记录 (4, 400) 的话,InnoDB 的处理流程是怎样的。

  • 第一种情况是,这个记录要更新的目标页在内存中。这时,InnoDB 的处理流程如下:
    • 对于唯一索引来说,找到 3 和 5 之间的位置,判断到没有冲突,插入这个值,语句执行结束;
    • 对于普通索引来说,找到 3 和 5 之间的位置,插入这个值,语句执行结束。
  • 第二种情况是,这个记录要更新的目标页不在内存中。这时,InnoDB的处理流程如下:
    • 对于唯一索引来说,需要将数据页读入内存,判断到没有冲突,插入这个值,语句执行结束;
    • 对于普通索引来说,则是将更新记录在 change buffer,语句执行就结束了。

在第一种情况中,唯一索引多了一个判断,只会耗费微小的 CPU 时间,可以忽略不计。

在第二种情况中,将数据从磁盘读入内存涉及随机 IO 的访问,是数据库里面成本最高的操作之一。普通索引因为 change buffer 减少了随机磁盘访问,所以对更新性能的提升是会很明显的。

3.5.3 索引的选择和实践

通过上面的分析,已经清楚了使用 change buffer 对更新过程的加速作用,也清楚了 change buffer 只限于用在普通索引的场景下,而不适用于唯一索引。那么,现在有一个问题就是:普通索引的所有场景,使用 change buffer 都可以起到加速作用吗?

  • 因为 merge 的时候是真正进行数据更新的时刻,而 change buffer 的主要目的就是将记录的变更动作缓存下来,所以在一个数据页做 merge 之前,change buffer 记录的变更越多(也就是这个页面上要更新的次数越多),收益就越大。
  • 因此,对于写多读少的业务来说,页面在写完以后马上被访问到的概率比较小,此时 change buffer 的使用效果最好。这种业务模型常见的就是账单类、日志类的系统。
  • 反过来,假设一个业务的更新模式是写入之后马上会做查询,那么即使满足了条件,将更新先记录在 change buffer,但之后由于马上要访问这个数据页,会立即触发 merge 过程。这样随机访问 IO 的次数不会减少,反而增加了 change buffer 的维护代价。所以,对于这种业务模式来说,change buffer 反而起到了副作用。

实践建议:

  • 这两类索引在查询能力上是没差别的,主要考虑的是对更新性能的影响。所以建议尽量选择普通索引。
  • 如果所有的更新后面,都马上伴随着对这个记录的查询,那么你应该关闭 change buffer。而在其他情况下,change buffer 都能提升更新性能。
  • 在实际使用中,普通索引和 change buffer 的配合使用,对于数据量大的表的更新优化还是很明显的。
  • 特别地,在使用机械硬盘时,change buffer 这个机制的收效是非常显著的。所以,如果有一个类似“历史数据”的库,并且出于成本考虑用的是机械硬盘时,那应该特别关注这些表里的索引,尽量使用普通索引,然后把 change buffer 尽量开大,以确保这个“历史数据”表的数据写入速度。

3.5.4 小结

  • 由于唯一索引用不上 change buffer 的优化机制,因此如果业务可以接受,从性能角度出发建议优先考虑非唯一索引。
  • 如果“业务无法确保”,就要注意以下几点:
    • 首先,业务正确性优先。
    • “业务代码已经保证不会写入重复数据”的情况下,讨论性能问题。
    • 如果业务不能保证,或者业务就是要求数据库来做约束,那么必须创建唯一索引。
    • 所以如果碰上了大量插入数据慢、内存命中率低的时候,唯一索引用不上 change buffer 的优化机制是一个排查思路。在大量插入数据的操作下,唯一索引可能会降低库存命中率,堵塞住更新语句。因为唯一索引来说,需要将数据页读入内存,判断到没有冲突,再插入这个值。将数据从磁盘读入内存涉及随机IO的访问,是数据库里面成本最高的操作之一,会降低系统的更新性能。
    • 在一些“归档库”的场景,是可以考虑使用唯一索引的。比如,线上数据只需要保留半年,然后历史数据保存在归档库。这时候,归档数据已经是确保没有唯一键冲突了。要提高归档效率,可以考虑把表里面的唯一索引改成普通索引。

3.6 change buffer 和 redo log

change buffer 和 redo log 简单对比在提升性能上的收益如下:

  • redo log 主要节省的是随机写磁盘的 IO 消耗(转成顺序写)。
  • change buffer 主要节省的则是随机读磁盘的 IO 消耗。

3.7 MySQL 为什么会选择错索引

3.7.1 优化器的逻辑

优化器选择索引的目的是找到一个最优的执行方案,并用最小的代价去执行语句。

优化器选择索引的因素:

  • 扫描行数。扫描的行数越少,意味着访问磁盘数据的次数越少,消耗的 CPU 资源越少。
  • 是否使用临时表。
  • 是否排序。

因为种种情况,优化器有时候可能会选择错索引。

MySQL 怎么判断扫描行数?

  • MySQL 在真正开始执行语句之前,并不能精确地知道满足这个条件的记录有多少条,而只能根据统计信息来估算记录数。
  • 这个统计信息就是索引的“区分度”。显然,一个索引上不同的值越多,这个索引的区分度就越好。而一个索引上不同的值的个数,我们称之为“基数”(cardinality)。也就是说,这个基数越大,索引的区分度越好。
    • MySQL 采用采样统计的方式获取索引的基数。
    • InnoDB 默认会选择 N 个数据页,统计这些页面上的不同值,得到一个平均值,然后乘以这个索引的页面数,就得到了这个索引的基数。
    • 而数据表是会持续更新的,索引统计信息也不会固定不变。所以,当变更的数据行数超过 1 / M 的时候,会自动触发重新做一次索引统计。
    • 在MySQL中,有两种存储索引统计的方式,可以通过设置参数 innodb_stats_persistent 的值来选择:
      • 设置为 on 的时候,表示统计信息会持久化存储。这时,默认的 N 是 20,M 是 10。
      • 设置为 off 的时候,表示统计信息只存储在内存中。这时,默认的 N 是 8,M 是 16。
    • 由于是采样统计,所以不管N是20还是8,这个基数都是很容易不准的。
  • 使用 explain 可以看到 MySQL 的预计扫描行数 rows。
  • 使用非主键索引,每次从非主键索引,上拿到一个值,都要回到主键索引上查出整行数据,这个代价优化器也要算进去的。而直接使用主键索引扫描就没有额外代价。
  • analyze table t 可以帮助表重新做统计信息分析,可以把基数调准。

3.7.2 索引选择异常和处理

原本可以执行得很快的 SQL 语句,执行速度却比预期的慢很多,怎么办呢?

  1. 采用 force index 强行选择一个索引
    • MySQL 会根据词法解析的结果分析出可能可以使用的索引作为候选项,然后在候选列表中依次判断每个索引需要扫描多少行。如果 force index 指定的索引在候选索引列表中,就直接选择这个索引,不再评估其他索引的执行代价。
    • 但是如果索引改了名字,这个语句也得改,显得很麻烦。
    • 如果以后迁移到别的数据库的话,这个语法还可能会不兼容。
    • 使用 force index 最主要的问题还是变更的及时性。因为选错索引的情况还是比较少出现的,所以开发的时候通常不会先写上 force index。而是等到线上出现问题的时候,你才会再去修改 SQL 语句、加上 force index 。但是修改之后还要测试和发布,对于生产系统来说,这个过程不够敏捷。
  2. 可以考虑修改语句,引导 MySQL 使用我们期望的索引
  3. 在有些场景下,我们可以新建一个更合适的索引,来提供给优化器做选择,或删掉误用的索引
  4. 删除没有必要存在的索引

3.8 怎么给字符串字段加索引

索引选取的越长,占用的磁盘空间就越大,相同的数据页能放下的索引值就越少,搜索的效率也就会越低。

给字符串字段加索引可以使用的方法:

  1. 直接创建完整索引。可能比较占空间。
  2. 创建前缀索引。节省空间,但会增加查询扫描次数,并且不能使用覆盖索引。
    • 但是使用前缀索引,定义好长度,就可以做到既节省空间,又不用额外增加太多的查询成本。
    • 因为 InnoDB 不确定前缀索引的定义是否截断了完整信息,索引还是会回到主键索引查找相应的字符串字段是否符合,相当于还是需要回表,失去了覆盖索引的作用。
  3. 倒序存储,再创建前缀索引。用于绕过字符串本身前缀的区分度不够的情况,不支持范围扫描,而且也会有前缀索引的问题。
  4. 创建 hash 字段索引。查询性能稳定,有额外的存储和计算消耗,不支持范围扫描。

四、锁

根据加锁的范围,MySQL 里面的锁大致可以分成全局锁、表级锁和行锁三类。

4.1 全局锁

全局锁:就是对整个数据库实例加锁。

MySQL 全局读锁命令:Flush tables with read lock (FTWRL)。

注:使用这个命令之后,其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句

经典使用场景:全库逻辑备份(也就是把整库每个表都 select 出来存成文本)。

全库逻辑备份的做法:

  • 开启全局锁,然后对整个库做备份。(注:会影响业务)
  • 开启可重复读的隔离级别,使用官方的 mysqldump 工具。当 mysqldump 使用参数 –single-transaction 的时候,导数据之前就会启动一个事务,来确保拿到一致性视图。而由于 MVCC 的支持,这个过程中数据是可以正常更新的。(前提:引擎要支持这个隔离级别和事务,MyISAM 就不支持)

既然要全库只读,为什么不使用 set global readonly=true 的方式呢?

  1. readonly 的值可能另有它用。在有些系统中,readonly 的值会被用来做其他逻辑,比如用来判断一个库是主库还是备库。因此,修改 global 变量的方式影响面更大,不建议使用。
  2. 长时间的 readonly状态风险较大。在异常处理机制上有差异。如果执行 FTWRL 命令之后由于客户端发生异常断开,那么 MySQL 会自动释放这个全局锁,整个库回到可以正常更新的状态。而将整个库设置为 readonly 之后,如果客户端发生异常,则数据库就会一直保持 readonly 状态,这样会导致整个库长时间处于不可写状态,风险较高。
  3. readonly 对 super 权限的用户无效

4.2 表级锁

表级别的锁:

  • 表锁
    • 语法:
      • lock tables … read/write 上锁,会限制别的线程的读写,也限定了本线程接下来的操作对象。
      • unlock tables 释放锁,也可以在客户端断开的时候自动释放。
    • 例子:
      • 某个线程 A 中执行 lock tables t1 read, t2 write。则其他线程写 t1、读写 t2 的语句都会被阻塞。同时,线程 A 在执行 unlock tables 之前,也只能执行读 t1、读写 t2 的操作。连写 t1 都不允许,自然也不能访问其他表。
  • 元数据锁(meta data lock,MDL)
    • MDL 不需要显式使用,在访问一个表的时候会被自动加上。
    • 作用:保证读写的正确性。
    • 在 MySQL 5.5 版本中引入了 MDL,当对一个表做增删改查操作的时候,加 MDL 读锁;当要对表做结构变更操作的时候,加 MDL 写锁。
    • 读锁之间不互斥,因此你可以有多个线程同时对一张表增删改查。
    • 读写锁之间、写锁之间是互斥的,用来保证变更表结构操作的安全性。因此,如果有两个线程要同时给一个表加字段,其中一个要等另一个执行完才能开始执行。

4.3 行锁

行锁:就是针对数据表中行记录的锁。

  • MySQL 的行锁由各个引擎自己实现,并不是所有引擎都支持。
  • 不支持行锁意味着并发控制只能使用表锁,对于这种引擎的表,同一张表上任何时刻只能有一个更新在执行,这就会影响到业务并发度。InnoDB 是支持行锁的,而 MyISAM 不支持,这也是 MyISAM 被 InnoDB 替代的重要原因之一。

两阶段锁协议:在InnoDB事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。

  • 如果事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放。
  • 这样调整顺序可以最大程度减少事务之间的锁等待,提高了并发度。

4.4 死锁和死锁检测

死锁:当并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待的状态。

出现死锁后的策略:

  1. 直接进入等待,直到超时。这个超时时间可以通过参数 innodb_lock_wait_timeout 来设置。
  2. 发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑。

第一种策略存在的问题:

  1. 等待时间过长无法承受。在 InnoDB 中,innodb_lock_wait_timeout 的默认值是 50s,意味着当出现死锁以后,第一个被锁住的线程要过 50s 才会超时退出,然后其他线程才有可能继续执行。对于在线服务来说,这个等待时间往往是无法接受的。
  2. 等待时间过短容易解开错误的锁。又不可能直接把这个时间设置成一个很小的值,比如 1s。这样当出现死锁的时候,确实很快就可以解开,但如果不是死锁,而是简单的锁等待呢?所以,超时时间设置太短的话,会出现很多误伤。

第二种策略存在的问题:

  1. 死锁检测要耗费大量的 CPU 资源。每个新来的被堵住的线程,都要判断会不会由于自己的加入导致了死锁,这是一个时间复杂度是 O(n) 的操作。假设有 1000 个并发线程要同时更新同一行,那么死锁检测操作就是 100 万这个量级的。虽然最终检测的结果是没有死锁,但是这期间要消耗大量的 CPU 资源。因此,就会看到 CPU 利用率很高,但是每秒却执行不了几个事务。

怎么解决由热点更新导致的大量死锁检测而产生的性能问题?

  1. 如果能确保这个业务一定不会出现死锁,可以临时把死锁检测关掉。但是这种操作本身带有一定的风险,因为业务设计的时候一般不会把死锁当做一个严重错误,毕竟出现死锁了,就回滚,然后通过业务重试一般就没问题了,这是业务无损的。而关掉死锁检测意味着可能会出现大量的超时,这是业务有损的。
  2. 控制并发度。如果并发能够控制住,比如同一行同时最多只有 10 个线程在更新,那么死锁检测的成本很低,就不会出现这个问题。
    • 在客户端做并发控制:不可行。假设有 600 个客户端,这样即使每个客户端控制到只有 5 个并发线程,汇总到数据库服务端以后,峰值并发数也可能要达到 3000。
    • 在数据库服务端做并发控制:基本思路就是,对于相同行的更新,在进入引擎之前排队。这样在 InnoDB 内部就不会有大量的死锁检测工作了。
    • 将一行改成逻辑上的多行来减少锁冲突
      • 以影院账户为例,可以考虑放在多条记录上,比如10个记录,影院的账户总额等于这10个记录的值的总和。这样每次要给影院账户加金额的时候,随机选其中一条记录来加。这样每次冲突概率变成原来的1/10,可以减少锁等待个数,也就减少了死锁检测的CPU消耗。
      • 这个方案看上去是无损的,但其实这类方案需要根据业务逻辑做详细设计。如果账户余额可能会减少,比如退票逻辑,那么这时候就需要考虑当一部分行记录变成0的时候,代码要有特殊处理。

推荐的死锁解决办法:发起死锁检测

推荐的避免或减少死锁的办法:控制并发度,控制访问相同资源的并发事务量。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇