首页
社区
课程
招聘
[分享]数据库系统分类、数据模型、反规范化技术、并发操作问题、共享锁和排他锁、数据库封锁协议和封锁粒度、死锁预防和解除
发表于: 57分钟前 30

[分享]数据库系统分类、数据模型、反规范化技术、并发操作问题、共享锁和排他锁、数据库封锁协议和封锁粒度、死锁预防和解除

57分钟前
30

AI模型:Deepseek


仅供参考


Let's Go!


=======================我是分割线=================================


数据库系统分类


按数据库系统的体系结构,通常可分为集中式、客户/服务器式、并行式和分布式四类。它们的主要区别在于:数据和处理逻辑放在哪些节点上、节点之间如何协作。


 1. 集中式数据库系统


特点:数据、DBMS、应用程序都集中在一台主机或服务器上,用户通过终端访问,终端本身不做数据处理。  


举例:  


- 早期银行主机系统:IBM 大型机上运行 DB2,柜员使用“哑终端”连接主机。  


- 单机版 MySQL/SQLite:某小公司在一台电脑上安装 SQLite,所有数据都存在本地文件中,应用程序也在这台电脑上运行。


 2. 客户/服务器式数据库系统


特点:数据库服务器负责数据管理、查询、事务和并发控制;客户端负责用户界面和部分业务逻辑,通过网络发送 SQL 请求。常见有两层 C/S 和三层 C/S。  


举例:  


- 超市收银系统:收银机安装客户端程序,后台服务器运行 SQL Server,收银机连接服务器完成商品查询和结账。  


- 学校教务系统:Web 应用服务器连接 MySQL/PostgreSQL 数据库,学生通过浏览器查询成绩。这属于三层客户/服务器结构。


 3. 并行式数据库系统


特点:使用多个处理器、多个磁盘或多个节点并行执行查询,以提高处理速度。常见结构有共享内存、共享磁盘、无共享等。  


举例:  


- Teradata 数据仓库:采用无共享 MPP 结构,多个节点并行扫描、连接和汇总海量数据。  


- Oracle RAC:多个数据库实例共享同一存储,并行处理事务和查询。  


- Greenplum:把一个大查询拆成多个子任务,在多个 Segment 节点上并行执行,常用于电信、电商的数据分析。


 4. 分布式数据库系统


特点:数据分布在多个地理或网络节点上,每个节点通常有一定自治能力,但对外可看作一个逻辑整体,支持全局事务和数据透明访问。  


举例:  


- Google Spanner:数据分布在全球多个数据中心,支持跨区域事务和强一致性。  


- TiDB、CockroachDB、OceanBase:数据自动分片、多副本存储,节点可分布在不同机房或城市。  


- 全国连锁银行系统:北京、上海、广州节点分别存储本地客户数据,跨地区转账由全局协调机制完成。


 简单对比


类型关键特点典型例子
集中式单机集中存储和处理单机 MySQL、SQLite、大型机 DB2
客户/服务器式客户端 + 数据库服务器SQL Server + 收银客户端、Web + MySQL
并行式多节点并行执行查询Teradata、Greenplum、Oracle RAC
分布式数据分布、多地自治、全局透明Spanner、TiDB、OceanBase、跨区银行系统


需要注意:并行式和分布式容易混淆。并行式通常在同一地点、紧耦合、由单一数据库系统统一管理,主要追求性能;分布式则更强调数据在地理上的分布、自治性和容灾能力。现实中很多系统是混合架构,例如分布式数据库内部也会采用并行执行技术。


=======================我是分割线=================================


数据模型


常用的基本数据模型主要有层次模型、网状模型、关系模型、面向对象模型。它们描述数据及其联系的方式不同。下面用“学生—课程—教师”这个常见场景来举例说明。


 1. 层次模型


特点:数据组织成树形结构,每个子节点只有一个父节点,适合表达一对多联系。  


典型系统:IBM IMS。  


例子:  


