一、前言
本文主要是利用springboot,实现一个单机版秒杀demo,通过单机版实现,可以对基本并发秒杀的知识有一定的了解。
二、秒杀业务类实现
首先先提供秒杀业务实现类,该业务类中,定义秒杀方法,方法用synchronized修饰,单机应用增加悲观锁,注意,与@Transactional一起使用是,不会生效,如要要使用的话在调用该方法的地方使用synchronized代码块//原因:Transactional事务是在锁之前开始的,事务范围广,当一个线程锁释放了,但是事务还没提交,当下个线程过来是,一起提交上一次事务,一般不建议使用,线程会单个使用,降低效率,并且不要在业务代码增加synchronized
代码语言:javascript复制/**
* spring 注解加在实现类
*/
@Service
@Transactional
public class OrderServiceImpl implements OrderService {
@Autowired
private StockMapper stockMapper;
@Autowired
private StockOrderMapper orderMapper;
@Autowired
private StringRedisTemplate redisTemplate;
//方法用synchronized修饰,单机应用增加悲观锁
//注意,与@Transactional一起使用是,不会生效,如要要使用的话在调用该方法的地方使用synchronized代码块
//原因:Transactional事务是在锁之前开始的,事务范围广,当一个线程锁释放了,但是事务还没提交,当下个线程过来是,一起提交上一次事务
//一般不建议使用,线程会单个使用,降低效率,并且不要在业务代码增加synchronized
@Override
public int kill(Integer id) {
//加入redis缓存限时抢购,即使获得令牌如果不在活动时间内也是无法抢购
//校验redis中秒杀商品是否结束
if (!redisTemplate.hasKey("kill" id)) {
throw new RuntimeException("当前商品的抢购活动已经结束啦!");
}
//校验库存
StockDO stockDO = checkStock(id);
//扣减库存
updateStock(stockDO);
//创建订单
return createOrder(stockDO);
/* //根据id校验库存
StockDO stockDO = stockMapper.checkStock(id);
if(stockDO.getSale().equals(stockDO.getCount())){
throw new RuntimeException("库存不足。。。");
}else {
//扣减库存
stockDO.setSale(stockDO.getSale() 1);
stockMapper.update(stockDO);
//创建订单
StockOrderDO stockOrderDO = new StockOrderDO();
stockOrderDO.setSid(id).setName(stockDO.getName()).setCreateTime(new Date());
orderMapper.insert(stockOrderDO);
return stockOrderDO.getId();
}*/
}
private StockDO checkStock(Integer id){
StockDO stockDO = stockMapper.checkStock(id);
if(stockDO.getSale().equals(stockDO.getCount())) {
throw new RuntimeException("库存不足。。。");
}
return stockDO;
}
private void updateStock(StockDO stockDO){
//使用乐观锁
//在sql完成销量 1 和版本号的 1,并且根据商品id和版本号同时查询更新商品
//使用版本号,数据库不支持并发写
int updateRows = stockMapper.update(stockDO);
if(updateRows == 0){
throw new RuntimeException("抢购失败,请重试!!!");
}
}
private Integer createOrder(StockDO stockDO){
StockOrderDO stockOrderDO = new StockOrderDO();
stockOrderDO.setSid(stockDO.getId())
.setName(stockDO.getName())
.setCreateTime(new Date());
orderMapper.insert(stockOrderDO);
return stockOrderDO.getId();
}
}
二.使用synchronized关键字悲观锁
使用synchronized关键字悲观锁,防止超卖,使用悲观锁的话,对资源浪费比较大,每一次只允许一个线程访问,降低效率,其他的只能等待,显示是不合理的。不过如果真的要是用synchronized,不要在业务代码使用,必须在调用业务代码的地方使用同同步代码块,原因如下:
业务代码使用了@Transactional注解,一起使用是不会生效,因为Transactional事务是在锁之前开始的,事务范围广,当一个线程锁释放了,但是事务还没提交,当下个线程过来是,一起提交上一次事务。
代码语言:javascript复制 @GetMapping("/kill")
public String kill(Integer id){
System.out.println("秒杀商品id = " id);
try {
//根据秒杀商品id,去调秒杀业务
//调用处增加synchronized悲观锁
//先加锁,然后在开启事物,可以保证安全性。
synchronized(this){
int orderId = orderService.kill(id);
return "秒杀成功,订单id:" orderId;
}
}catch (Exception e){
e.printStackTrace();
return e.getMessage();
}
}
三、数据库层面加锁
数据库层面version版本号,乐观锁防止超卖,利用数据库不支持并发写,每一次只允许一个线程操作。数据增加version字段,每次修改都更加version去修改,并且数据增加销售量。
代码语言:javascript复制 <update id="update">
update stock
set sale = sale 1,
version = version 1
where id = #{id} and version =#{version}
</update>
使用测试发现利用乐观锁进行控制商品秒杀,同样的并发量,明显效率比较高,时间也是缩短一半左右。
四、Google guava 加锁
Google guava RateLimiter令牌桶算法接口限流,在利用乐观锁的实现超卖的前提下进行限流,因为是接口限流所以是在前端调用的时候进行限制。
这边插入一下限流算法的概念:
代码语言:javascript复制限流:是对某一时间窗口内的请求数进行限制,保持系统的可用性和稳定性,防止因流量暴增而导致的系统运行缓慢或者宕机
接口限流:在面临高并发的抢购请求时,我们如果不对接口进行限流,可能会对后台系统造成极大的压力,
大量的请求抢购成功时需要调用下单接口,过多的请求达到数据库时会对系统的稳定性造成影响
常用限流算法:令牌桶算法、漏斗算法(用的少),Google开源项目Guava中的RateLimiter使用的就是令牌桶控制的算法。
在实际开发高并发系统时有三把利器:缓存、降级、限流
缓存:缓存的目的是提升系统访问量速度和增大系统处理容量
降级:当服务压力剧增的情况下,根据当前业务情况及流量对一些服务和页面的降级(springcloud的hystrix),
以此释放服务器资源以保证核心任务的正常运行
限流:目的是通过对并发访问/请求进行限速,
或者对一个时间窗口内的请求进行限速来保护系统,一旦达到限制的速率则可以拒绝服务、排队或者等待、降级处理
漏斗算法(简单粗暴):漏桶(Leaky Bucket)算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),
当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求,可以看出漏桶算法能强行限制数据的传输速率
令牌桶算法:令牌桶算法(Token Bucket)和 Leaky Bucket 效果一样但方向相反的算法,更加容易理解.随着时间流逝,
系统会按恒定1/QPS时间间隔(如果QPS=100,则间隔是10ms)往桶里加入Token(想象和漏洞漏水相反,有个水龙头在不断的加水),
如果桶已经满了就不再加了.新请求来临时,会各自拿走一个Token,如果没有Token可拿了就阻塞或者拒绝服务.
设置超时时间,超过时间自己抛弃
详细算法介绍,可以参考:https://cloud.tencent.com/developer/article/2316325
代码实现:
代码语言:javascript复制 //创建令牌桶实例
private RateLimiter rateLimter = RateLimiter.create(10);
/**
* 秒杀方法
* @param id
* @return
*/
@GetMapping("/killToken")
public String killToken(Integer id){
//System.out.println("秒杀商品id = " id);
//秒杀前,执行限流操作,获得到了令牌才执行抢购
//@return {@code true} if the permit was acquired, {@code false} otherwise
//如果超过等待时间
if (!rateLimter.tryAcquire(1,TimeUnit.SECONDS)){
log.error("当前请求被限流,直接抛弃,抢购失败!!,当前活动过于火爆,请重试");
return "抢购失败!!,当前活动过于火爆,请重试";
}
try {
//根据秒杀商品id,去调秒杀业务
//调用处增加synchronized悲观锁
//先加锁,然后在开启事物,可以保证安全性。
// synchronized(this){
int orderId = orderService.kill(id);
log.info("秒杀商品id = " id);
return "秒杀成功,订单id:" orderId;
// }
}catch (Exception e){
e.printStackTrace();
return e.getMessage();
}
}
五、Redis缓存
4.Redis缓存抢购时间,主要是为了缓解数据压力,利用缓存在调用数据库前,判断是否秒杀活动结束了,并且秒杀的话存在的时间也不是很长,如果存在才进行数据库操作,所以即使获得的秒杀资格但是活动结束的话也是抢购失败,主要是在业务层进行控制。即数据存一个秒杀key,设置秒杀时间比如:set kill1 EX 180 表示秒杀key存在180秒
代码语言:javascript复制 //加入redis缓存限时抢购,即使获得令牌如果不在活动时间内也是无法抢购
//校验redis中秒杀商品是否结束
if (!redisTemplate.hasKey("kill" id)) {
throw new RuntimeException("当前商品的抢购活动已经结束啦!");
}
我正在参与2023腾讯技术创作特训营第二期有奖征文,瓜分万元奖池和键盘手表