使用MySQL乐观锁解决超卖问题

在秒杀系统设计中,超卖是一个经典、常见的问题,任何商品都会有数量上限,如何避免成功下订单买到商品的人数不超过商品数量的上限,这是每个抢购活动都要面临的难点。

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://stackoverflow.com/questions/17431338/optimistic-locking-in-mysql

本文由博客一文多发平台 OpenWrite 发布!

版权声明:本文为博主原创文章,未经博主允许不得转载。
原文地址:https://www.cnblogs.com/dtyy/p/13829276.html