大学组织结构可以表示为:


学校
├── 计算机学院
│   ├── 软件工程系
│   │   ├── 学生张三
│   │   └── 学生李四
│   └── 计算机科学系
│       └── 学生王五
└── 外国语学院
    └── 英语系
        └── 学生赵六


在学生选课场景中,可以设计成:


学生张三
├── 选课记录:数据库
└── 选课记录:操作系统


但层次模型不擅长直接表达“一个学生选多门课,一门课被多个学生选”的多对多关系,通常需要冗余数据或引入虚拟节点。


 2. 网状模型


特点:数据组织成有向图,一个节点可以有多个父节点,能较自然地表达多对多联系。  


典型系统:CODASYL DBTG、IDMS。  


例子:  


学生和课程之间是多对多关系:


学生张三 ——选课——> 数据库
学生张三 ——选课——> 操作系统
学生李四 ——选课——> 数据库
教师王老师 ——授课——> 数据库
教师刘老师 ——授课——> 操作系统


这里“学生”和“课程”通过“选课”联系连接;“教师”和“课程”通过“授课”联系连接。一个学生可以连接多门课程,一门课程也可以连接多个学生。网状模型通过“系”来表达这种图状联系。


 3. 关系模型


特点:用二维表表示数据,行是元组,列是属性,通过主键和外键建立联系。  


典型系统:MySQL、Oracle、SQL Server、PostgreSQL。

  

例子:


学生选课系统可以设计为三张表:


学生表 Student


学号姓名专业
1001张三软件工程
1002李四计算机科学


课程表 Course


课程号课程名教师
C01数据库王老师
C02操作系统刘老师


选课表 SC


学号课程号成绩
1001C0190
1001C0285
1002C0188


其中,SC 表通过 学号 和 课程号 两个外键,把学生和课程的多对多关系拆成两个一对多关系。关系模型结构简单、查询方便,是目前最主流的数据库模型。


 4. 面向对象模型


特点:把数据和对数据的操作封装成对象,支持类、继承、封装、多态和对象标识。适合表达复杂数据类型和复杂联系。

  
典型系统:ObjectStore、Versant、db4o;对象关系型数据库如 PostgreSQL、Oracle 也支持部分对象特性。  


例子:  


可以定义类层次:


Person
├── Student
│   ├── 属性:学号、姓名、专业
│   └── 方法:选课()
└── Teacher
    ├── 属性:工号、姓名、职称
    └── 方法:授课()


再定义课程类:


Course
├── 属性:课程号、课程名、学分
└── 方法:添加学生()


一个 Student 对象可以直接包含一个课程对象集合:


Student 张三


- 学号:1001
- 姓名:张三
- 已选课程:数据库、操作系统


在 CAD、多媒体、地理信息系统等场景中,面向对象模型很常见。例如 CAD 中可以定义:


图形
├── 圆
├── 矩形
└── 多边形


每个图形对象有自己的属性和绘制方法。


 简单对比


模型数据结构联系表达能力典型例子
层次模型树一对多学校—学院—系—学生
网状模型图多对多学生—选课—课程
关系模型二维表通过主外键表达MySQL 中的学生表、课程表、选课表
面向对象模型对象、类继承、引用、复杂对象CAD 图形对象、多媒体对象


总体来看,层次模型和网状模型属于早期导航式模型;关系模型因简单、理论成熟而成为主流;面向对象模型更适合复杂数据结构和对象行为封装。现实中很多系统采用关系模型,并结合面向对象思想,如 ORM 框架或对象关系数据库。


=======================我是分割线=================================


反规范化技术


反规范化是在数据库规范化设计的基础上,为了提高查询性能、减少连接或聚合计算,而有意引入冗余、合并或分割表的技术。下面用电商系统常见的“客户—订单—商品”场景举例说明四种常见反规范化技术。


 1. 增加冗余列


定义:在表中存储原本可以通过连接其他表得到的列,避免频繁 JOIN。


