一般电子商务网站会有团购,秒杀等活动,而这样的活动特点是请求量激增,数以万计的用户会抢购一个商品,这样会面临活动商品库存有限,高并发下如何控制库存不出现超卖的问题。
注意:
- 我们的数据存放在MySQL中
- 使用的语言是Java
为何会发生超卖
一般库存扣除的逻辑代码如下:
//remainder为剩余库存数量
int remainder=statement.query("select remainder from stock where stock_id='$STOCK_ID$'");
//amount为本次订单抢购数量
if(remainder>=amount!=0){
statement.execute("update stock set remainder=remainder-amount where stock_id='$STOCK_ID$'")
}
防止超卖我们首先想到的是通过事务去解决这个问题:
startTransaction();//开启事务
try{
//remainder为剩余库存数量
int remainder=statement.query("select remainder from stock where stock_id='$STOCK_ID$'");
//amount为本次订单抢购数量
if(remainder>=amount!=0){
statement.execute("update stock set remainder=remainder-amount where stock_id='$STOCK_ID$'")
}
}catch(Exception e){
rollback();//回滚
}
commit();//提交事务
上面的事务下执行逻辑其实隐藏一个很大的漏洞-有可能库存会变为负数(即超卖),具体分析如下:
- 由于是高并发,假设有三个用户a,b,c同时抢购该物品,并进入到了这个事务中,这三个用户查到的库存数是一样的(MySQL rr级别下总是读取事务开始时的行数据)
- 然后进入到update,假设这三个用户同时进入update操作,这个时候由于 行级锁的排他性限制,MySQL会将update操作串行化
- 上面update执行完后,有可能会发生库存变为负数的情况(超卖)
怎么解决超卖
最简单的方式("锁方式")
针对上述超卖的情况我们可以通过更改下执行sql代码来实现:
startTransaction();//开启事务
try{
statement.execute("update stock set remainder=remainder-amount where stock_id='$STOCK_ID$' and $remainder>=$amount")
}
}catch(Exception e){
rollback();//回滚
}
commit();//提交事务
上面的修改可以杜绝库存超卖的现象。注意以上在MySQL一致性非锁定读(rr隔离级别下)。
针对以上,我们可以换个思路,在库存数据结构中加入version字段来控制记录修改版本,也可以解决上述问题,如下:
startTransaction();//开启事务
try{
//remainder为剩余库存数量
int remainder=statement.query("select remainder,version from stock where stock_id='$STOCK_ID$'");
//amount为本次订单抢购数量
if(remainder>=amount!=0){
statement.execute("update stock set remainder=remainder-amount,version=version+1 where stock_id='$STOCK_ID$' and version=$version")
}
}catch(Exception e){
rollback();//回滚
}
commit();//提交事务
但是我们真的能这么做吗?我们的业务可是高并发,面对的是1万+TPS,那如果按上面继续执行会遇到什么问题?
很显然不能,在高并发下,会有很多这样的修改(update),每个请求都需要等待"锁",某些请求可能永远都获取不到锁,这种请求就会卡在那里,直到超时。同时,由于这种写请求很多,会造成大量的请求超时,连锁反应就是应用系统连接数被耗光,直至系统异常crash。即使重启系统,由于请求量大,系统也会立马挂掉。
高并发下如何解决超卖
引入缓存
主要思路是:
- 首先在团购秒杀开始前将需要的物品库存信息放入缓存中
- 使用锁来处理其并发请求
- 将缓存中的数据同步到数据库。
我们此处使用redis作为缓存。
应用操作redis减库存的大体思路为:
- 首先通过redis api监听相关物品的库存信息,在事务开启前保证该物品库存信息无人修改
- 获取现有库存信息,判断库存不为0并且当前库存量大于等于订单所需数量
- 满足上述2的话则进行扣除操作
- 如果在1的过程中有别人更新了该物品库存信息版本,则重试
- 知道库存为0或者剩余库存不满足当前订单扣除数量退出
具体代码如下:
public void secondBuyProduct(Jedis jedis, String stockId, int orders) {
//CAS重试
while (true) {
try {
//监视key,如果在后续事务执行之前key的值被其他命令所改动,那么事务将被打断
jedis.watch(stockId);
int prdNum = Integer.parseInt(jedis.get(stockId));
//判断库存是否满足订单数量要求
if (prdNum > 0 && prdNum - orders >= 0)) {
Transaction transaction = jedis.multi();
//减库存并写入
transaction.set(stockId, String.valueOf(prdNum - orders));
List<Object> res = transaction.exec();
//事务提交后如果为null,说明key值在本次事务提交前已经被改变,本次事务不执行。
if (res != null && !res.isEmpty()) {
System.out.println("抢购成功!");
break;
}
} else {
System.err.println("被抢光了!");
break;
}
} catch (Exception e) {
System.err.println("抢购出错:" + e.toString());
e.printStackTrace();
} finally {
jedis.unwatch();
}
}
}
网友评论