为什么要分库分表(设计高并发系统的时候,数据库层面该如何设计)?

日订单量达到100万单后,我们做了订单中心重构.pdf

分表

比如你单表都几千万数据了,你确定你能扛住么?绝对不行,单表数据量太大,会极大影响你的 sql 执行的性能,到了后面你的 sql 可能就跑的很慢了。一般来说,就以我的经验来看,单表到几百万的时候,性能就会相对差一些了,你就得分表了。

分表是啥意思?就是把一个表的数据放到多个表中,然后查询的时候你就查一个表。比如按照用户 id 来分表,将一个用户的数据就放在一个表中。然后操作的时候你对一个用户就操作那个表就好了。这样可以控制每个表的数据量在可控的范围内,比如每个表就固定在 200 万以内。

分库

分库是啥意思?就是你一个库一般我们经验而言,最多支撑到并发 2000,一定要扩容了,而且一个健康的单库并发值你最好保持在每秒 1000 左右,不要太大。那么你可以将一个库的数据拆分到多个库中,访问的时候就访问一个库好了。

这就是所谓的分库分表,为啥要分库分表?你明白了吧。

# 分库分表前 分库分表后
并发支撑情况 MySQL 单机部署,扛不住高并发 MySQL从单机到多机,能承受的并发增加了多倍
并发量高,磁盘IO使用情况(读、写) MySQL 单机磁盘容量几乎撑满 拆分为多个库,数据库服务器磁盘使用率大大降低
SQL CUP执行性能(函数移到应用服务器、临时表大数据量遍历慢) 单表数据量太大,SQL 越跑越慢 单表数据量减少,SQL 执行效率明显提升

用过哪些分库分表中间件?不同的分库分表中间件都有什么优点和缺点?

Sharding-jdbc