常见例子:订单表原本只存 customer_id,查询订单列表时还要连接客户表获取客户姓名和电话。为了减少连接,直接在订单表中冗余存储 customer_name、customer_phone。


规范化设计:


Customer(customer_id, name, phone)
Orders(order_id, customer_id, order_date, amount)


查询订单时需连接:

sql
SELECT o.order_id, c.name, c.phone, o.order_date, o.amount
FROM Orders o
JOIN Customer c ON o.customer_id = c.customer_id;


反规范化后:


Orders(order_id, customer_id, customer_name, customer_phone, order_date, amount)


查询时不再需要连接客户表:


sql
SELECT order_id, customer_name, customer_phone, order_date, amount
FROM Orders;


优点:减少 JOIN,提高查询速度。  


缺点:客户改名或换电话时,需要同步更新所有历史订单,否则数据不一致。


 2. 增加派生列


定义:存储可以由其他列计算得出的列,避免每次查询时进行聚合或复杂计算。


常见例子:订单总金额原本需要根据订单明细中的 quantity  price 汇总得到。为了快速查询,在订单表中增加 total_amount 派生列。


规范化设计:


Orders(order_id, customer_id, order_date)
OrderItem(order_id, product_id, quantity, price)


查询订单总金额:


sql


SELECT o.order_id, SUM(oi.quantity  oi.price) AS total_amount
FROM Orders o
JOIN OrderItem oi ON o.order_id = oi.order_id
GROUP BY o.order_id;


反规范化后:


Orders(order_id, customer_id, order_date, total_amount)


下单或修改明细时,由程序或触发器更新 total_amount。


优点:查询订单总金额时无需聚合,速度快。  

缺点:订单明细变更时必须同步维护 total_amount,否则会出错。

 

3. 重新组表


定义:将经常一起查询的两个或多个表合并成一张表,减少连接操作。常发生在 1:1 关系或频繁联合查询的场景。


常见例子:用户基本信息和用户扩展信息原本分为两张表,但每次查询用户资料都要连接。可以合并为一张宽表。


规范化设计:


User(user_id, username, password_hash)
UserProfile(user_id, nickname, avatar, bio, email, phone)


查询用户完整信息:


sql

SELECT u.user_id, u.username, p.nickname, p.avatar, p.bio, p.email, p.phone
FROM User u
JOIN UserProfile p ON u.user_id = p.user_id;


反规范化后:


User(user_id, username, password_hash, nickname, avatar, bio, email, phone)


查询时只需一张表:


sql

SELECT user_id, username, nickname, avatar, bio, email, phone
FROM User;


优点:减少连接,查询简单。  

缺点:表变宽,可能出现大量 NULL;若原本是 1:N 关系,合并会导致大量重复数据。


4. 分割表


定义:将一张大表按列或按行拆分成多张较小的表。按列拆叫垂直分割,按行拆叫水平分割。分割表主要是为了减少 I/O、提高查询和管理效率,常与反规范化一起讨论。

 

4.1 垂直分割


常见例子:商品表中,description、image_url 等大字段不常查询,而商品列表只需要 product_id、product_name、price。可以把商品表拆成基本表和详情表。


原表:


Product(product_id, product_name, category_id, price, description, image_url)


垂直分割后:


ProductBase(product_id, product_name, category_id, price)
ProductDetail(product_id, description, image_url)


查询商品列表时只访问 ProductBase,速度快。

 

4.2 水平分割


常见例子:订单表数据量巨大,按年份拆分成多个表。


原表:


Orders(order_id, customer_id, order_date, total_amount)


水平分割后:


Orders_2023(order_id, customer_id, order_date, total_amount)
Orders_2024(order_id, customer_id, order_date, total_amount)
Orders_2025(order_id, customer_id, order_date, total_amount)


查询 2025 年订单时只扫描 Orders_2025,减少 I/O。现代数据库通常用分区表实现水平分割。


优点:单表变小,查询和维护更快。  

