数据库底层 - 2

锁的类别

全局锁

锁整个数据库。适合全库备份等场景。

表锁

锁整张表。适合读多写少,全表扫描或表结构更改的场景。InnoDB 中无索引的 updatedelete 可能会导致锁升级为表锁。

意向锁

表示事务打算对表中某些数据加锁,但不会直接锁定。比如在实行 select for update 操作时,会先在表上加一个意向锁,然后在目标行上加排他锁。该锁由 InnoDB 自动管理。当事务需要添加行锁时,会先在表上添加意向锁。这样当要添加表锁时,可以通过查看表上的意向锁,快速判断是否有冲突。

意向锁主要用于协同其他锁进行工作。避免为了添加表锁而不得不扫描全表的情况。而共享锁允许多个事务同时加从而读取同一数据。但排他锁和其他共享锁或排他锁均互斥。

当前持有的锁 X(排他锁) S(共享锁) IX(意向排他) IS(意向共享)
X
S
IX
IS

还存在 SIX 这种“意向共享 + 排他锁”,用于“既要读整张表,又要修改某些行”,与其他的锁都互斥。

行锁

行锁的底层是通过给索引加锁实现的,InnoDB 只有在通过索引条件检索数据时,才能使用行级锁。

  • 记录锁
    这是行锁最基本的表现形式,当我们使用唯一索引或主键索引进行等值查询时,MySQL会为该记录自动添加排他锁,禁止其他事务读取或修改锁定记录

  • 间隙锁
    进行范围查询时,锁定记录间的“间隙”(无数据的区间),防止其他事务在该范围内插入新纪录,防止幻读

    在执行for update lock in share mode等加锁语句,且查询条件是范围查询时,就会自动加间隙锁

  • 临键锁
    这是前两者的结合体,锁索引记录和记录之间的空隙。

    该锁的间隙是一个左闭右闭=开区间。MySQL默认的行锁就是临键锁。

使用命令 show engine innodb status 可以查看死锁信息

事务

事务遵循 ACID 特性,即原子性、一致性、隔离性和持久性。

  • 原子性 Atomicity
    要么全部完成,要么全部不完成。一般通过 undo log 实现

  • 一致性 Consistency
    确保事务从一个一致的状态转移到另一个一致的状态。

    例如银行的转账中,无论转没转成功,最后转账双方的总金额应该是不变的。

  • 隔离性 Isolation
    并发执行的事务是彼此隔离的,一个事务的执行不会被其他的事务干扰。
    隔离级别详情需要看下面内容

  • 持久性 Durability
    事务一旦提交,它对数据所做的更改就是永久性的。一般通过redo log实现。

    一般一个数据页大小为16kb.如果发生崩溃,可能会导致数据页不完整。为了解决值写入部分的问题,MySQL采用了双写机制,脏盘刷页时,先将数据页写入到一个双写缓冲区内,这是一个2M的连续空间,然后再将其写入到磁盘的实际位置。

事务一般有如下阶段

  • 活动阶段:事务开始执行。
  • 部分提交状态:最终语句执行完毕后,但还没有 commit 的状态。
  • 失败状态:发现无法正常执行后。
  • 终止状态:事务回滚后,数据库恢复至事务开始前的状态。中止后一般会重启事务或取消事务。
  • 提交状态:事务执行成功。

隔离级别

SQL标准定义了四种事务隔离级别,用来平衡事务的隔离性和并发性能

  • 读取未提交,允许读取尚未提交的数据变更。
  • 读取已提交,允许读取已提交的数据。Oracle或SQL Server默认的隔离级别。
  • 可重复读,对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己修改。MySQL InnoDB 的默认隔离级别
  • 可串行化,所有事务依次逐个执行,这样事务间就不会造成干扰。
隔离级别 脏读 不可重复读 幻读
读取未提交
读取已提交
可重复读 是,但在InnoDB中很难发生
可串行化

不同的隔离级别可能会产生不同的不一致问题

  • 脏读,及一个事务读到了另一个尚未提交的事务修改的数据。该数据可能会回滚,因此可能根本不存在。那么第一个事务读到的数据就是“脏”的。
  • 不可重复读,在同一个事务中,两次读取同一条记录,得到的结果不一致。也就是说,在事务执行期间,它正在处理的数据被其他已提交的事务修改了。
  • 幻读,在同一个事务中,两次执行相同的条件查询,第二次查询看到了第一次查询没看到的新的“幻影”行。也就是说,在事务执行期间,有其他 已提交的事务插入了新的、满足查询条件的记录。

不可重复读的重点在于修改了已有的行,幻读的重点在于新增了原本不存在的行。

在可串行化调度中,多个事务的并发执行结果为这些事务按某种次序串行化执行的结果。要实现这个效果,就必须满足两段锁协议 (2PL)

  1. 获得封锁,在对任何数据进行读写操作前,事务首先要获得对该数据的封锁。
  2. 释放封锁,在释放一个封锁后,事务不再申请和获得任何其他封锁。

