1. 共享锁(S锁) vs 排他锁(X锁)——厕所社交礼仪
-
共享锁(S锁)
-
场景:一群人围观同一份数据(比如查工资条)
-
规则:
-
你可以读,我也可以读,大家排排坐吃果果
-
但不能写!谁要是敢改数据,就像在围观群众面前撕了工资条——直接引发众怒!
-
-
数据库操作:
SELECT ... LOCK IN SHARE MODE
(手动加锁)或某些隔离级别自动触发 -
兼容性:共享锁之间是好朋友,但和排他锁是死对头
-
-
排他锁(X锁)
-
场景:独占数据搞事情(比如发工资、删库跑路)
-
规则:
-
我进厕所不仅锁门,还要贴封条:"施工中,勿扰!"
-
其他人读都不让读(某些隔离级别下允许读旧版本,但这是后话)
-
-
数据库操作:
UPDATE
,DELETE
,INSERT
自动加排他锁 -
兼容性:六亲不认,见谁怼谁
-
扩展知识点:
-
锁兼容矩阵(想象成厕所门口的排队名单)
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)隔离级别下自动启用
防坑指南(附赠厕所管理员的忠告)
-
索引是锁的好基友:
-
没有索引?行锁直接退化成表锁!(像因为找不到钥匙,直接把整个厕所封了)
-
-
监控锁状态:
-
MySQL:
SHOW ENGINE INNODB STATUS
(看死锁日志) -
通用:
SELECT * FROM information_schema.INNODB_LOCKS;
-
-
隔离级别的影响:
-
读未提交(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;
更优方案:
-
数据库加唯一索引(终极防御)
-
用Redis分布式锁先抢注用户名
开发者的保命锦囊
-
索引是爸爸:
-
没有索引的
WHERE
条件会锁全表(像用核弹灭蚊子) -
联合索引要注意最左前缀原则
-
-
事务越短越好:
-
不要在事务里调用外部API(比如发短信)
-
像在厕所里不要刷短视频——后面的人会疯
-
-
监控锁等待:
SHOW ENGINE INNODB STATUS; -- 看LATEST DETECTED DEADLOCK SELECT * FROM information_schema.INNODB_TRX; -- 看长时间运行的事务
-
隔离级别不是越高越好:
-
默认用
Read Committed
,Repeatable Read
可能引发更多锁冲突
-
总结:数据库锁就像交通信号灯——用好了秩序井然,用错了就是大型追尾现场。现在你可以自信地说:「我曾经在百万并发的系统中,优雅地处理过锁的问题……至少理论上!」 🚦💻