用生活场景带你理解数据库中锁相关知识点

1. 共享锁(S锁) vs 排他锁(X锁)——厕所社交礼仪

  • 共享锁(S锁)

    • 场景:一群人围观同一份数据(比如查工资条)

    • 规则

      • 你可以读,我也可以读,大家排排坐吃果果

      • 但不能写!谁要是敢改数据,就像在围观群众面前撕了工资条——直接引发众怒!

    • 数据库操作SELECT ... LOCK IN SHARE MODE(手动加锁)或某些隔离级别自动触发

    • 兼容性:共享锁之间是好朋友,但和排他锁是死对头

  • 排他锁(X锁)

    • 场景:独占数据搞事情(比如发工资、删库跑路)

    • 规则

      • 我进厕所不仅锁门,还要贴封条:"施工中,勿扰!"

      • 其他人读都不让读(某些隔离级别下允许读旧版本,但这是后话)

    • 数据库操作UPDATEDELETEINSERT 自动加排他锁

    • 兼容性:六亲不认,见谁怼谁

扩展知识点

  • 锁兼容矩阵(想象成厕所门口的排队名单)

    S锁X锁
    S锁
    X锁

2. 行锁 vs 表锁 vs 页锁——锁的「精准打击」等级

  • 行锁(Row Lock)

    • 操作:精确锁住一行数据(比如只锁住ID=9527的摸鱼员工记录)

    • 优点:高并发神器,其他人可以愉快地操作其他行

    • 缺点:锁太多会占用内存,管理起来像在管理一厕所的VIP卡

    • 数据库支持:InnoDB的看家本领(MyISAM:我不配)

  • 表锁(Table Lock)

    • 操作:直接锁住整个表(比如要修改表结构)

    • 优点:管理简单,一把锁搞定

    • 缺点:并发性能像便秘——一锁全堵

    • 典型翻车现场ALTER TABLE 时全表锁,导致线上服务卡成PPT

  • 页锁(Page Lock)

    • 操作:锁住一页数据(比如数据库按4KB分页管理)

    • 特点:中庸之道,比表锁灵活,比行锁省资源

    • 支持数据库:SQL Server的隐藏技能

扩展知识点

  • 锁升级:当行锁太多时,数据库可能「掀桌子」直接升级为表锁(像厕所管理员大喊:"都给我出去!我要大扫除!")

  • 意向锁(Intention Lock)

    • 作用:快速判断表里有没有行被锁(像在厕所门口挂个牌子:"本层可能有VIP占坑")

    • 类型:意向共享锁(IS)、意向排他锁(IX)