缺点:跨分割查询复杂,需要应用层或数据库分区功能支持。


 总结


反规范化技术常见例子主要目的
增加冗余列订单表增加客户姓名、电话减少 JOIN
增加派生列订单表增加总金额、商品表增加库存总值避免聚合计算
重新组表用户表和用户资料表合并减少连接,简化查询
分割表商品表垂直拆分;订单表按年份水平拆分减少 I/O,提高查询效率


反规范化的核心是用空间换时间,但会带来数据一致性维护成本。实际项目中通常先规范化设计,再根据查询性能瓶颈有针对性地反规范化。


=======================我是分割线=================================


并发操作问题


下面用“银行账户余额”和“商品库存”这两个常见场景,说明三种并发操作问题。


 1. 丢失更新


定义:两个事务同时读取同一数据,各自基于读到的值进行修改,后提交的事务覆盖了先提交的事务,导致先前的更新丢失。


常见例子:商品库存扣减


初始库存 quantity = 10。


时刻事务 T1事务 T2说明
1BEGIN; SELECT quantity = 10
T1 读到库存 10
2
BEGIN; SELECT quantity = 10T2 也读到库存 10
3UPDATE quantity = 10 - 1 = 9; COMMIT
T1 卖出 1 件,库存改为 9
4
UPDATE quantity = 10 - 2 = 8; COMMITT2 卖出 2 件,基于旧值 10 改为 8


最终库存是 8,但正确结果应该是:


10 - 1 - 2 = 7


T1 的更新被 T2 覆盖了,这就是丢失更新。


常见解决思路:
 

悲观锁:SELECT ... FOR UPDATE
 

乐观锁:增加版本号,更新时检查版本
 

原子更新:UPDATE product SET quantity = quantity  1 WHERE id = 1


2. 不可重复读


定义:同一个事务内,两次读取同一行数据,由于另一个事务修改并提交了该数据,导致两次读取结果不一致。


常见例子:同一事务内两次查询账户余额


初始余额 balance = 100。


时刻事务 T1事务 T2说明
1BEGIN; SELECT balance = 100
T1 第一次读,余额 100
2
BEGIN; UPDATE balance = 200; COMMITT2 将余额改为 200 并提交
3SELECT balance = 200
T1 第二次读,余额变成 200


T1 在同一个事务中,第一次读到 100,第二次读到 200,前后不一致,这就是不可重复读。


常见解决思路:
 

将隔离级别提高到“可重复读”
 

对读取的行加共享锁,防止其他事务修改

 

3. 读脏数据


定义:一个事务读取了另一个事务尚未提交的修改。如果那个事务后来回滚,那么读到的数据就是无效的“脏数据”。


常见例子:转账中间状态被读到


初始余额 balance = 100。


时刻事务 T1事务 T2说明
1BEGIN; UPDATE balance = 200;
T1 把余额改为 200,但未提交
2
BEGIN; SELECT balance = 200;T2 读到了 T1 未提交的值 200
3ROLLBACK;
T1 回滚,余额恢复为 100
4

T2 之前读到的 200 是脏数据


T2 读到的 200 实际上从未真正生效,这就是读脏数据,也叫脏读。


常见解决思路:
 

将隔离级别提高到“读已提交”或更高
 

避免读取其他事务未提交的数据


 总结对比


并发问题核心原因常见例子后果
丢失更新两个事务都写,后写覆盖先写库存扣减、计数器累加一次更新被覆盖
不可重复读同一事务内两次读,期间数据被提交修改同一事务内两次查余额前后读取结果不一致
读脏数据读到其他事务未提交的数据读到转账中间状态,对方又回滚读到无效数据


简单记忆:
 

丢失更新:两个人同时改,一个把另一个盖掉了。
 

不可重复读:同一个人两次读,结果不一样。
 

读脏数据:读到了别人还没确认、最后又反悔的数据。


=======================我是分割线=================================


共享锁和排他锁


