在秒杀系统设计中,超卖是一个经典、常见的问题,任何商品都会有数量上限,如何避免成功下订单买到商品的人数不超过商品数量的上限,这是每个抢购活动都要面临的难点。
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}
参考资料
https://*.com/questions/17431338/optimistic-locking-in-mysql
本文由博客一文多发平台 OpenWrite 发布!