3. 死锁——程序员的「厕所对峙」哲学

  • 经典死锁场景

    -- 程序员A  
    BEGIN;  
    UPDATE 厕所 SET 状态='占用' WHERE 坑位=1; -- 锁住坑位1  
    UPDATE 厕所 SET 状态='占用' WHERE 坑位=2; -- 等待坑位2的锁  
    
    -- 程序员B  
    BEGIN;  
    UPDATE 厕所 SET 状态='占用' WHERE 坑位=2; -- 锁住坑位2  
    UPDATE 厕所 SET 状态='占用' WHERE 坑位=1; -- 等待坑位1的锁  
    • 结果:两人大眼瞪小眼,数据库OS:"这俩憨批..."(随机回滚一个事务)

  • 死锁检测

    • 等待图算法:数据库画个圈圈诅咒你(检测循环等待)

    • 超时机制:等太久直接踢人(innodb_lock_wait_timeout

  • 如何避免死锁

    • 统一加锁顺序(比如永远先锁坑位1再锁坑位2)

    • 事务尽量短小精悍(别蹲坑玩手机)

    • 必要时使用 SELECT ... FOR UPDATE 提前占坑


4. 乐观锁 vs 悲观锁——「信任」还是「怀疑」?

  • 悲观锁(默认不信任人类)

    • 哲学:"总有刁民想改朕的数据!"

    • 实现:直接用数据库锁机制(SELECT ... FOR UPDATE

    • 适用场景:高并发写操作(比如双十一秒杀)

  • 乐观锁(相信世界充满爱)

    • 哲学:"你们先改,改完我再看看有没有冲突~"

    • 实现:版本号(Version)或时间戳

      UPDATE 账户 SET 余额=100, version=2  
      WHERE id=1 AND version=1; -- 如果version被改过,更新失败  

    • 适用场景:低并发写操作(比如修改个人昵称)

扩展知识点

  • ABA问题

    • 场景:版本号从1→2→1,乐观锁误以为没变化

    • 解决:用不可逆的版本号(比如自增ID)或追加时间戳


5. 间隙锁(Gap Lock)——防止「插队」的正义使者

  • 作用:锁住一个范围(比如ID>100且<200),防止其他事务插入数据

  • 经典场景

    SELECT * FROM 员工 WHERE 工资 > 10000 FOR UPDATE; 
    • 此时不仅锁住现有高工资员工,还会锁住「未来可能插入的高工资员工」的位置

  • 数据库支持:InnoDB在可重复读(Repeatable Read)隔离级别下自动启用


防坑指南(附赠厕所管理员的忠告)

  1. 索引是锁的好基友

    • 没有索引?行锁直接退化成表锁!(像因为找不到钥匙,直接把整个厕所封了)

  2. 监控锁状态

    • MySQL:SHOW ENGINE INNODB STATUS(看死锁日志)

    • 通用:SELECT * FROM information_schema.INNODB_LOCKS;

  3. 隔离级别的影响

    • 读未提交(Read Uncommitted):厕所门都不关,随时偷看

    • 读已提交(Read Committed):只读确认过的坑位

    • 可重复读(Repeatable Read):拍张厕所快照,后面一直看这张照片

    • 串行化(Serializable):一人用完厕所,下一个人才能进


总结:锁的本质是「协调多个程序员的如厕需求」,用好了如丝般顺滑,用错了轻则排队骂娘,重则屎山爆发!

接下来,我将再用几个真实开发场景让你更深入了解锁!!!

场景 1:电商秒杀——「库存超卖」惨案

翻车现场

# 错误代码:没有锁保护
def seckill(product_id):
    stock = db.query("SELECT stock FROM products WHERE id=?", product_id)
    if stock > 0:
        db.execute("UPDATE products SET stock=stock-1 WHERE id=?", product_id)
        # 创建订单...

结果:100人同时抢最后1件商品,数据库显示库存-99(老板连夜删库跑路)

救命姿势1——乐观锁(适合低并发)

UPDATE products 
SET stock = stock - 1, version = version + 1 
WHERE id=123 AND version=当前查到的version

原理:就像在商品页面写「最后1件!」,但实际可能有100个单身狗同时点击购买,只有1个幸运儿能成功,其他人看到「手慢了,下次早点哦!」

救命姿势2——悲观锁(高并发推荐)

BEGIN;
SELECT * FROM products WHERE id=123 FOR UPDATE; -- 加排他锁
UPDATE products SET stock=stock-1 WHERE id=123;
COMMIT;

原理:第一个抢到锁的用户会像超市抢鸡蛋的大妈,死死抱住商品不让别人碰,直到付款完成

进阶方案:用Redis队列削峰,把请求排队处理(像发号码牌,叫到号的才能进)


场景 2:转账——「余额变灵异事件」

翻车现场

// 错误代码:先读后写无锁
public void transfer(int fromId, int toId, BigDecimal amount) {
    BigDecimal fromBalance = accountDao.getBalance(fromId);
    if (fromBalance.compareTo(amount) >= 0) {
        accountDao.subtractBalance(fromId, amount); // A事务可能在此处被其他事务修改余额!
        accountDao.addBalance(toId, amount);
    }
}

结果:用户A余额100元,同时发起两笔100元转账,两笔都成功,余额变成-100元(银行连夜聘请你为首席魔术师)

正确姿势——悲观锁

BEGIN;
SELECT balance FROM accounts WHERE id=123 FOR UPDATE; -- 锁住行
UPDATE accounts SET balance=balance-100 WHERE id=123;
COMMIT;

原理:就像ATM机一次只允许一个人操作,后面的人必须排队

附加知识点

  • 永远先锁扣款账户,再锁收款账户(统一加锁顺序防死锁)

  • 余额检查要用 >=0 的约束(数据库最后防线)


场景 3:订单状态更新——「我到底付没付钱?」

翻车现场

-- 错误操作:直接全表扫
UPDATE orders SET status='paid' WHERE user_id=456 AND status='unpaid';

结果:当用户有10万条未支付订单时,这个SQL会锁住整个表(DBA提着刀来找你)

正确姿势——行锁+索引

-- 假设user_id和status有联合索引
BEGIN;
SELECT id FROM orders 
WHERE user_id=456 AND status='unpaid' 
FOR UPDATE; -- 只锁符合条件的行
UPDATE orders SET status='paid' WHERE id IN (...);
COMMIT;

原理:就像快递员不会为了送一个包裹封锁整个小区,而是精准找到你的门牌号


场景 4:统计报表——「老板要的数据永远对不上」

需求:每天凌晨统计销售额,期间要保证数据不被修改

错误做法

BEGIN;
SELECT SUM(amount) FROM orders; -- 耗时10分钟
-- 此时其他事务仍然可以修改数据!
COMMIT;

结果:统计结果像薛定谔的猫——永远不确定

正确姿势——快照读(MVCC)

BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; 
SELECT SUM(amount) FROM orders; -- 读取事务开始时的快照
COMMIT;

原理:给数据库拍张照,后续操作都是看这张照片(哪怕实际数据已经变了)


场景 5:用户注册——「我怎么变成张三丰了?」

翻车现场

-- 没有唯一性检查
INSERT INTO users(username) VALUES ('张三');
-- 高并发时可能插入多条相同用户名

救命姿势——行锁+唯一索引

BEGIN;
SELECT * FROM users WHERE username='张三' FOR UPDATE; -- 锁住不存在的行?需要配合唯一索引
INSERT INTO users(username) VALUES ('张三');
COMMIT;

更优方案

  1. 数据库加唯一索引(终极防御)

  2. 用Redis分布式锁先抢注用户名


开发者的保命锦囊

  1. 索引是爸爸

    • 没有索引的WHERE条件会锁全表(像用核弹灭蚊子)

    • 联合索引要注意最左前缀原则

  2. 事务越短越好

    • 不要在事务里调用外部API(比如发短信)

    • 像在厕所里不要刷短视频——后面的人会疯

  3. 监控锁等待

    SHOW ENGINE INNODB STATUS; -- 看LATEST DETECTED DEADLOCK
    SELECT * FROM information_schema.INNODB_TRX; -- 看长时间运行的事务

  4. 隔离级别不是越高越好

    • 默认用Read CommittedRepeatable Read可能引发更多锁冲突


总结:数据库锁就像交通信号灯——用好了秩序井然,用错了就是大型追尾现场。现在你可以自信地说:「我曾经在百万并发的系统中,优雅地处理过锁的问题……至少理论上!」 🚦💻

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值