共享锁(S锁)和排它锁(X锁)是数据库中最基本的两种封锁类型,主要用来控制并发事务对同一数据的读写。


 共享锁:又叫读锁。多个事务可以同时读同一数据,但都不能修改。
 

排它锁:又叫写锁。一个事务加了排它锁后,其他事务既不能读也不能写,必须等待。


它们的兼容关系如下:


当前锁 \ 请求锁共享锁 S排它锁 X
共享锁 S兼容不兼容
排它锁 X不兼容不兼容


简单说:共享锁之间可以共存,排它锁和任何锁都不能共存。

 

1. 图书馆阅览的例子


把数据库中的一行数据想象成图书馆里的一本书。

 

共享锁:多个读者可以同时坐在阅览室里看同一本书。大家只是读,不会改动内容,所以可以共存。
 

排它锁:如果有一个读者要把这本书借走,或者要在书上做笔记、修改内容,他就需要独占这本书。此时其他读者不能再借,也不能再读,必须等他处理完。


对应到数据库:

 

事务 A 对某行加共享锁,事务 B 也可以加共享锁,两个事务都能读。

 事务 C 想修改这行,需要加排它锁,但必须等 A、B 释放共享锁。
 

如果事务 C 先加了排它锁,事务 A、B 想加共享锁也会被阻塞。


 2. 银行账户查询与修改


假设账户表 account 中有一行:


idbalance
11000

 

场景一:两个事务同时查询余额


事务 A:


BEGIN;
SELECT  FROM account WHERE id = 1 LOCK IN SHARE MODE;


事务 B:


BEGIN;
SELECT  FROM account WHERE id = 1 LOCK IN SHARE MODE;


这两个查询都加共享锁,可以同时执行,都能读到余额 1000。因为共享锁之间兼容。

 

场景二:一个事务要修改余额


事务 A 先执行:


BEGIN;
SELECT  FROM account WHERE id = 1 FOR UPDATE;


这里 FOR UPDATE 加了排它锁。此时事务 B 如果执行:


SELECT  FROM account WHERE id = 1 LOCK IN SHARE MODE;


就会被阻塞,直到事务 A 提交或回滚。


同样,如果事务 B 执行:


UPDATE account SET balance = balance  100 WHERE id = 1;


也会被阻塞,因为 UPDATE 需要排它锁,而排它锁与排它锁不兼容。

 

3. 商品库存扣减


商品表 product:


idnamestock
1手机10


事务 A 要扣减库存:


BEGIN;
SELECT stock FROM product WHERE id = 1 FOR UPDATE;
UPDATE product SET stock = stock  1 WHERE id = 1;
COMMIT;


事务 A 对 id=1 的商品行加了排它锁。此时事务 B 如果也想扣减库存:


BEGIN;
SELECT stock FROM product WHERE id = 1 FOR UPDATE;


事务 B 会等待,直到事务 A 提交。这样可以避免两个事务同时读到 stock=10,然后都减 1,最终只减了 1 的丢失更新问题。


如果事务 B 只是普通查询,不加锁:


SELECT stock FROM product WHERE id = 1;


在 READ COMMITTED 等隔离级别下,普通 SELECT 通常不加锁,可能读到快照数据,不会阻塞。但如果事务 B 也加共享锁或排它锁,就会受到锁兼容性的限制。


 4. SQL 中常见加锁方式


操作加锁类型说明
SELECT ... LOCK IN SHARE MODE 或 SELECT ... FOR SHARE共享锁读数据,并阻止其他事务修改
SELECT ... FOR UPDATE排它锁读数据,并准备修改,独占该行
UPDATE、DELETE、INSERT排它锁写操作自动加排它锁
普通 SELECT通常不加锁取决于隔离级别,可能读快照

 

总结


锁类型别名允许其他事务读允许其他事务写常见例子
共享锁 S读锁可以加共享锁读不可以多人同时查看同一账户余额
排它锁 X写锁不可以不可以修改余额、扣减库存、转账


