0%

关于大流量下防止超卖的思考

简介

最近在思考如何在大流量下防止超卖的问题,其实在并发下防止超卖或者其它一些并发问题,本质上绕不开锁的问题,因为如果没有锁无法保证数据同步的问题(至少是目前还没想到其它方法),这边进行了一次尝试.

悲观锁

一般悲观锁都由数据库等底层组件时间,比如在mysql的命令行中我们可以开启一个事务,然后再执行select * from table for update,然后再执行其它增删改操作,最后再提交事务即可,悲观锁就是你在select * from table for update时直接锁定这条数据,其它任何操作都要等到锁的释放才能进行操作(这里是事务的提交),在查找网上资料后悲观锁好像在代码中并没有什么比较好的实践方案所以放弃.

乐观锁

其本质就是在数据库字段的最后加一个版本号或者更新时间的字段,在操作时先查询库存和版本信息,如果大于1则只需update操作并且在where中带上版本号或者更新日期,如果版本号和更新日期和之前获取的时候不匹配则更新失败,这样对数据变相的加锁,保证一次只能一个请求操作数据保证并发安全,在并发情况下我们可以做一些优化,比如将请求写入到队列中,由消费者去消费,消费者由于乐观锁操作失败时加入一定的重试次数等

redis

redis的一些操作中保证了数据的原子性操作如list 的push和pop,那么我们可以应用redis的这些特性来实现此功能,在list中初始化你要卖货物的数量,然后再一个个pop出来直到list不为空为止。如果将所有库存信息都放在一个list中卖大量货物时会导致消费过慢,因此我们可以将库存信息存放在多个list中,一个list中只保存相对量的库存比如1000个,但是这样做还需要加入额外的代码逻辑,比如一个list过早的消费网,那么他就要去其它list中获取库存信息

代码

乐观锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RequestMapping("sell")
public String sell() {
//获取库存信息,你们保存库存量和最后更新时间
Stock stock = stockMapper.findNum("1");
//如果没有库存
if(stock.getNum()<1){
return "out";
}
//如果由于乐观锁导致无法更新成功,重试20次
for (int i = 0; i < 20; i++) {
//将版本信息,id,和当前时间传入数据库更新库存
int result = stockMapper.changeStock("1", stock.getNum() - 1, new Date(), stock.getUpdate());
if (result == 1) {
return "success";
}
}
return "fail";
}
redis
1
2
3
4
5
6
7
8
9
10
11
12
//初始化redis信息
@RequestMapping("initStock")
public void initStock(int num){
List<String> cargoes=new ArrayList<>();
for(int i=0;i<num;i++){

cargoes.add("cargo_____"+i);
}
redisUtil.pushAll(cargoes);


}
1
2
3
4
5
6
7
8
9
10
11
//销售货物
@RequestMapping("sell")
public void sell(){

String cargo=redisUtil.sell();
if(cargo!=null){
//这里并没有加入任何锁
stockMapper.sell("1");
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//redis 代码
//初始化库存
public void pushAll(List<String> cargoes){

redisTemplate.opsForList().leftPushAll("stock_num",cargoes);


}

//销售商品
public String sell(){

Object object= redisTemplate.opsForList().leftPop("stock_num");
return object!=null?object.toString():null;
}

测试

实际中我将库存设置为1000,再使用Jmeter测试工程开启100个线程,每个线程循环200次去重复调用http接口,均能保证不超卖