当前位置: 首页 > news >正文

数据库-锁

1. 锁的基本概念

为什么需要锁?

想象一下多人同时编辑同一个文档的场景:

  • 用户A和用户B同时读取余额为100元

  • 用户A存入50元,余额变为150元

  • 用户B取出30元,余额变为70元(应该是120元才对!)

这就是典型的并发问题,锁就是为了解决这类问题而生的。

锁的分类维度


2. 按锁粒度划分

2.1 全局锁

是什么:锁定整个数据库实例,让数据库处于只读状态。

使用场景

  • 全库逻辑备份

  • 重大数据库维护操作

MySQL示例

sql

-- 加全局锁
FLUSH TABLES WITH READ LOCK;-- 执行备份操作...-- 释放锁
UNLOCK TABLES;

特点

  • 🔴 影响极大:整个数据库无法写入

  • ⚡ 性能影响:所有写操作被阻塞

  • 🎯 使用谨慎:只在维护和备份时使用

2.2 表锁

是什么:锁定整张表。

分类

  1. 表共享读锁:多个事务可以同时读,但不能写

  2. 表独占写锁:只有一个事务能读写,其他事务都不能访问

MySQL示例

sql

-- 手动加表锁
LOCK TABLES users READ;  -- 读锁
LOCK TABLES users WRITE; -- 写锁-- 操作完成后解锁
UNLOCK TABLES;-- InnoDB自动表锁的情况
ALTER TABLE users ADD COLUMN age INT; -- DDL语句会自动加表锁

特点

  • 🔴 粒度较粗:影响整张表

  • ⚡ 性能中等:并发度较低

  • 🎯 适用场景:表结构变更、全表更新

2.3 行锁

是什么:只锁定表中的某一行或多行。

MySQL InnoDB行锁实现

sql

-- 事务1
BEGIN;
UPDATE users SET balance = balance - 100 WHERE id = 1; -- 对id=1的行加行锁-- 事务2(同时执行)
BEGIN;
UPDATE users SET balance = balance + 50 WHERE id = 2; -- 可以执行,不影响
UPDATE users SET balance = balance + 50 WHERE id = 1; -- 被阻塞,等待事务1释放锁

行锁的细分类型

  1. 记录锁:锁定单条记录

  2. 间隙锁:锁定记录之间的间隙,防止幻读

  3. 临键锁:记录锁+间隙锁,InnoDB默认行锁算法

间隙锁示例

sql

-- 假设id有记录:1, 3, 5, 7, 9
BEGIN;
SELECT * FROM users WHERE id > 5 AND id < 9 FOR UPDATE;-- 这会锁定(5,7)和(7,9)这两个区间
-- 其他事务无法在区间内插入新记录,如无法插入id=6或id=8的记录

特点

  • 🟢 粒度最细:只影响被锁定的行

  • ⚡ 性能最佳:并发度最高

  • 🔧 实现复杂:需要更多系统资源


3. 按锁态度划分

3.1 悲观锁

核心思想:"凡事做好最坏的打算",认为数据在并发访问时很可能被修改,所以先加锁再访问。

实现方式

sql

-- MySQL悲观锁实现
BEGIN;-- 方式1:SELECT ... FOR UPDATE (排他锁)
SELECT * FROM accounts WHERE user_id = 123 FOR UPDATE;
-- 执行业务逻辑...
UPDATE accounts SET balance = balance - 100 WHERE user_id = 123;COMMIT;-- 方式2:SELECT ... LOCK IN SHARE MODE (共享锁)
SELECT * FROM products WHERE stock > 0 LOCK IN SHARE MODE;

适用场景

  • 写多读少的场景

  • 对数据一致性要求极高的场景(如金融交易)

  • 临界区代码执行时间较长的场景

3.2 乐观锁

核心思想:"相信大家都很守规矩",认为并发冲突很少发生,只在更新时检查数据是否被修改。

实现方式

  1. 版本号机制

sql

-- 数据库表增加version字段
CREATE TABLE products (id BIGINT PRIMARY KEY,name VARCHAR(100),stock INT,version INT DEFAULT 0
);-- 业务逻辑(需要重试机制)
-- 第一次读取
SELECT id, stock, version FROM products WHERE id = 1;
-- 假设读到:stock=10, version=1-- 更新时检查版本号
UPDATE products 
SET stock = stock - 1, version = version + 1 
WHERE id = 1 AND version = 1; -- 关键:带上版本号条件-- 如果受影响行数为0,说明版本号不匹配,需要重试
  1. 时间戳机制

sql