一句话记忆:


 共享锁:大家都能看,但谁也别改。


 排它锁:我要改,谁都别动,读也不行。


=======================我是分割线=================================


数据库封锁协议和封锁粒度


数据库中的封锁协议规定“什么时候加锁、什么时候释放锁”,用来解决并发事务之间的干扰;封锁粒度规定“锁住多大的数据范围”,用来在并发度和开销之间做权衡。下面分别用常见例子说明。

 

一、封锁协议


最常见的封锁协议是三级封锁协议和两阶段封锁协议。以银行转账为例:


账户表:


账户余额
A1000
B1000


事务 T1:从 A 转 100 给 B。  

事务 T2:从 A 转 200 给 C。

 

1. 一级封锁协议


规则:事务修改数据前必须先加排它锁 X,直到事务结束才释放。读数据不加锁。


例子:


 T1 要修改 A,先对 A 加 X 锁。
 

T2 也要修改 A,必须等 T1 释放 X 锁后才能修改。
 

因此不会出现两个事务都读到 A=1000,然后分别改成 900 和 800,最后只减了 100 的丢失更新问题。


能解决:丢失更新。  

不能解决:读脏数据、不可重复读。


例如:T1 修改 B=1100 但未提交,T2 读 B 得到 1100;随后 T1 回滚,T2 读到的就是脏数据。

 

2. 二级封锁协议


规则:在一级基础上,事务读数据前必须加共享锁 S,读完即可释放。


例子:

 

T1 修改 B 并加了 X 锁,尚未提交。

 T2 想读 B,必须先加 S 锁。但 S 锁与 X 锁不兼容,所以 T2 会等待。

 直到 T1 提交或回滚后,T2 才能读 B。
 

这样 T2 不会读到 T1 未提交的 B 值,避免了脏读。


能解决:丢失更新、读脏数据。  

不能解决:不可重复读。


例如:T2 第一次读 A=1000,加 S 锁读完后释放;T1 接着修改 A=900 并提交;T2 再读 A 时变成 900,前后不一致。


 3. 三级封锁协议


规则:在一级基础上,事务读数据前必须加 S 锁,并且直到事务结束才释放。


例子:


 T2 第一次读 A 时加 S 锁,并且一直持有到事务结束。
 

T1 想修改 A,必须加 X 锁,但 X 锁与 S 锁不兼容,所以 T1 必须等 T2 结束。

 因此 T2 在整个事务中两次读 A 都是 1000,不会出现不可重复读。


能解决:丢失更新、读脏数据、不可重复读。  

缺点:锁持有时间长,并发度降低。

 

4. 两阶段封锁协议(2PL)


规则:事务分为两个阶段:
 

增长阶段:只能加锁,不能解锁。
 

缩减阶段:只能解锁,不能加锁。


例子:


T1 正确顺序:


加锁 A → 加锁 B → 修改 A → 修改 B → 解锁 A → 解锁 B


T1 错误顺序:


加锁 A → 加锁 B → 解锁 A → 加锁 C   // 不允许,解锁后不能再加锁


两阶段封锁协议可以保证事务的可串行化调度,但可能产生死锁。严格两阶段封锁还要求所有锁都在事务结束时才释放。


 封锁协议小结


协议加锁规则能防止常见问题
一级封锁修改前加 X 锁,事务结束释放丢失更新脏读、不可重复读
二级封锁一级 + 读前加 S 锁,读完释放丢失更新、脏读不可重复读
三级封锁一级 + 读前加 S 锁,事务结束释放丢失更新、脏读、不可重复读并发度低
两阶段封锁增长阶段只加锁,缩减阶段只解锁保证可串行化可能死锁

 

二、封锁粒度


封锁粒度指封锁对象的大小,常见有行级、页级、表级、数据库级,以及多粒度封锁。


以商品表 product 为例:


idnamestockprice
1手机105000
2电脑58000
3耳机20500


 1. 行级锁


