高并发秒杀系统初见 简单记录学习视频内容

214 阅读1分钟

高并发秒杀系统初见

语言是自己总结的,可能会有很多说不到位的地方。

所用到的:mysql=》redis=》zookeeper

测压工具:Jmeter

数据库表:

iShot2021-12-01 14.35.03.png

接口流程:访问秒杀接口 localhost:8080/test/buy 所需参数productId,完成购买后表中库存量-1,order表生成一个新的购买订单,返回给前端“购买成功”。反之返回“已售空”。

单线程和多线程对数据库的操作

对我来说在项目中经常用到的update语句是这样的:

Product product = productMapper.selectOne(new QueryWrapper<Product>().eq("ID",productId));
product.setStock(product.getStock()-1);
productMapper.update(product,new QueryWrapper<>());

这种做法是通过代码运算的方式,将代码中得到的差值塞进表里

在只有单个请求进来的情况下这个应该不会出错,但是在多线程的情况下,就容易造成错误。

比如表里的stock为10,当三个用户同时请求接口时,stock这个值分别减了三次1,但是得到的结果都是9,于是明明有三个用户请求接口成功,表中的数据却是9而不是7

在多线程情况下,加锁可以解决问题

代码层面上的加锁效率过低,第一个解决方法是把这个锁教给mysql自带的乐观锁处理

语句:

update product set stock = stock-1 where id = #{productId} and stock >= 0

Mysql的操作具有原子性,所以不会出现减错误的情况

mysql的压力测试

使用jmeter对mysql进行压力测试,同时请求接口1000次,重复十次,达到万数级别的接口压力。按照视频的测试结果来看,每秒吞吐量只有三百左右。

iShot2021-12-01 12.01.38.png

测试结果:表数据一切正常,未发生数据错误的情况,在本机上的吞吐量达到了近两千。

iShot2021-12-01 12.03.18.png

也就是说相对于我这张表和目前简单的运行逻辑来说,用mysql其实也能马马虎虎地通过了。

从mysql到redis

升级方案:秒杀开始前对所有商品进行init到redis的操作,通过redis的原子减,直接减少库存量,再同步到mysql

@Autowired
private StringRedisTemplate stringRedisTemplate;
​
public void init(){
  List<Product> products = productMapper.selectList();
  products.forEach(o->{
    stringRedisTemplate.put(o.getId(),o.getStock();)
  });
}
​
​
int stock = stringRedisTemplate.decrement(productId); //stock为减后的库存数
if(stock < 0){
  return "已售空";
}

经测试,完成此步优化后的吞吐量比起上面单纯用mysql的高了三四倍。

redis到mysql的同步漏洞

在从redis的预减库存到mysql的真正减库存中间,当代码出现bug或者抛出错误时就会出现两边数据不对等的情况,造成错误。

解决方法:

try{
逻辑代码
}catch (Exception e){
stringRedisTemplate.increment(prodectId); //原子加,将减少的库存补回去
}

多级缓存 从redis到jvm

即在redis缓存的基础上再加一层jvm级别的判断,创建一个常量ConcurrentHashMap,当redis中的stock值已经为0时,set一个boolean进去。每次进入秒杀方法先对这个boolean的存在进行判断,如果存在,即已经售空,接口直接返回,不会打到redis上去。

在分布式架构上会遇到的问题

上面所说的ConcurrentHashMap确实可以在单体架构上保证线程安全,但如果引入分布式架构,还是会产生问题,因此需要引入分布式锁。

分布式锁通常由mysql、redis和zookeeper来完成,实战中一般还是用redis较多,zookeeper能支撑的并发量并不大。但是这里还是简单阐述一下zookeeper实现上述ConcurrentHashMap同步的原理:

在zookeeper建立一个节点,可以以productId为key,ture or false为value,当有一台服务器上的map中出现了售空标记,就将这个节点的value改成false,代表已售空,其他机器在监听到这个节点变化的同时,也将自己的map进行修改。

这里会出现的问题时,无论是zookeeper监听项目还是服务器监听zookeeper,中间都会产生延迟,而在这个时候请求已经过来并且直接打在redis上了,就会拖慢吞吐量。

redis分布式锁在这里可以简单引用做存放一个<productId,boolean>的键值对,每次进入方法对时候先去判断这个键值对。不过这也有一个问题,就是上一步的优化无效了,从jvm层面又回到了redis

\