-- 使用update_time作为乐观锁条件
UPDATE orders 
SET status = 'PAID', update_time = NOW() 
WHERE order_id = 1001 AND update_time = '2023-10-27 10:00:00';
  1. 条件判断机制

sql

-- 基于业务字段的条件
UPDATE accounts 
SET balance = balance - 100 
WHERE user_id = 123 AND balance >= 100; -- 余额足够才更新

适用场景

  • 读多写少的场景

  • 系统吞吐量要求高的场景

  • 冲突概率较低的业务场景


4. 按锁兼容性划分

4.1 共享锁

特性

  • 多个事务可以同时获取共享锁

  • 不能与排他锁共存

  • 用于读取操作

sql

-- MySQL
SELECT * FROM table_name LOCK IN SHARE MODE;-- SQL Server
SELECT * FROM table_name WITH (HOLDLOCK);

4.2 排他锁

特性

  • 只有一个事务能获取排他锁

  • 不能与其他任何锁共存

  • 用于写入操作

sql

-- MySQL
SELECT * FROM table_name FOR UPDATE;-- SQL Server
UPDATE table_name SET column = value WHERE condition;

4.3 意向锁

作用:为了在表级和行级锁之间建立协调机制,提高锁检查效率。

类型

  • 意向共享锁:事务打算在表中的某些行上加共享锁

  • 意向排他锁:事务打算在表中的某些行上加排他锁

锁兼容性矩阵

当前锁 →
请求锁 ↓
无锁共享锁(S)排他锁(X)意向共享(IS)意向排他(IX)
共享锁(S)
排他锁(X)
意向共享(IS)
意向排他(IX)

5. 实际应用场景分析

5.1 电商库存扣减场景

方案1:悲观锁实现

sql

BEGIN;
-- 锁定要更新的商品记录
SELECT stock FROM products WHERE id = 1001 FOR UPDATE;-- 检查库存
-- 业务逻辑:如果stock >= 要购买的数量,则扣减
UPDATE products SET stock = stock - 1 WHERE id = 1001;
COMMIT;

优点:强一致性,不会超卖
缺点:并发性能较低

方案2:乐观锁实现

java

