秒杀系统技术点(GLODON TOT20)
超卖问题
秒杀的特点就是,短时间内系统的访问量骤增,系统请求的并发量突然暴增的情况下,这时候我们直接操作数据库不做任何处理的情况下,容易出现库存已经卖空后,继续扣除库存的情况,即总共有100件库存,总共卖出了110件商品,这个就与事实不符。
那么这个问题有什么解决方案吗,当然有,今天就来讲讲其中的一种解决方式——乐观锁。
当然,与之对应的就有悲观锁,Java中对应的就是使用synchronized关键字,悲观锁就是每次有一个线程进来操作,我就将它锁住,其它线程只能等待,只有当前线程处理完释放锁之后,其他线程才能拿到锁进行处理,这样,等库存为0之后,只有一个线程进来,发现卖完了就不进行处理,这也就防止了超卖问题的产生。
很明显,悲观锁有个很明显的缺点,就是每次只能有一个线程拿到锁进行处理,那么其他没拿到锁的线程等待时间较长,系统响应较慢。
乐观锁是什么呢?
在设计数据库时,在表里面多加一个version字段(数字Integer),用来标记当前版本号,当每一次对某一条数据进行修改时,版本号进行+1,当修改数据时,版本号与当前库里的版本号不一致则不能修改。
id | goods_name | stock | version |
1 | 可口可乐 | 99 | 1 |
2 | 汇源果汁 | 87 | 1 |
每次获取一条记录时:
select id, goods_name, stock, version from goods
映射为一个Java对象Goods的goods:
扣除库存的时候,顺便更新版本号:
goods.setId(goods.getId());
goods.setGoodsName(goods.getGoodsName());
goods.setStock(goods.getStock() - 1);
goods.setVersion(goods.getVersion() + 1);
goodsDao.updateById(goods);
条件加上version,当版本号不一致,则不能修改
update .... where id = #{id} and version = #{version}
这样,当同时有多个线程进来,获取到数据,每次只能有一个线程修改数据成功,即抢购成功,其他线程修改失败,则系统提示抢购失败。
高并发限流
当突然有成千上万个请求进入系统的话,或者说,有恶意攻击者使用工具发送恶意请求,服务器的压力是很大的,这时候,我们就必须考虑到限流
限流的话,有漏斗算法和令牌桶算法
漏斗算法,很形象,每次来的请求我先放在我漏斗上面的小空间内,从漏斗下面流出(通过)的速率的一定的,当漏斗上面的小空间存满的话,其他进来的请求就直接抛弃
令牌桶算法,也很形象,系统以恒定速度生成一定数量的令牌,这些令牌放在一个令牌桶里,当有线程进来时,先从桶里拿令牌,拿到了令牌就进行处理,没拿到令牌就直接抛弃请求(抢购失败)
springboot令牌桶的具体实现如下:
先导入pom依赖:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.2-jre</version>
</dependency>
接着创建令牌桶:
//创建令牌桶,用于对秒杀接口进行限流
private RateLimiter rateLimiter = RateLimiter.create(30);
当请求进来时,先从桶里拿令牌,拿到令牌就处理,拿不到令牌就抛弃:
//如果3秒之内没拿到令牌,则秒杀失败
if(!rateLimiter.tryAcquire(3, TimeUnit.SECONDS)){
log.info("秒杀失败,当前活动过于火爆,请重试");
return new ResponseModel(1, "秒杀失败,当前活动过于火爆,请重试", null);
}
//拿到令牌则接着处理秒杀业务
return productService.secKillProduct(secKillProductBO);
redis实现倒计时
实现倒计时功能主要是根据redis中可以设置一对key-value的生存时间
setex pro1 100 hello
上面这个命令就是设置pro1:hello这个键值对的生存时间为100秒,100秒之后redis会自动删除这条数据
那么,我们可以提前将需要秒杀的商品根据id存入redis中,key可以使用指定字符串加id,如pro1、pro2等,value就随便存一个值,这里主要是根据key来判断redis中是否存在此商品信息,如下:
setex pro1 1800 product
我将id为1的商品存入redis,并设置过期时间为1800秒,即半小时
当用户秒杀时,将商品id传入系统,后天做一个简单的字符串拼接
String key = "pro" + id;
然后根据这个key去redis中查询此key是否存在,如果存在,则进行正常的秒杀流程操作数据库,如果不存在,则代表商品未上架或者秒杀已结束,直接抛弃请求。