MVCC

MVCC机制全称为 Multi-Version Concurrency Control (多版本并发控制)。它是通过在每个数据行上维护多个版本的数据来实现的。

当一个事务执行读操作时,它会使用快照读取,读取该事务开始前的数据快照,这样,读操作就不会被写操作阻塞,反之亦然。

读操作分为两种:当前读和快照读。

  • 快照读指事务在执行select查询时,InnoDB不会直接读取当前最新数据,而是根据事务开始时生成的快照去判断每条记录的可见性,从而读取符合条件的历史版本
  • 当前读指事务在进行更新、插入和删除操作时,其本质上也要进行读操作。为了保证修改的是最新的数据,并防止并发冲突,InnoDB必须读取最新版本的数据并加锁。

示意图

在不同的隔离级别下

  • 读已提交
    每一次执行select语句时,都会重新生成一个新的快照。因此它能读取到最新已提交的数据,避免了脏读,但无法避免不可重复读。
  • 可重复读
    只有第一次执行select语句时才会生成一个快照,并且这个快照贯穿整个事务的始终。因此事务中后续的所有读操作都是在同一个快照下进行,避免了不一致问题。
  • 其他的级别不需要MVCC机制

MVRC

Oracle 中使用的多版本控制工具。

修改数据时,旧数据存入回滚段(Undo Segment),数据行头记录事务信息。
查询时:

  • 若数据未被修改,则直接读
  • 数据被修改且事务未提交 / 在查询后提交 → 从回滚段重建历史版本

因此 Oracle 默认隔离级别为读已提交。

高可用

读写分离

可以对数据库的读写操作分散到不同的节点上,一个节点为主,负责写操作,多个从。负责读操作。应用层通过中间件(小型项目自己处理,中型项目通过spring + 多数据源插件,AOP 注解自动路由,大型项目通过中间件,如MyCat、Atlas、shardingsphere 或 MySQL Router)进行代理,自动路由请求。

主库将数据变更通过binlog同步到从库,从而保证数据一致性。

  1. 主库将数据库中数据的变化写入binlog
  2. 从库链接主库
  3. 从库会创建一个IO线程向主库请求更新的binlog
  4. 主库会创建一个binlog dump线程来发送binlog,从库中的IO线程负责接收
  5. 从库的IO线程将接收的binlog写入到relay log中
  6. 从库的SQL线程读取relay log同步数据到本地,即再执行一遍SQL

但是在接收binlog的过程中可能会造成主从延迟,对某些业务场景可能无法容忍,一般有三种解决方案。

  • 对一致性高的数据走主库查询
  • 对非关键业务允许短暂数据不一致,在业务端阻塞
  • 采用半同步复制,主库在事务提交时,要等至少一个库确认收到binlog(但不要求执行完成)才算提交成功

造成主从延迟的原因可能有很多种,例如从库机器性能比主库差,从库处理请求太多,事务过大,从库太多,网络延迟,等等。

分库分表

分库分两种

  • 水平分库,一个表按一定的规则拆分到不同的库中
  • 垂直分库,按照业务模块将不同的表拆分到不同的库中

分表也可以垂直分表或水平分表。其中水平分表一般在单表数据达到500万级,或者数据库占用空间过大时,就可以考虑。

数据的分片有以下几种方案

  • 哈希分片:求指定分片键的哈希,然后根据哈希值确定数据应该被放置在哪个表中
    适合随机读写的场景,但对动态伸缩(动态新增表或库)不友好
  • 范围分片:按照特定的范围区间来分配数据
    适合经常进行范围查找且数据分布均匀的场景,不适合随机读写的场景
  • 映射表分片(路由分片):使用一个单独的映射表来存储分片键和分片位置的对应关系。映射表的策略可以支持任何类型的分片算法
    可以灵活调整分片规则,但需要维护额外的表,增加了查询的开销和复杂度

可以使用ShardingSphere插件来实现分库分表。

不停机扩容分为三个阶段

  1. 新旧库同时写入,确保数据实时同步;可以借助消息队列实现异步补偿,幂等避免重复写入。读操作仍然走旧库
  2. 通过某些脚本将旧库的历史同步到新库,关键业务在查询时同时查询新旧库,进行数据校验,确保一致性
  3. 确保新库数据一致性后,逐步将读请求切换到新库,然后下线旧库

分库分表不是分得越细越好,进行分库分表后

  • 同一个数据库中的表分布在了不同的数据库中,导致无法使用join联表查询
  • 若单个操作涉及多个数据库,那么数据库自带的事务就无法满足需求了,必须引入分布式事务
  • 自增ID容易引发冲突,需要引入分布式ID,使用全局唯一方案

数据库底层 - 2
https://ivanclf.github.io/2025/10/25/sql-3/
作者
Ivan Chan
发布于
2025年10月25日
许可协议