个人主页会编程的土豆欢迎来访作者简介后端学习者❄️个人专栏数据结构与算法数据库leetcode✨那些你一个人走过的夜路终将化作照亮未来的光写给基础一般的同学假设你不懂 InnoDB、隔离级别读完目标知道行锁解决什么问题、怎么写、什么时候用/不用并能对照自己的FOR UPDATE代码讲清楚〇、先别急着背名词你先记住一个生活场景图书馆里有一排柜子。行锁≈ 你正在用某一格柜子时给这一格挂了「使用中」别人不能同时往这一格塞东西。其它柜子不受影响。对应到数据库一行数据 一格柜子比如某一个座位那一行行锁 暂时不许别人改这一行表锁 整排柜子全锁粗暴别人动哪一行都受影响抢票/选座场景里我们通常要的是只锁住被抢的那几个座位行而不是锁整张seats表。一、为什么需要锁没有锁会怎样数据库经常被很多请求同时访问。想象两个用户几乎同时做这件事1. 读一下座位是不是空闲 2. 如果是空闲就改成「我锁定了」如果中间没有互斥可能变成用户A读到空闲 用户B读到空闲 ← A还没改完 用户A改成A锁定 用户B改成B锁定 ← 超卖/覆盖这就是并发下的经典问题检查的时候成立真正改的时候已经变了。锁的作用很朴素让「读 判断 改」对同一行变成一个人做完另一个人再来。二、行锁是什么定义 边界2.1 一句话定义行锁Row Lock数据库锁住的是表里的某一行或若干行而不是整张表。在 MySQL 的InnoDB引擎里行锁很常见。如果你的表是 MyISAM行锁支持很弱课设表一定要用 InnoDB。我们项目 schema 写的就是ENGINEInnoDB。2.2 行锁管多久通常管到事务提交 Commit或事务回滚 Rollback也就是说行锁 lifespan ≈事务的寿命事务很快结束锁就很快释放你在事务里睡觉、调慢接口锁就会被你拿很久——别人会卡很久2.3 行锁「禁止」的是什么对同一行别人想再SELECT ... FOR UPDATE也要独占UPDATE/DELETE这行往往会等待直到你的事务结束。注意别人改别的行通常还能改「绝对不能读」不一定普通SELECT在默认隔离下往往还能读到旧的已提交数据这点后文会再说初学先抓住「改同一行会等」三、和表锁、业务锁的区别一定要分清3.1 行锁 vs 表锁行锁表锁锁的范围一行/几行整张表并发好各改各的行差大家排队典型场景改订单、改座位整表维护、某些引擎选座只要锁「被点的座位」用行锁没必要锁全表。3.2 行锁 vs 业务锁定超容易混你项目里其实有两层「锁」数据库行锁业务锁定字段怎么体现SELECT ... FOR UPDATEstatus1,lock_user_id,lock_until管多久事务期间很短支付窗口如 15 分钟目的并发瞬间不打架提交后座位仍显示被占比喻再强调一次行锁你在柜台办手续时工作人员按住档案不让别人同时改业务锁办完后给你发「临时占座牌」15 分钟内座位算你的只有行锁、没有业务字段事务一提交别人立刻又能下单抢。只有业务字段、没有行锁两人可能同时读到空闲一起改成锁定。两样都要才是完整的「抢座」。四、怎么用行锁写法在 MySQL / InnoDB 里课设最常用、最好讲的就是SELECT ... FROM 表 WHERE 条件 FOR UPDATE;并且它必须放在事务里才有工程意义。4.1 最小模板请背这个骨架START TRANSACTION; -- 或 Go 里 tx.Begin() SELECT status, lock_user_id FROM seats WHERE id 101 AND schedule_id 12 FOR UPDATE; -- 锁住这一行 -- 在这里根据 status 判断 -- 然后 UPDATE / INSERT COMMIT; -- 成功提交放锁 -- 或 ROLLBACK; -- 失败回滚放锁Go 里对应tx, err : db.Begin() defer func() { _ tx.Rollback() }() err tx.QueryRow( SELECT status, lock_user_id FROM seats WHERE id? AND schedule_id? FOR UPDATE, seatID, scheduleID, ).Scan(status, lockUID) // 校验... // tx.Exec(UPDATE ...) // tx.Exec(INSERT ...) tx.Commit()4.2 几个写法要点① 一定要用事务对象tx不要混用// 正确查询和更新都走 tx tx.QueryRow(... FOR UPDATE) tx.Exec(UPDATE ...) // 错误锁在 tx 上更新走另一个连接 → 行锁等于白加 tx.QueryRow(... FOR UPDATE) db.Exec(UPDATE ...) // 危险② WHERE 尽量点到具体行并走索引WHERE id ? -- 好锁目标清晰 WHERE schedule_id 12 -- 可能锁很多行甚至触发更粗的锁你项目用id? AND schedule_id?目标是具体座位同时防止乱传别的场次 id。③ 事务要短事务里不要time.Sleep调用微信/支付宝等很久的外部接口超大循环无关操作锁拿得越久别人等得越惨。④ 失败要回滚Go 里常用defer func() { _ tx.Rollback() }()中途return err会自动撤成功再Commit()。五、什么情况下应该用行锁下面这些场景很适合行锁或同类互斥5.1 稀缺资源「谁拿走算谁的」选座、选房态、优惠券一券一人库存扣减也可用条件更新但行锁思路同类号码池取号特征同一行不能被两个事务同时成功占有。5.2 「先读后改」且中间不能插队模式是读当前状态 → 按状态做分支判断 → 写回新状态只要两个人可能对同一行做这三步就要考虑互斥。FOR UPDATE就是把三步包进同一把锁里。5.3 状态机迁移要串行例如订单用户支付定时任务超时取消两边都可能改同一订单。对订单行FOR UPDATE保证同一时刻只有一个迁移成功。你项目支付、取消、超时清理都锁了订单行。5.4 一句话判断题问自己如果两个请求同时跑到这段逻辑会不会把同一行改出矛盾结果会 → 考虑行锁或条件更新等互斥手段。不会只是各写各的日志、各插各的评论→ 通常不必。六、什么情况下不要乱用行锁6.1 纯展示查询首页列电影、刷座位图给用户看SELECT * FROM seats WHERE schedule_id?一般不要加FOR UPDATE。原因展示不需要独占加了只会让别人更新更慢。6.2 冲突极少、可以接受重试例如点赞数 1偶尔冲突用乐观锁/重试更轻。不是所有UPDATE都要先FOR UPDATE。6.3 可以一条条件更新搞定时UPDATE seats SET status1, lock_user_id? WHERE id? AND status0;若影响行数1 成功0 失败——这是另一种互斥偏乐观/条件写。简单扣库存常用这个。你选座因为要「读很多字段做复杂校验 插订单」用FOR UPDATE更清晰。6.4 跨很久的用户操作不要指望FOR UPDATE 锁住 等用户掏出手机扫码 5 分钟 再 Commit这会把行锁拿 5 分钟系统会卡死。正确做法事务快速结束长时间占用用业务字段锁定到lock_until七、结合你的项目行锁出现在哪7.1 下单抢座锁座位行tx.QueryRow( SELECT status,row_no,col_no,lock_user_id FROM seats WHERE id? AND schedule_id? FOR UPDATE, sid, scheduleID)然后已售 → 失败别人锁定 → 失败通过 → 插订单、把座位改成锁定两个用户抢同一sid后到的在FOR UPDATE等待你提交后他读到新状态校验失败7.2 支付 / 取消 / 超时锁订单行SELECT status,user_id,pay_deadline FROM orders WHERE id? FOR UPDATE防止一边支付、一边超时释放把状态改乱。7.3 用一张图串起来用户点下单 → BEGIN → 对每个座位 FOR UPDATE行锁 → 校验 → INSERT 订单 → UPDATE 座位为业务锁定 → COMMIT行锁释放但 status 仍是锁定 → 15 分钟内别人仍不能买业务锁 → 支付成功再锁订单行座位改已售 → 或超时锁订单行座位改回空闲八、慢慢看一段「带行锁」的完整逻辑用人话对应代码① Begin开启事务开始办事还没对外宣布 ② defer Rollback中途失败就当没发生 ③ FOR UPDATE按住这个座位档案 ④ 看状态卖了别人占了→ 不行就返回错误自动回滚 ⑤ 都 OK创建订单草稿 ⑥ 座位写上「我锁定了、锁到几点、对应哪张单」 ⑦ Commit对外宣布成功放开行锁其中③④是行锁发挥作用的时刻⑥是业务锁⑦之后行锁没了业务锁还在。九、常见误区基础差最容易踩误区 1以为 FOR UPDATE 可以单独写不需要事务工程上请永远事务包住 FOR UPDATE 后续写误区 2以为加了行锁整张表别人都不能动只影响冲突的行简化理解。其它座位照样能卖误区 3以为行锁能代替支付超时行锁很短。长时间占座必须靠lock_until 定时任务误区 4前端把按钮禁用了就不需要锁了前端防的是手滑防不了并发和抓包。互斥必须在服务器/数据库误区 5所有 SELECT 都加 FOR UPDATE展示接口不要乱加否则自己把自己锁慢误区 6表不是 InnoDB没有真正的行级事务锁体验。建表看一眼ENGINEInnoDB十、怎么自己验证「行锁生效了」方法 A双浏览器推荐两个账号打开同一场次同时选同一座位下单一人成功一人提示锁定/已售方法 B人为拖慢事务仅本地调试在Commit前临时Sleep(5秒)另一个请求会明显卡住。测完删掉。方法 C查库成功一方SELECT id,status,lock_user_id,order_id FROM seats WHERE id?;应看到锁定人是成功用户。十一、行锁相关的兄弟概念先混个眼熟以后学更深会遇到现在够用知道名字概念直觉共享锁 S / 排他锁 X行锁里 FOR UPDATE 偏排他我改你别改死锁A 锁1等2B 锁2等1数据库踢掉一个锁等待超时等太久报错innodb_lock_wait_timeout乐观锁不加长锁靠版本号/条件更新冲突就重试悲观锁先锁再干活FOR UPDATE 属于这类你的课设选型是悲观行锁适合「抢座位这种冲突可预期」的场景。十二、什么时候用速查表场景建议两人可能改同一座位/同一订单用行锁FOR UPDATE或等价互斥下单读状态分支写订单写座位事务 行锁支付与超时可能同时改订单对订单行加锁首页列表、座位图展示不用 FOR UPDATE用户扫码等 5 分钟不要持有行锁等用业务锁定字段简单 stock-1可考虑UPDATE ... WHERE stock0十三、选座是稀缺资源竞争。我在事务里对座位行使用SELECT FOR UPDATE行锁保证同一时刻只有一个事务能完成校验和锁定事务很快提交。支付窗口靠座位状态和lock_until业务锁定维持超时任务负责释放。这样既防并发超卖又不会把行锁拿太久。十四、小结行锁锁的是一行解决多人同时改同一行的冲突。常用写法SELECT ... FOR UPDATE必须放在事务里读写都走同一tx。适合抢座、库存互斥、状态机并发迁移。不适合纯展示也不要用行锁代替长时间业务占座。你项目里下单锁座位行支付/超时锁订单行——这就是标准用法
数据库行锁到底是什么?——从零讲到影院票务里的用法
个人主页会编程的土豆欢迎来访作者简介后端学习者❄️个人专栏数据结构与算法数据库leetcode✨那些你一个人走过的夜路终将化作照亮未来的光写给基础一般的同学假设你不懂 InnoDB、隔离级别读完目标知道行锁解决什么问题、怎么写、什么时候用/不用并能对照自己的FOR UPDATE代码讲清楚〇、先别急着背名词你先记住一个生活场景图书馆里有一排柜子。行锁≈ 你正在用某一格柜子时给这一格挂了「使用中」别人不能同时往这一格塞东西。其它柜子不受影响。对应到数据库一行数据 一格柜子比如某一个座位那一行行锁 暂时不许别人改这一行表锁 整排柜子全锁粗暴别人动哪一行都受影响抢票/选座场景里我们通常要的是只锁住被抢的那几个座位行而不是锁整张seats表。一、为什么需要锁没有锁会怎样数据库经常被很多请求同时访问。想象两个用户几乎同时做这件事1. 读一下座位是不是空闲 2. 如果是空闲就改成「我锁定了」如果中间没有互斥可能变成用户A读到空闲 用户B读到空闲 ← A还没改完 用户A改成A锁定 用户B改成B锁定 ← 超卖/覆盖这就是并发下的经典问题检查的时候成立真正改的时候已经变了。锁的作用很朴素让「读 判断 改」对同一行变成一个人做完另一个人再来。二、行锁是什么定义 边界2.1 一句话定义行锁Row Lock数据库锁住的是表里的某一行或若干行而不是整张表。在 MySQL 的InnoDB引擎里行锁很常见。如果你的表是 MyISAM行锁支持很弱课设表一定要用 InnoDB。我们项目 schema 写的就是ENGINEInnoDB。2.2 行锁管多久通常管到事务提交 Commit或事务回滚 Rollback也就是说行锁 lifespan ≈事务的寿命事务很快结束锁就很快释放你在事务里睡觉、调慢接口锁就会被你拿很久——别人会卡很久2.3 行锁「禁止」的是什么对同一行别人想再SELECT ... FOR UPDATE也要独占UPDATE/DELETE这行往往会等待直到你的事务结束。注意别人改别的行通常还能改「绝对不能读」不一定普通SELECT在默认隔离下往往还能读到旧的已提交数据这点后文会再说初学先抓住「改同一行会等」三、和表锁、业务锁的区别一定要分清3.1 行锁 vs 表锁行锁表锁锁的范围一行/几行整张表并发好各改各的行差大家排队典型场景改订单、改座位整表维护、某些引擎选座只要锁「被点的座位」用行锁没必要锁全表。3.2 行锁 vs 业务锁定超容易混你项目里其实有两层「锁」数据库行锁业务锁定字段怎么体现SELECT ... FOR UPDATEstatus1,lock_user_id,lock_until管多久事务期间很短支付窗口如 15 分钟目的并发瞬间不打架提交后座位仍显示被占比喻再强调一次行锁你在柜台办手续时工作人员按住档案不让别人同时改业务锁办完后给你发「临时占座牌」15 分钟内座位算你的只有行锁、没有业务字段事务一提交别人立刻又能下单抢。只有业务字段、没有行锁两人可能同时读到空闲一起改成锁定。两样都要才是完整的「抢座」。四、怎么用行锁写法在 MySQL / InnoDB 里课设最常用、最好讲的就是SELECT ... FROM 表 WHERE 条件 FOR UPDATE;并且它必须放在事务里才有工程意义。4.1 最小模板请背这个骨架START TRANSACTION; -- 或 Go 里 tx.Begin() SELECT status, lock_user_id FROM seats WHERE id 101 AND schedule_id 12 FOR UPDATE; -- 锁住这一行 -- 在这里根据 status 判断 -- 然后 UPDATE / INSERT COMMIT; -- 成功提交放锁 -- 或 ROLLBACK; -- 失败回滚放锁Go 里对应tx, err : db.Begin() defer func() { _ tx.Rollback() }() err tx.QueryRow( SELECT status, lock_user_id FROM seats WHERE id? AND schedule_id? FOR UPDATE, seatID, scheduleID, ).Scan(status, lockUID) // 校验... // tx.Exec(UPDATE ...) // tx.Exec(INSERT ...) tx.Commit()4.2 几个写法要点① 一定要用事务对象tx不要混用// 正确查询和更新都走 tx tx.QueryRow(... FOR UPDATE) tx.Exec(UPDATE ...) // 错误锁在 tx 上更新走另一个连接 → 行锁等于白加 tx.QueryRow(... FOR UPDATE) db.Exec(UPDATE ...) // 危险② WHERE 尽量点到具体行并走索引WHERE id ? -- 好锁目标清晰 WHERE schedule_id 12 -- 可能锁很多行甚至触发更粗的锁你项目用id? AND schedule_id?目标是具体座位同时防止乱传别的场次 id。③ 事务要短事务里不要time.Sleep调用微信/支付宝等很久的外部接口超大循环无关操作锁拿得越久别人等得越惨。④ 失败要回滚Go 里常用defer func() { _ tx.Rollback() }()中途return err会自动撤成功再Commit()。五、什么情况下应该用行锁下面这些场景很适合行锁或同类互斥5.1 稀缺资源「谁拿走算谁的」选座、选房态、优惠券一券一人库存扣减也可用条件更新但行锁思路同类号码池取号特征同一行不能被两个事务同时成功占有。5.2 「先读后改」且中间不能插队模式是读当前状态 → 按状态做分支判断 → 写回新状态只要两个人可能对同一行做这三步就要考虑互斥。FOR UPDATE就是把三步包进同一把锁里。5.3 状态机迁移要串行例如订单用户支付定时任务超时取消两边都可能改同一订单。对订单行FOR UPDATE保证同一时刻只有一个迁移成功。你项目支付、取消、超时清理都锁了订单行。5.4 一句话判断题问自己如果两个请求同时跑到这段逻辑会不会把同一行改出矛盾结果会 → 考虑行锁或条件更新等互斥手段。不会只是各写各的日志、各插各的评论→ 通常不必。六、什么情况下不要乱用行锁6.1 纯展示查询首页列电影、刷座位图给用户看SELECT * FROM seats WHERE schedule_id?一般不要加FOR UPDATE。原因展示不需要独占加了只会让别人更新更慢。6.2 冲突极少、可以接受重试例如点赞数 1偶尔冲突用乐观锁/重试更轻。不是所有UPDATE都要先FOR UPDATE。6.3 可以一条条件更新搞定时UPDATE seats SET status1, lock_user_id? WHERE id? AND status0;若影响行数1 成功0 失败——这是另一种互斥偏乐观/条件写。简单扣库存常用这个。你选座因为要「读很多字段做复杂校验 插订单」用FOR UPDATE更清晰。6.4 跨很久的用户操作不要指望FOR UPDATE 锁住 等用户掏出手机扫码 5 分钟 再 Commit这会把行锁拿 5 分钟系统会卡死。正确做法事务快速结束长时间占用用业务字段锁定到lock_until七、结合你的项目行锁出现在哪7.1 下单抢座锁座位行tx.QueryRow( SELECT status,row_no,col_no,lock_user_id FROM seats WHERE id? AND schedule_id? FOR UPDATE, sid, scheduleID)然后已售 → 失败别人锁定 → 失败通过 → 插订单、把座位改成锁定两个用户抢同一sid后到的在FOR UPDATE等待你提交后他读到新状态校验失败7.2 支付 / 取消 / 超时锁订单行SELECT status,user_id,pay_deadline FROM orders WHERE id? FOR UPDATE防止一边支付、一边超时释放把状态改乱。7.3 用一张图串起来用户点下单 → BEGIN → 对每个座位 FOR UPDATE行锁 → 校验 → INSERT 订单 → UPDATE 座位为业务锁定 → COMMIT行锁释放但 status 仍是锁定 → 15 分钟内别人仍不能买业务锁 → 支付成功再锁订单行座位改已售 → 或超时锁订单行座位改回空闲八、慢慢看一段「带行锁」的完整逻辑用人话对应代码① Begin开启事务开始办事还没对外宣布 ② defer Rollback中途失败就当没发生 ③ FOR UPDATE按住这个座位档案 ④ 看状态卖了别人占了→ 不行就返回错误自动回滚 ⑤ 都 OK创建订单草稿 ⑥ 座位写上「我锁定了、锁到几点、对应哪张单」 ⑦ Commit对外宣布成功放开行锁其中③④是行锁发挥作用的时刻⑥是业务锁⑦之后行锁没了业务锁还在。九、常见误区基础差最容易踩误区 1以为 FOR UPDATE 可以单独写不需要事务工程上请永远事务包住 FOR UPDATE 后续写误区 2以为加了行锁整张表别人都不能动只影响冲突的行简化理解。其它座位照样能卖误区 3以为行锁能代替支付超时行锁很短。长时间占座必须靠lock_until 定时任务误区 4前端把按钮禁用了就不需要锁了前端防的是手滑防不了并发和抓包。互斥必须在服务器/数据库误区 5所有 SELECT 都加 FOR UPDATE展示接口不要乱加否则自己把自己锁慢误区 6表不是 InnoDB没有真正的行级事务锁体验。建表看一眼ENGINEInnoDB十、怎么自己验证「行锁生效了」方法 A双浏览器推荐两个账号打开同一场次同时选同一座位下单一人成功一人提示锁定/已售方法 B人为拖慢事务仅本地调试在Commit前临时Sleep(5秒)另一个请求会明显卡住。测完删掉。方法 C查库成功一方SELECT id,status,lock_user_id,order_id FROM seats WHERE id?;应看到锁定人是成功用户。十一、行锁相关的兄弟概念先混个眼熟以后学更深会遇到现在够用知道名字概念直觉共享锁 S / 排他锁 X行锁里 FOR UPDATE 偏排他我改你别改死锁A 锁1等2B 锁2等1数据库踢掉一个锁等待超时等太久报错innodb_lock_wait_timeout乐观锁不加长锁靠版本号/条件更新冲突就重试悲观锁先锁再干活FOR UPDATE 属于这类你的课设选型是悲观行锁适合「抢座位这种冲突可预期」的场景。十二、什么时候用速查表场景建议两人可能改同一座位/同一订单用行锁FOR UPDATE或等价互斥下单读状态分支写订单写座位事务 行锁支付与超时可能同时改订单对订单行加锁首页列表、座位图展示不用 FOR UPDATE用户扫码等 5 分钟不要持有行锁等用业务锁定字段简单 stock-1可考虑UPDATE ... WHERE stock0十三、选座是稀缺资源竞争。我在事务里对座位行使用SELECT FOR UPDATE行锁保证同一时刻只有一个事务能完成校验和锁定事务很快提交。支付窗口靠座位状态和lock_until业务锁定维持超时任务负责释放。这样既防并发超卖又不会把行锁拿太久。十四、小结行锁锁的是一行解决多人同时改同一行的冲突。常用写法SELECT ... FOR UPDATE必须放在事务里读写都走同一tx。适合抢座、库存互斥、状态机并发迁移。不适合纯展示也不要用行锁代替长时间业务占座。你项目里下单锁座位行支付/超时锁订单行——这就是标准用法