// Java代码示例(需要重试机制)
@Transactional
public boolean deductStock(Long productId, Integer quantity) {int maxRetries = 3;for (int i = 0; i < maxRetries; i++) {Product product = productMapper.selectById(productId);if (product.getStock() < quantity) {return false; // 库存不足}int rows = productMapper.updateStock(productId, quantity, product.getVersion());if (rows > 0) {return true; // 更新成功}// 版本冲突,重试Thread.sleep(50); // 短暂等待后重试}throw new RuntimeException("库存扣减失败,请重试");
}

sql

-- 对应的Mapper SQL
UPDATE products 
SET stock = stock - #{quantity}, version = version + 1 
WHERE id = #{productId} AND version = #{version} AND stock >= #{quantity};

5.2 银行转账场景

sql

-- 必须使用悲观锁保证强一致性
BEGIN;-- 锁定转出账户
SELECT * FROM accounts WHERE account_no = 'A001' FOR UPDATE;
-- 锁定转入账户(注意顺序,避免死锁)
SELECT * FROM accounts WHERE account_no = 'B002' FOR UPDATE;-- 执行转账
UPDATE accounts SET balance = balance - 100 WHERE account_no = 'A001';
UPDATE accounts SET balance = balance + 100 WHERE account_no = 'B002';COMMIT;

6. 死锁与解决方案

死锁产生条件

  1. 互斥条件:资源不能被共享

  2. 占有且等待:持有资源并等待其他资源

  3. 不可抢占:资源只能自愿释放

  4. 循环等待:多个进程形成等待环

死锁示例

sql

-- 事务1
BEGIN;
UPDATE users SET name = 'A' WHERE id = 1; -- 锁定id=1
UPDATE users SET name = 'B' WHERE id = 2; -- 等待事务2释放id=2-- 事务2(同时执行)
BEGIN;
UPDATE users SET name = 'C' WHERE id = 2; -- 锁定id=2
UPDATE users SET name = 'D' WHERE id = 1; -- 等待事务1释放id=1
-- 💥 死锁发生!

死锁预防和解决

  1. 保持锁顺序:总是按相同顺序获取锁

  2. 设置锁超时SET innodb_lock_wait_timeout = 50;

  3. 死锁检测:数据库自动检测并回滚代价较小的事务

  4. 降低隔离级别:从可重复读降为读已提交,减少间隙锁


7. 面试高频题目

题目1:悲观锁 vs 乐观锁

Q:请详细说明悲观锁和乐观锁的区别及适用场景。

A

  • 思想差异:悲观锁认为冲突经常发生,先加锁再访问;乐观锁认为冲突很少发生,先访问再检查

  • 实现方式:悲观锁通过数据库锁机制实现;乐观锁通过版本号/时间戳实现

  • 性能差异:悲观锁并发度低但一致性强;乐观锁并发度高但需要重试机制

  • 适用场景:悲观锁适合写多读少、强一致性场景;乐观锁适合读多写少、高并发场景

题目2:数据库锁粒度

Q:谈谈你对数据库锁粒度的理解,不同粒度的锁各有什么优缺点?

A

  • 行锁:粒度最小,并发度高,但开销大,可能死锁

  • 表锁:粒度大,并发度低,但开销小,不会死锁

  • 页锁:介于行锁和表锁之间

  • 选择原则:根据业务并发性和数据一致性要求选择合适粒度

题目3:死锁处理

Q:数据库中出现死锁时,MySQL是如何处理的?

A

  1. 死锁检测:InnoDB会检测死锁循环

  2. 选择牺牲者:回滚undo量最小的事务

  3. 错误返回:向客户端返回ERROR 1213 (40001): Deadlock found

  4. 事务回滚:被选择的事务自动回滚

题目4:实战设计题

Q:设计一个电商秒杀系统的库存扣减方案,要求保证不超卖且性能良好。

A

sql

-- 方案:Redis预减库存 + 数据库最终扣减 + 乐观锁
-- 1. Redis预扣库存(减轻数据库压力)
-- 2. 数据库最终扣减使用乐观锁
UPDATE products 
SET stock = stock - 1, version = version + 1,sale_count = sale_count + 1 
WHERE id = #{productId} AND version = #{version} AND stock > 0;-- 3. 如果更新失败,在Redis中恢复库存
-- 4. 前端配合排队、限流机制

题目5:锁的升级和降级

Q:什么情况下会发生锁升级?有什么影响?

A

  • 锁升级:当单个事务持有的行锁超过阈值时,数据库可能将行锁升级为表锁

  • 触发条件:SQL Server中当行锁数量超过5000,MySQL InnoDB较少发生

  • 影响:并发度急剧下降,可能引发阻塞链

  • 避免方法:优化事务,减少锁持有数量和时间


总结

数据库锁是一个复杂的主题,但掌握它对于构建高并发、高可用的系统至关重要。关键要点:

  1. 理解不同粒度的锁及其适用场景

  2. 根据业务特点选择悲观锁或乐观锁

  3. 注意死锁的预防和处理

  4. 在实际应用中考虑性能和一致性的平衡

http://www.dtcms.com/a/420657.html

相关文章:

  • 解决 Python Crypto安装后依然无效的问题
  • 数字化转型:概念性名词浅谈(第五十六讲)
  • solidworks 实体分割 保存实体 后转换钣金 钣门框出图 结构构件添加 钣金出图教程 平板样式不是展开解决办法 比例激活1比1
  • (Kotlin协程六)协程和RxJava的区别
  • 企业网站制作及cms技术网站开发文档教学
  • 比较好的建立站点如何修改wordpress主题
  • 想让客户公司做网站的话语wordpress s5主题
  • 网站制作框架公司网页怎么制作教程
  • 【实战】理解服务器流量监控中的“上行”和“下行”
  • 洛阳直播网站建设个人空间网站
  • MyBatis-Plus使⽤
  • 长春网站制作报价南京软件定制
  • 烟台网站建设方案wordpress推荐好友
  • 著名网站用什么语言做后台定制软件开发报价
  • 质量好网站建设商家建设网站的建设费用包括哪些内容
  • 《Linux 基础 IO 完全指南:从文件描述符到缓冲区》
  • 如何上传ftp网站程序c 做网站开发
  • 【Linux】库的制作与原理(1)
  • 网站建设策划书悠悠如何做百度竞价推广
  • NVIDIA Warp v1.9.0深度解析:GPU加速物理仿真与计算的革命性进展
  • 网站怎么挂广告有没有做字的网站
  • dede电影网站模版个人博客模板wordpress
  • 临清设计网站网站建立价格
  • 公司电子商务网站建设规划方案米拓网站建设步骤
  • 易基因:Cell Res/IF25.9:童明汉/蓝斐/汤富酬合作利用ChIP-seq及多组学分析揭示精子发生的表观遗传调控机制
  • 免费免费网站模板wordpress主题更换字体教程 | hu
  • 郑州网站推广策划做景观私活的网站
  • 虚拟环境中多个activate:.bat、.fish、ps1以及无后缀的
  • 秦皇岛网站制作哪个好制作电子商务网站页面
  • 国际网站 建设网站建设技术人员