例子:


事务 T1:


UPDATE product SET stock = stock  1 WHERE id = 1;


只锁住 id=1 这一行。


事务 T2:


UPDATE product SET stock = stock  1 WHERE id = 2;


可以同时执行,因为锁的是不同行。


优点:并发度高。  

缺点:锁数量多,管理开销大。


 2. 页级锁


例子:


假设数据库一页存放 id=1~100 的商品。T1 更新 id=1,锁住整页。  

T2 想更新 id=2,虽然和 T1 不是同一行,但在同一页,也必须等待。


优点:开销比行级锁小。  

缺点:并发度比行级锁低,可能出现“锁不相关数据”的情况。


 3. 表级锁


例子:


事务 T1 要批量调价:


UPDATE product SET price = price  0.9;


对整张 product 表加排它锁。  

事务 T2 想查询商品:


SELECT  FROM product WHERE id = 1;


必须等待 T1 完成。


优点:开销小,实现简单。  

缺点:并发度低。


 4. 数据库级锁


例子:


数据库管理员做全库备份时,对整个数据库加锁。  

此时其他用户不能对任何表进行读写,直到备份完成。


优点:保证备份一致性。  

缺点:并发度极低,通常只在特殊维护时使用。


 5. 多粒度封锁与意向锁


多粒度封锁允许不同事务在不同粒度上加锁。为了协调表锁和行锁,引入了意向锁。


例子:


事务 T1 要修改 product 表中 id=1 的行:


1. 先对 product 表加意向排它锁 IX,表示“我打算在表内某些行上加 X 锁”。
2. 再对 id=1 行加排它锁 X。


事务 T2 要读整张 product 表:


1. 对 product 表加共享锁 S。
2. 发现表上已有 IX 锁,而 S 与 IX 不兼容,所以 T2 等待。


这样数据库不用逐行检查,就能快速判断表级锁和行级锁是否冲突。


 封锁粒度小结


封锁粒度例子并发度开销
行级锁扣减某个商品库存高大
页级锁锁住包含多行的一页中中
表级锁批量调价、整表统计低小
数据库级锁全库备份极低很小
多粒度封锁先表级意向锁,再行级锁灵活较复杂

 总结


 封锁协议解决的是“加锁和解锁的时机”问题:


   一级防丢失更新;


   二级防脏读;


   三级防不可重复读;


   两阶段封锁保证可串行化。


 封锁粒度解决的是“锁多大范围”的问题:


   粒度越细,并发越高,但开销越大;


   粒度越粗,开销越小,但并发越低;


   多粒度封锁通过意向锁协调不同粒度的锁。


实际数据库如 MySQL InnoDB 默认常用行级锁,并配合多粒度锁和两阶段封锁来兼顾并发与一致性。


=======================我是分割线=================================

死锁预防和解除


死锁是指两个或多个事务互相持有对方需要的锁,并等待对方释放,形成循环等待,导致谁也无法继续执行。


下面用“银行账户转账”这个常见例子说明。


账户表:


账户id余额
A11000
B21000


事务 T1:从 A 转 100 给 B,需要锁 A 和 B。  

事务 T2:从 B 转 200 给 A,需要锁 B 和 A。


不加控制时可能发生:


时刻T1T2
1对 A 加排它锁
2
对 B 加排它锁
3请求 B 的锁,等待 T2
4
请求 A 的锁,等待 T1


此时 T1 等 T2,T2 等 T1,形成循环等待,这就是死锁。


 一、死锁的预防法


 1. 顺序申请法


做法:规定所有事务必须按同一个固定顺序申请锁。例如按账户 id 从小到大加锁。


例子:


规定:所有转账事务必须先锁 id 小的账户,再锁 id 大的账户。


 A 的 id = 1,B 的 id = 2。


 T1 转账 A→B:先锁 A,再锁 B。


 T2 转账 B→A:也必须先锁 A,再锁 B。


执行过程:


时刻T1T2
1锁住 A
2
请求锁 A,但 A 已被 T1 锁住,等待
3再锁 B,执行转账
4提交,释放 A、B
5
获得 A,再获得 B,执行转账


T2 在等待 A 时并没有持有 B,所以不会出现“T1 持有 A 等 B,T2 持有 B 等 A”的循环等待。这样就从根源上预防了死锁。


本质:破坏死锁的“循环等待”条件。


 2. 一次申请法


做法:事务开始前,一次性申请它所需要的全部锁。如果有一个锁拿不到,就整体等待,不持有任何锁。


例子:


T1 和 T2 都需要锁 A 和 B。


 T1 开始时,一次性申请 A 和 B。


 T2 开始时,也一次性申请 A 和 B。


系统要么把 A、B 都分配给 T1,要么都不分配。假设 T1 先申请成功:


时刻T1T2
1一次性申请 A、B,成功
2执行转账一次性申请 A、B,失败,整体等待
3提交,释放 A、B
4
获得 A、B,执行转账


T2 在拿不到全部锁时,不会先持有 A 再等 B,也不会先持有 B 再等 A,因此不会形成“持有并等待”的局面。


本质:破坏死锁的“请求与保持”条件。  


缺点:事务开始前可能不知道需要哪些锁;资源利用率低,并发度下降。


 二、死锁的解除法


死锁预防有时代价较高,实际数据库系统常用“检测 + 解除”的方式。


 1. 死锁检测程序


做法:维护一张等待图。节点表示事务,边表示等待关系。如果图中出现环,就说明发生了死锁。


例子:


上面的转账死锁中:


 T1 等待 T2 释放 B,所以有边 T1 → T2。


 T2 等待 T1 释放 A,所以有边 T2 → T1。


等待图:


T1 →   T2
↑           ↓
└────┘


形成环:


T1 → T2 → T1


死锁检测程序发现这个环后,就判定 T1 和 T2 发生死锁。


实际数据库中,检测程序可能周期性运行,也可能在事务等待超过一定时间后触发。例如 MySQL InnoDB 就具有死锁检测机制。


 2. 解锁程序


做法:检测到死锁后,选择一个“牺牲者”事务,把它回滚,释放它持有的锁,从而打破循环等待,让其他事务继续执行。


例子:


等待图发现 T1 和 T2 死锁。解锁程序选择回滚 T2,因为 T2 执行时间短、持有锁少、回滚代价小。


执行过程:


时刻操作
1检测到 T1、T2 死锁
2选择 T2 作为牺牲者
3回滚 T2,释放 T2 持有的 B 锁,撤销 T2 的修改
4T1 获得 B 锁,继续完成转账
5T2 稍后重新执行


选择牺牲者时通常考虑:

 事务已执行的时间;


 事务持有锁的数量;


 回滚代价大小;


 事务优先级;


 是否已经修改过数据。


回滚后,解锁程序还要负责恢复数据一致性,并释放相关锁。


 总结


类别方法核心思想例子
死锁预防顺序申请法所有事务按同一顺序申请锁,破坏循环等待转账都先锁 id 小的账户,再锁 id 大的账户
死锁预防一次申请法事务开始前一次性申请全部锁,破坏请求与保持T1、T2 都一次性申请 A、B 两把锁,拿不到就整体等待
死锁解除死锁检测程序用等待图检测是否存在环T1 等 T2,T2 等 T1,等待图成环
死锁解除解锁程序选牺牲者回滚,释放锁,打破循环回滚 T2,释放 B,让 T1 继续执行


简单记忆:


 顺序申请法:大家都按同一个顺序拿锁,避免互相卡住。


 一次申请法:要么全拿,要么不拿,不半路占着等。


 死锁检测程序:画等待图,找环。


 解锁程序:挑一个事务回滚,把锁放掉,打破死锁。



传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!

最后于 56分钟前 被babyfox编辑 ,原因:
收藏
点赞 ・0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回