小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
场景
在平时的工作中,常常会有要对数量有精确控制的业务需求。比如商品库存量、奖品数量、报名人数限制等等,这些场景往往并发较高。拿减商品库存场景来说,如果控制不好,很有可能出现超卖的现象。
方法一:使用文件排它锁
flock 函数用于获取文件的锁,这个锁同时只能被一个线程获取到,其它没有获取到锁的线程,要么阻塞,要么获取失败。
在获取到锁的时候,先查询库存,如果库存大于 0,则进行下订单操作,减库存,然后释放锁。
方法二:使用悲观锁
InnoDB 存储引擎支持行级锁,当某行数据被锁定时,其它进程不能对这行数据进行操作。
在 InnoDB 存储引擎中,当我们执行 update 语句的时候,会自动为该行加一个行锁。
update goods set repertory = repertory - 1 WHERE goods_id = 1234 and repertory > 0
复制代码
另外一种方式,使用 for update
显式加锁,使用 for update
需要满足两个条件:
-
引擎是 InnoDB
-
操作需要在事务中(begin/commit)
begin;
select repertory from goods where goods_id = 1234 for update;
if (repertory > 0) {
update goods set repertory = repertory - 1 where goods_id = 1234;
}
commit;
复制代码
悲观锁采用的是「先获取锁再访问」的策略,但是这种策略会增加数据库的负担,且有可能会导致死锁。在实际生产中,对于高并发的场景并不会使用悲观锁,因为当一个事务锁住了记录,那么其他事务都会发生祖泽,大量的事务阻塞可能会拖垮整个系统。
方法三:使用乐观锁
乐观锁是总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和 CAS 算法实现。
我们以版本号方法为例。
版本号机制一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数,当数据被修改时,version 值会加一。当线程 A 要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值与当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。
select version as old_version from goods where goods_id = 1234;
update goods set repertory = repertory - 1, version = version + 1 where goods_id = 1234 and repertory = 0 and version = ${version};
复制代码
假设此时有 3 个库存,此时 version = 1,6 个请求都过来了,都执行上述语句,查到的 version 都为 1,但是肯定有一线程先执行成功,此时 version = 2,那么其他 update 语句发现 version 不等于上次 select 出来的 version,说明这次 version 被其他请求修改过了,就会放弃这次 update。
方法四:使用队列
我们可以使用 Redis 队列方案来解决高并发问题,我们用一个队列来接收请求,然后再一个一个将数据 pop 出来进行处理。
我们用订单号、用户 ID、商品 ID、购买数量等,生成一个唯一 ID,然后将该值 push 到订单队列中,再启动其他线程消费该数据。
lpush repertory orderId_goodsId_accountId_count
复制代码
方法五:使用 Redis
在高并发情况下,频繁的读库写库,会造成严重的性能问题。我们可以使用 Redis。
Redis 的操作都是原子性的,可以将商品的库存写入 Redis 中。下单之前对库存进行 DECR
操作,如果返回结果 >=0
则可以正常下单、减库存。否则提示库存不足。
172.18.4.181:7006> set repertory 2
OK
172.18.4.181:7006> DECR repertory
(integer) 1
172.18.4.181:7006> DECR repertory
(integer) 0
172.18.4.181:7006> DECR repertory
(integer) -1
复制代码