在秒杀系统设计中, 超卖是一个经典, 常见的问题, 任何商品都会有数量上限, 如何避免成功下订单买到商品的人数不超过商品数量的上限, 这是每个抢购活动都要面临的难点.
1 超卖问题描述
在多个用户同时发起对同一个商品的下单请求时, 先查询商品库存, 再修改商品库存, 会出现资源竞争问题, 导致库存的最终结果出现异常.
问题: 当商品 A 一共有库存 15 件, 用户甲先下单 10 件, 用户乙下单 8 件, 这时候库存只能满足一个人下单成功, 如果两个人同时提交, 就出现了超卖的问题.
可以采用多种方式解决超卖问题. 使用 synchronized 可以保证数据一致性, 但是效率低, 并且分布式环境下无用; 使用数据库锁表会造成数据库性能低下. 单体条件下, 采用乐观锁是比较合适的方式, 集群可以考虑分布式锁.
2 乐观锁
2.1 乐观锁介绍
悲观锁, 认为数据很容易被其他线程修改, 为保证数据正确性, 每次获取并修改数据时, 对数据加锁. 例如 Java 中的 synchronized 和 Lock 相关类.
而乐观锁, 认为自己在操作时不会有其他线程干扰, 所以不对被操作对象加锁. 在更新时会判断修改期间是否有其他线程修改过. 如果没被修改过, 则表示只有当前线程在操作, 正常修改数据. 如果数据被其他线程修改过, 则会停止刚才的更新, 选择执行策略, 例如抛弃, 报错, 重试等.
乐观锁一般使用 CAS 算法实现. 例如 Java 中的原子类, 并发容器.
2.2 没有锁的更新操作
乐观锁, 不是数据库功能, 是一种数据库实践. 假设进行以下操作: 从表中获取某行数据, 计算数据, 更新数据该行数据.
- CREATE TABLE theTable(
- iD int NOT NULL,
- val1 int NOT NULL,
- val2 int NOT NULL
- )
- INSERT INTO theTable (iD, val1, val2) VALUES (1, 2 ,3);
没有锁的处理
-- 查询数据
- SELECT iD, val1, val2
- FROM theTable
- WHERE iD = @theId;
-- 计算新值
-- 更新数据
- UPDATE
- theTable
- SET
- val1 = @newVal1,
- val2 = @newVal2
- WHERE
- iD = @theId;
-- 继续执行
2.3 乐观锁的实现方式 1-- 条件控制
-- 查询数据
- SELECT iD, val1, val2
- FROM theTable
- WHERE iD = @theId;
-- 计算新值
-- 更新数据
- UPDATE
- theTable
- SET
- val1 = @newVal1,
- val2 = @newVal2
- WHERE
- iD = @theId
- AND val1 = @oldVal1
- AND val2 = @oldVal2;
-- 判断影响行数
-- {if AffectedRows == 1 }
-- {继续执行}
-- {else}
-- {数据过期}
-- {endif}
上面操作的关键在于, UPDATE 指令的结构与后续受影响的行数检查, 从而判断是否有人修改数据. 上面所有操作没有使用事务, 这也表明乐观锁的关键不在于事务本身.
2.4 扩展: 事务的使用
-- 查询数据
- SELECT iD, val1, val2
- FROM theTable
- WHERE iD = @theId;
-- 计算新值
-- 开始事务, 更新数据
- UPDATE
- theTable
- SET
- val1 = @newVal1,
- val2 = @newVal2
- WHERE
- iD = @theId
- AND val1 = @oldVal1
- AND val2 = @oldVal2;
-- 判断影响行数
- -- {
- if AffectedRows == 1
- }
- -- COMMIT TRANSACTION; // 提交事务
-- {继续执行}
- -- {
- else
- }
- -- ROLLBACK TRANSACTION; // 回滚事务
-- {数据过期}
-- {endif}
使用了事务, 便可以回滚修改. 通过事务, 我们可以确定每次回滚的操作量是多少, 在何处放置事务边界以及在何处检查冲突.
对于其他进程在当前事务提交之前, 会发生什么, 取决于数据库当前的隔离级别. 以 SQL Server 为例, 其隔离级别是 READ_COMMITTED, 更新的行被锁定, 直到 COMMIT 为止, 因此 "其他进程" 无法对该行执行任何操作 (保持等待状态), 而 SELECT(实际上只能执行 READ_COMMITTED) .
2.5 乐观锁的实现方式 2-- 版本号
使用版本号, 也是乐观锁常用实现方式. 通过在表中增加一个 version 字段: 读取数据时, 将 version 字段值一并读出, 数据更新一次, 则 version 值加 1. 当我们提交更新时, 判断表中最新的 version 值与之前读出的 version 值是否一致, 如果一致, 则更新, 否则视为过期数据.
-- 查询数据
- SELECT iD,val1,val2,VERSION
- FROM theTable
- WHERE iD = @theId;
-- 计算新值
- UPDATE
- theTable
- SET
- val1 = @newVal1,
- val2 = @newVal2,
- VERSION = VERSION + 1
- WHERE
- iD = @theId
- AND VERSION = @oldversion;
-- 判断影响行数
-- {if AffectedRows == 1 }
-- {继续执行}
-- {else}
-- {数据过期}
-- {endif}
参考资料
来源: http://www.bubuko.com/infodetail-3683449.html