当当开源的,属于 client 层(项目直接依赖jar)方案,目前已经更名为 [ShardingSphere](https://github.com/apache/incubator-shardingsphere)(后文所提到的 Sharding-jdbc,等同于 ShardingSphere)。确实之前用的还比较多一些,因为 SQL 语法支持也比较多,没有太多限制。支持分库分表、读写分离、分布式 id 生成、柔性事务(最大努力送达型事务、TCC 事务)。而且确实之前使用的公司会比较多一些(这个在官网有登记使用的公司,可以看到从 2017 年一直到现在,是有不少公司在用的),目前社区也还一直在开发和维护,还算是比较活跃,个人认为算是一个现在也可以选择的方案

Mycat

属于 proxy 层(类似于中间件)方案,支持的功能非常完善,而且目前应该是非常火的而且不断流行的数据库中间件,社区很活跃,也有一些公司开始在用了。但是确实相比于 Sharding jdbc 来说,年轻一些,经历的锤炼少一些。

总结

Sharding-jdbc 这种 client 层方案的优点在于不用部署,运维成本低,不需要代理层的二次转发请求,性能很高
缺点但是如果遇到升级啥的需要各个系统都重新升级版本再发布,各个系统都需要耦合 Sharding-jdbc 的依赖;

Mycat 这种 proxy 层方案的缺点在于需要部署,自己运维一套中间件,运维成本高,但是好处在于对于各个项目是透明的,如果遇到升级之类的都是自己中间件那里搞就行了。

通常来说,这两个方案其实都可以选用,但是我个人建议中小型公司选用 Sharding-jdbc,client 层方案轻便,而且维护成本低,不需要额外增派人手,而且中小型公司系统复杂度会低一些,项目也没那么多;但是中大型公司最好还是选用 Mycat 这类 proxy 层方案,因为可能大公司系统和项目非常多,团队很大,人员充足,那么最好是专门弄个人来研究和维护 Mycat,然后大量项目直接透明使用即可

你们具体是如何对数据库如何进行垂直拆分或水平拆分的?

水平拆分的意思,就是把一个表的数据给弄到多个库的多个表里去,但是每个库的表结构都一样,只不过每个库表放的数据是不同的,所有库表的数据加起来就是全部数据。水平拆分的意义,就是将数据均匀放更多的库里,然后用多个库来扛更高的并发,还有就是用多个库的存储容量来进行扩容。

面试题 - 图1
垂直拆分的意思,就是把一个有很多字段的表给拆分成多个表或者是多个库上去。每个库表的结构都不一样,每个库表都包含部分字段。一般来说,会将较少的访问频率很高的字段放到一个表里去,然后将较多的访问频率很低的字段放到另外一个表里去。因为数据库是有缓存的,你访问频率高的行字段越少,就可以在缓存里缓存更多的行,性能就越好。这个一般在表层面做的较多一些。
面试题 - 图2
好了,无论分库还是分表,上面说的那些数据库中间件都是可以支持的。就是基本上那些中间件可以做到你分库分表之后,中间件可以根据你指定的某个字段值,比如说 userid,自动路由到对应的库上去,然后再自动路由到对应的表里去

你的项目里该如何分库分表?

一般来说,垂直拆分,你可以在表层面来做,对一些字段特别多的表做一下拆分;水平拆分,你可以说是并发承载不了,或者是数据量太大,容量承载不了,你给拆了,按什么字段来拆,你自己想好;分表,你考虑一下,你如果哪怕是拆到每个库里去,并发和容量都 ok 了,但是每个库的表还是太大了,那么你就分表,将这个表分开,保证每个表的数据量并不是很大。

分库分表的方式(水平拆分)

  • 一种是按照 range 来分,就是每个库一段连续的数据,这个一般是按比如时间范围来的,但是这种一般较少用,因为很容易产生热点问题,大量的流量都打在最新的数据上了。

  • 或者是按照某个字段 hash 一下均匀分散,这个较为常用。

range 来分,好处在于说,扩容的时候很简单,因为你只要预备好,给每个月都准备一个库就可以了,到了一个新的月份的时候,自然而然,就会写新的库了;缺点,但是大部分的请求,都是访问最新的数据。实际生产用 range,要看场景。

hash 分发,好处在于说,可以平均分配每个库的数据量和请求压力;坏处在于说扩容起来比较麻烦,会有一个数据迁移的过程,之前的数据需要重新计算 hash 值重新分配到不同的库或表。

现在有一个未分库分表的系统,未来要分库分表,如何设计才可以让系统从未分库分表动态切换到分库分表上?

停机迁移方案

网站或者app 挂个公告,0-6点进行运维,无法访问。

接着到0点停机,系统停掉,没有有流量写入了,此时老的单库单表数据库静止了。然后通过写好一个导数的一次性工具系统,然后直接启动连接到新的分库分表上去,开多台机器执行

修改服务的新的数据库地址配置信息,重新发布系统。然后验证一下,就完事了。如果有问题,没到3点重试,否则直接回滚到单表。第二天凌晨重试。

600万数据 可以通过 开3台机器(每个机器每小时大概可以迁移180万的数据量) 每台机器多线程开20个线程跑。

不停机双写迁移方案

简单来说,就是在线上系统里面,之前所有写库的地方,增删改操作,除了对老库增删改,都加上对新库的增删改,这就是所谓的双写,同时写俩库,老库和新库。

然后系统部署之后,新库数据差太远,用之前说的导数工具,跑起来读老库数据写新库,写的时候要根据 gmt_modified 这类字段判断这条数据最后修改的时间,除非是读出来的数据在新库里没有,或者是比新库的数据新才会写。简单来说,就是不允许用老数据覆盖新数据。

导完一轮之后,有可能数据还是存在不一致,那么就程序自动做一轮校验,比对新老库每个表的每条数据,接着如果有不一样的,就针对那些不一样的,从老库读数据再次写。反复循环,直到两个库每个表的数据都完全一致为止。

接着当数据完全一致了,就 ok 了,基于仅仅使用分库分表的最新代码,重新部署一次,不就仅仅基于分库分表在操作了么,还没有几个小时的停机时间,很稳。所以现在基本玩儿数据迁移之类的,都是这么干的。

如何设计可以动态扩容缩容的分库分表方案?

一开始上来就是 32 个库,每个库 32 个表,那么总共是 1024 张表。

我可以告诉各位同学,这个分法,第一,基本上国内的互联网肯定都是够用了,第二,无论是并发支撑还是数据量支撑都没问题。
每个库正常承载的写入并发量是 1000,那么 32 个库就可以承载 32 1000 = 32000 的写并发,如果每个库承载 1500 的写并发,32 1500 = 48000 的写并发,接近 5 万每秒的写入并发,前面再加一个MQ,削峰,每秒写入 MQ 8 万条数据,每秒消费 5 万条数据。

有些除非是国内排名非常靠前的这些公司,他们的最核心的系统的数据库,可能会出现几百台数据库的这么一个规模,128 个库,256 个库,512 个库。

1024 张表,假设每个表放 500 万数据,在 MySQL 里可以放 50 亿条数据。
每秒 5 万的写并发,总共 50 亿条数据,对于国内大部分的互联网公司来说,其实一般来说都够了

刚开始的时候,这个库可能就是逻辑库,建在一个数据库上的,就是一个 MySQL 服务器可能建了 n 个库,比如 32 个库。后面如果要拆分,就是不断在库和 MySQL 服务器之间做迁移就可以了。然后系统配合改一下配置即可。

比如说最多可以扩展到 32 个数据库服务器,每个数据库服务器是一个库。如果还是不够?最多可以扩展到 1024 个数据库服务器,每个数据库服务器上面一个库一个表。因为最多是 1024 个表。

这么搞,是不用自己写代码做数据迁移的,都交给 DBA 来搞好了,但是 DBA 确实是需要做一些库表迁移的工作,但是总比你自己写代码,然后抽数据导数据来的效率高得多吧。

哪怕是要减少库的数量,也很简单,其实说白了就是按倍数缩容就可以了,然后修改一下路由规则。

这里对步骤做一个总结:

  1. 设定好几台数据库服务器,每台服务器上几个库,每个库多少个表,推荐是 32 库 * 32 表,对于大部分公司来说,可能几年都够了。
  2. 路由的规则,orderId 模 32 = 库,orderId / 32 模 32 = 表
  3. 扩容的时候,申请增加更多的数据库服务器,装好 MySQL,呈倍数扩容,4 台服务器,扩到 8 台服务器,再到 16 台服务器。
  4. 由 DBA 负责将原先数据库服务器的库,迁移到新的数据库服务器上去,库迁移是有一些便捷的工具的。
  5. 我们这边就是修改一下配置,调整迁移的库所在数据库服务器的地址。
  6. 重新发布系统,上线,原先的路由规则变都不用变,直接可以基于 n 倍的数据库服务器的资源,继续进行线上系统的提供服务。

    分库分表之后,id 主键如何处理?

    数据库自增id

  • 通过单独的获取id服务器,获取id

优点:可以确保每个id是唯一的,
缺点:高并发的话,就会有瓶颈

适合的场景这种适合数据量大,但是并发量比较低的场景。

  • 设置数据库 sequence 或者表自增字段步长

比如说,现在有 8 个服务节点,每个服务节点使用一个 sequence 功能来产生 ID,每个 sequence 的起始 ID 不同,并且依次递增,步长都是 8。

适合的场景可以防止产生的ID重复时,这种方案实现起来比较简单,也能到达性能目标。但是服务节点固定,步长也固定,将来还要增加服务节点,就不好搞了。

UUID

好处就是本地生成,不要基于数据库来了;不好之处就是,UUID 太长了、占用空间大,作为主键性能太差了;更重要的是,UUID 不具有有序性,会导致 B+ 树索引在写的时候有过多的随机写操作(连续的 ID 可以产生部分顺序写),还有,由于在写的时候不能产生有顺序的 append 操作,而需要进行 insert 操作,将会读取整个 B+ 树节点到内存,在插入这条记录后会将整个节点写回磁盘,这种操作在记录占用空间比较大的情况下,性能下降明显。

适合的场景:如果你是要随机生成个什么文件名、编号之类的,你可以用 UUID,但是作为主键是不能用 UUID 的。

获取系统当前时间

这个就是获取当前时间即可,但是问题是,并发很高的时候,比如一秒并发几千,会有重复的情况,这个是肯定不合适的。基本就不用考虑了。

适合的场景:一般如果用这个方案,是将当前时间跟很多其他的业务字段拼接起来,作为一个 id,如果业务上你觉得可以接受,那么也是可以的。你可以将别的业务字段值跟当前时间拼接起来,组成一个全局唯一的编号。

snowflake 算法

snowflake 算法是 twitter 开源的分布式 id 生成算法,采用 Scala语言实现,id 是 64 位 long 型的。

  • 1 bit:不用,代表是正数
  • 41 bits:代表的是时间戳,单位是毫秒。换算成年大概是69年
  • 10 bits:记录工作机器id,最多可以不是 2^10 机器 1024,其中 5 bits 代表机房id 32个机房,5 bits 代表机器id 32台机器
  • 12 bits:这里用来记录同一个毫秒内产生的不同id,12 bits 可以代表的最大正整数是 2^12 - 1 = 4096 ,也就是说可以用这个 12 bits 代表的数字来区分同一个毫秒内的 4096 个不同的 id。

获取id流程:

首先自己搞一个服务,然后对每个机房的每个机器都初始化这么一个id,刚开始这个机房的这个机器的序号就是 0。然后每次接收到一个请求,说这个机房的这个机器要生成一个 id,你就找到对应的 Worker 生成。

通过 synchronized保证线程安全 获取 id ,41 bit是当前毫秒单位的一个时间戳,然后传进来的 32 以内的机房 id 32 以内的机器 id,

  1. 如果跟上次生成 id 的时间还在一个毫秒内,毫秒内存序列+1;毫秒内序列溢出,超过 4096 ,序列重置为 0,阻塞到下一个毫秒,重新获取时间戳。
  2. 如果发生了系统时间回调,也就是当前时间戳小于上一次的时间戳。
    1. 方案一是发现时钟回拨后,算出来回拨多少,保存为时间偏移量,然后后面每次获取时间戳都加上偏移量,每回拨一次更新一次偏移量
    2. 方案二是,只在第一次生成id或启动时获取时间戳并保存下来,每生成一个id,就计下数,每个毫秒数能生成的id数是固定的,到生成满了,再把时间戳加一,这样就不依赖于系统时间了,每个毫秒数的使用率也最高

优点:
高性能高可用:不依赖数据库,完全纯内存
容量大:每秒中能生成数百万的自增ID。
ID自增:存入数据库中,索引效率高。
缺点:
严重依赖系统时间,系统时间被回调,或者改变,可能会造成id冲突或者重复。

得物分布式id

主要解决了,多活时候,双相同步id冲突。
id服务器:
生产id,id的组合是 地区+时间戳+bizTag。
根据bizTag以及步长缓存一段id到redis中。比如 订单bizTag=1 缓存10000 个id,步长可以后台配置。每生产一段id,数据会记录一下目前的分布式id的下次起点。
客户端:直接调用id服务器。延迟很低,一般在2到3ms。

如何实现 MySQL 的读写分离?

就是基于主从复制架构,简单来说,就是搞一个主库,挂多个从库,然后我们就单单只是写主库,然后主库会自动把数据给同步到从库上去。

MySQL 主从复制原理的是啥?

主要原理

主库将变更写入 binlog 日志,然后从库连接到主库之后,从库有一个 IO 线程,将主库的 binlog 日志拷贝到自己本地,写入一个 relay 日志中。接着从库中有一个 SQL 线程从 relay日志读取 binlog,然后执行 binlog 日志中的内容,也就是在自己本地再次执行一遍 SQL,这样就可以保证自己跟主库的数据是一样的。

延时问题

这里有过一个非常重要的一点,就是从库同步主库数据的过程是串行化的,也就是说主库上并行的操作,在从库上会串行执行。由于从库从主库拷贝日志以及串行执行 SQL 的特点,在高并发场景下,从库的数据一定会比主库慢一些,是有延时的

压测过,大概 主库并发量 1000/s 延时 几毫秒,2000/s 延时 几十毫秒,当达到4000 , 5000/s,主库都快死了,此时从库 延时会达到几秒钟。

大概在 mysql5.6版本之后, 从库 IO线程,读取主库的binlog 日志的时候,也可以支持多线程读取。

宕机问题,对应的 半同步复制 和 并行复制

场景:当主库突然宕机,然后恰好数据还没有同步到从库,那么有些数据可能从库上是还没有的,有些数据可能就丢失了。

所以 MySQL 实际上在这一块有两个机制,一个是半同步复制,用来解决主库数据丢失问题;一个是并行复制,用来解决主从同步延时问题。

半同步复制

也就是 semi-sync 复制,指的是主库写入 bingo 日志之后吗,就会将强制此时立即将数据同步到从库,从库将日志写入自己本地的 relay log 之后,接着会返回一个 ack 主库,主库接收到至少一个从库的 ack 之后才会认为操作完成了。

并行复制

指的是从库开启多个 SQL 线程,并行读取 relay log 中不同 库的日志, 然后并发重放不同库的日志,这是库级别的并行。

MySQL 主从同步延时问题(精华)

场景:先插入一条数据,再把它查出来,然后更新这个条数据。在生产环境达到 2000/s,这个时候,主从复制延迟大概是小几十毫秒。那么一些数据,在高峰期时候没有更新。因为延时,导致查询从库不能立刻查到。(查询时候 没有数据为null ,更新 通过id =null,导致更新失败)

  • 重写代码,写代码的时候,插入之后,直接更新。不要查询再更新。
  • 如果必须要查询,插入之后,设置直连主库查询,再更新。(不推荐这种方法,这种读写分离的意义就丧失了)
  • 分库,将一个主库分为多个主库,每个主库的写并发就减少了几倍,此时主从延时可以忽略不计。
  • 打开 MySQL 支持的并发复制,多个库并行复制。但是这种,在高并发,某个库单库并发达到 2000/s,并发复制还是没有意义的。

    存储引擎

    myisam
    不支持事务,不支持外键约束,二级(复制)索引,索引文件和数据文件分开的。适用于那种少量的插入,大量的查询的场景。

innodb
支持事务,走聚簇索引,支持高并发,高可用等成熟的数据库架构。比如分库分表,读写分离,主备架构。

聚簇索引和二级索引

聚簇索引:叶子节点存的是完整的数据。像innodb的主键索用的就是聚簇索引。
二级索引:叶子节点存的是索引,如果是myisam存储引擎,他最终叶子节点存的还有地址值,他还要回表到一个hash表中找到具体的数据。innodb 他存的是索引数据和主键id,他要回表到主键索引,找到完整的数据。

索引结构

B- 树

  1. 根节点至少包含2个孩子。
  2. 每个节点最多含有m个孩子(m>=2,m 代表几阶,也就是每个节点最多可以存储几个关键字)
  3. 除了根节点和叶节点外,其他节点至少有 ceil(m/2)个孩子。
  4. 所有的叶子结点都在同一层。
  5. 非叶子结点存储数据(关键字)的个数=指向儿子的指针个数-1;
  6. 数据(关键字)集合分布在整棵树中,任何一个数据(关键字)出现且只出现在一个节点中。(数据分散,对磁盘读写更加随机,代价比较高)
  7. 搜索可能在非叶子节点结束。(搜索,不稳定)

    B+树

  8. 非叶子结点的子树指针和节点存储的索引个数相同,同时指针P[i]的区间在P[i] 到 P[i+1],前闭后开。(B树是前开,后开。)

  9. 相比B - 树, 所有叶子结点增加一个指针,指向下个一个叶子结点。(这样有利于对数据库的扫描,可以支持范围搜索)
  10. 所有关键字都在叶子结点。(1. 稳定的查找效率,每次都是从根节点找到叶子结点 2. 这样都同一在同一个板块存数据,这样磁盘读写代价更低。)

B+树好处:

  1. 每次从根节点查找,查找效率稳定。
  2. 叶子结点顺序相连,有利于数据库的扫描。
  3. 数据都存储在叶子结点,有利于磁盘的读写。

    索引优化

    排查慢sql

  • 慢查询的开启并捕获或开启全局日志
  • explain + 慢SQL分析

    合理使用索引

  • 主键自动建立唯一索引

  • 频繁作为查询条件的字段应该创建索引
  • 频繁更新的字段不适合创建索引—-因为每次更新不单单是更新了记录还会更新索引
  • 单键/组合索引的选择问题,who? (在高并发下倾向创建组合索引)
  • 组合索引,遵守最左匹配原则
  • 查询中排序的字段,排序字段若通过索引去访问将大大提高排序速度
  • 利用索引字符串值的前缀(如果是字符串,使用字符串前缀)
  • 尽量使用覆盖索引进行查询,避免回表带来的性能损耗。
  • 联合索引,like/范围查/存在函数无效

    索引优缺点

    索引优势

    IO成本优势

    类似大学图书馆建书目索引,提高数据检索的效率,降低数据库的IO成本

    CPU消耗低

    通过索引列对数据进行排序,降低数据排序的成本,降低了CPU的消耗

    索引缺点

    空间上的代价

    一个索引都为对应一棵B+树,树中每一个节点都是一个数据页,一个页默认会占用16KB的存储空间,所以一个索引也是会占用磁盘空间的。

    时间上的代价

    索引是对数据的排序,那么当对表中的数据进行增、删、改操作时,都需要去维护修改内容涉及到的B+树索引。所以在进行增、删、改操作时可能需要额外的时间进行一些记录移动,页面分裂、页面回收等操作来维护好排序。

    事务特性

    原子性
    一致性
    隔离性
    持久性

    事务的隔离级别

    隔离性存在的问题

    脏写:两个事务都更新一个数据,结果有个一人回滚了把另外一个人更新的数据也回滚没了。
    脏读:一个事务读到了另外一个事务没有提交的时候修改数据,结果另外一个事务回滚了,下次读就读不到了。
    不可重复读:就是多次读一条数据,别的事务老是修改数据值还提交了,多次读到的值不同。
    幻读:就是范围查询,每次查到的数据不同,有时候别的事务插入了新的值,就会读到更多的数据。

    几种隔离级别

    RU:可以读到别人没有提交的事务修改的数据,只能避免脏写。
    RC:可以读到人家提交的事务修改过的数据,避免的脏读、脏写。
    RR:不会读到别的已经提交事务修改的数据,避免了脏读,脏写,不可重复读。
    串行:让事务都串行执行,可以避免所有问题。

Mysql实现MVCC机制,是基于 undo log 多版本控制链+ReadView机制来做的,默认RR隔离级别,就可以避免以上所有问题。

undo log

每次开启事务,增删改操作都会记录一下当前事务的id,修改的值 同时会指向 上个事务的 undo log。

redo log、undo log 、 binlog 区别

  • redo log作用:重做日志

作用于InnoDB 确保事务的持久性。防止在发生故障的时间点,尚有脏页未写入磁盘,在重启mysql服务的时候,根据redo log进行重做,从而达到事务的持久性这一特性。

  • undo log 作用:回滚日志

作用于引擎层面 保存了事务发生之前的数据的一个版本,可以用于回滚,同时可以提供多版本并发控制下的读(MVCC),也即非锁定读

  • binlog 作用:二级制日志

作用于mysql server 用于复制,在主从复制中,从库利用主库上的binlog进行重播,实现主从同步。 用于数据库的基于时间点的还原。

ReadView

m_ids:当前所有未提交的事务id。
min_trx_id:当前未提交的最小的事务id。
max_trx_id:当前未提交的下一次的事务id。
creator_id:当前事务的id。

Mysql 在 RC情况下,避免了脏读,脏写,但是没有避免不可重复读
在一个事务里,每次查询都会重新开启 readView,当readView 有其他事务提交之后。下次再开启 readView,那么未提交的事务 ids 中就没有刚刚提交的事务id。那么顺着 undo log 版本链读取的时候,会读刚刚已经提交的事务。

RR情况,避免了 脏写,脏读,不可重复读,幻读。
在一个事务里,只会开启一个 readView,每次查询都会按照相同的 readView,这样读取 undo log 版本链得到的结果都一样。

避免脏写

通过锁机制,来避免的脏写。
当多个事务同时想对一条数据写的时候,第一个事务会创建一个锁,里面包含着自己的 事务id 和 等待状态 fasle,然后把锁跟这行事务关联在一起。
另一个事务过来想的时候,发现这条记录被加锁,那么他就会排队等待,同时他也生成一个锁,里面 包含自己的 事务id 和 等待状态为 true。
当第一个事务提交了,释放锁,他会唤醒排在他后面的锁,改为fasle。那么后面的事务就获取到锁了。

锁机制

读锁与写锁

  • 读锁:共享锁、Shared Locks、S锁。
  • 写锁:排他锁、Exclusive Locks、X锁。

    读操作

    select …lock in share mode; (显示共享锁)
    将查找到的数据加上一个S锁,允许其他事务继续获取这些记录的S锁,不能获取这些记录的X锁(会阻塞)

    select … for update; (显示排他锁)
    将查找到的数据加上一个X锁,不允许其他事务获取这些记录的S锁和X锁。

    写操作

  • DELETE:删除一条数据时,先对记录加X锁,再执行删除操作。

  • INSERT:插入一条记录时,会先加隐式锁来保护这条新插入的记录在本事务提交前不被别的事务访问到。
  • UPDATE

    • 如果被更新的列,修改前后没有导致存储空间变化,那么会先给记录加X锁,再直接对记录进行修改。
    • 如果被更新的列,修改前后导致存储空间发生了变化,那么会先给记录加X锁,然后将记录删掉,再Insert一条新记录。

      隐式锁:

  • 一个事务插入一条记录后,还未提交,这条记录会保存本次事务id,而其他事务如果想来读取这个记录会发现事务id不对应,会自己加一把锁,等待前面的事务提交之后,唤醒他解锁。所以相当于在第二个事务插入一条记录时,隐式的给这条记录加了一把隐式锁。

    悲观锁、乐观锁

  • 悲观锁: 用的就是数据库的行锁,认为数据库会发生并发冲突,直接上来就把数据锁住,其他事务不能修改,直至提交了当前事务。(比如加 for update。)

  • 乐观锁: 其实是一种思想,认为不会锁定的情况下去更新数据,如果发现不对劲,才不更新(回滚)。在数据库中往往添加一个version字段来实现。

    行锁范围分类

  • LOCK_REC_NOT_GAP: 单个行记录上的锁。行锁

  • LOCK_GAP:间隙锁,锁定一个范围,但不包括记录本身。GAP锁的目的,是为了防止同一事务的两次当前读 ,出现幻读的情况。(间隙锁会锁从你查的id到最近小的id之间会锁住)
  • LOCK_ORDINARY:(Next_Key) 锁定一个范围,并且锁定记录本身。对于行的查询,都是采用该方法,主要目的是解决幻读的问题。

    避免死锁

  • 尽量控制事务大小,减少锁定资源量和时间长度

  • 合理设计索引,尽量缩小锁的范围
  • 在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁概率
  • 尽可能低级别事务隔离