当前位置:   article > 正文

springcloud——并发请求处理方案_springcloud高并发解决方案

springcloud高并发解决方案

目录

1.业务逻辑处理

2.数据库层面保底

3.利用mysql update行锁

4.基于redis控制请求数量

5.利用mq进行流量削峰

6.使用限流算法

7.使用分布式锁


生活中的抢购、抢订等活动遍地都是,比如双11抢购商品、抢占活动名额、抢火车票等。而这些活动往往伴随的是大量的并发请求访问服务器,如果处理不当,会使得服务器崩溃导致活动无法正常展开。

具体来说,我们可能会面对这样的问题:我们上架了一件商品,库存为100,进行秒杀活动。当活动开始时,用户进行抢购,并发发送了1000个请求过来,远远大于库存数量。此时,我们面对并发请求,需要保证:

  • 不出现超卖现象,即库存不为负数
  • 限制进入数据库请求的数量,保证数据库服务器不蹦。
  • 避免用户恶意请求,时间内频发点击请求需拒绝
  • 避免业务处理过程过长,即保证运行时间

针对大量的并发请求,我们一般可以基于以下方案进行处理。

1.业务逻辑处理

如果活动开始时我们无法应当过多的请求,那我们首先可以考虑业务逻辑上先对请求进行筛减。比如限制双十一的预购活动,就是让用户先进行报名,拥有名额的用户才可以在活动开始时发送请求,从而做到业务层面上对请求进行减少。

不同的业务需要根据实际进行设计。

2.数据库层面保底

面对并发请求,困难的往往是数据库中的写操作,读操作是没有并发性问题,所以我们可以在数据库使用乐观锁对数据库数据进行保底。

乐观锁:即认为任务并不是所有请求都会同时修改数据,只是在修改的时候判断是否被其他线程修改了,如果是,此处操作撤销。

原理:mysql进行update操作时,会自动判断version是否为当前版本号(即当读取的version=1,update的时候会把version=1也作为where里面的判断条件),执行结束后把该行数据的version+1。

由于mysql中update操作是加锁进行的,所以两个请求并发过来时,两个请求读到的version都为1:前一个请求先执行update操作,把 version改为2;后一个请求执行update前判断 version为2,与读取到的1不一致,撤销操作。

实现方法:我们可以通过mybatis中的@version注解标记的字段作为版本号(乐观锁字段),然后在数据表中建立version字段即可

3.利用mysql update行锁

与乐观锁同理,由于mysql中update操作是加锁进行的,我们可以在update语句时添加判断条件帮助保证数据库层面不会出现超卖。即两个请求并发过来时,两个请求读到的库存都为1:前一个请求先执行update操作,把库存-1改为0;后一个请求执行update前判断库存为0,不符合update条件,不执行操作。

实现方式:update table set count = count -1 and count>0

4.基于redis控制请求数量

我们可以利用redis的原理性递减来应对海量的请求,控制进入数据库操作的人数。

原理:redis单线程性能高,操作内存快;多线程存在上下文切换,且多线程为保证线程安全需上锁,导致效率变慢。redis的原子性递减操作可以保证请求一个接一个进行递减,从而保证了进入的请求数量。

实现方式:首先,我们可以通过定时任务等方式,在活动开始前将库存(即允许的请求数量)缓存进redis中,然后使用redis的increment操作进行减库存,然后进行判断,当递减后库存小于0,则抛出异常。

demo

定时任务:

  1. //定时任务,缓存库存
  2. @Scheduled(cron = "0 0 6 * * ?")
  3. //秒 分 时 日 月 周 年(可选);*表所有可能的值,-指定范围值,/表示步长
  4. public void updateSeckillStatue(){
  5. log.info("修改球场状态及秒杀时间");
  6. List<SeckillGoods> list = seckillCourtMapper.selectList(null);
  7. for (SeckillGoods goods:list ){
  8. //将库存缓存进redis
  9. redisUtil.set("seckill-goods:" + goods.getId(), goods.getCount());
  10. }
  11. }

 在秒杀接口进行判断:

  1. //判断库存,redis原子递减
  2. long count = redisUtil.decr("seckill-goods:" + goods_id, 1);
  3. if (count < 0){
  4. //该球场已被订购,抛出异常
  5. throw new GlobalException(SeckillCodeMsg.SECKILL_COURT_NULL);
  6. }

注意: 缓存进redis中的库存只是用来控制进入service层人数的,不是真正的库存。

5.利用mq进行流量削峰

我们可以使用mq进行异步处理,设置最大消费者的数量。从而减少进入数据库请求人数,达到流量削峰的目的:

原理:rabbitmq队列是先进先出的顺序,先来后到。即1000个请求并发,根据最大消费者数量对请求进行限制:如果最大消费者的数量为100,库存为100,则前100个请求抢单成功之后就注定了后900个请求是抢单失败的。

实现方式: 在配置文件中配置最大消费者数量,然后controller中接口作为生产者,service业务类作为消费者,对消息进行监听。

demo:(此处Jackson序列化类省略)

配置文件:

  1. spring:
  2. rabbitmq:
  3. host: localhost
  4. username: guest
  5. password: guest
  6. port: 5672
  7. publisher-confirm-type: correlated #发布确认模式:correlated即交换机收到消息后触发回调方法
  8. publisher-returns: true #回退消息,当找不到routing key对应的队列时,是否回退信息
  9. listener:
  10. simple:
  11. concurrency: 10 #消费者最少数量
  12. max-concurrency: 100 #消费者最大数量
  13. prefetch: 1 #消费者每次处理一条消息
  14. auto-startup: true
  15. default-requeue-rejected: false #消息被拒绝是否重新进入队列

 生产者:

  1. OrderMessageDto orderMessageDto = new OrderMessageDto(seckill_id,userId,ip);
  2. //rabbitmq异步处理订单
  3. rabbitTemplate.convertAndSend(EXCHANGE_ORDER,"order_route", orderMessageDto,
  4. new CorrelationData(String.valueOf(seckill_id)));

 消费者:

  1. @RabbitListener(queues = QUEUE_ORDER)
  2. @Transactional(rollbackFor = GlobalException.class)
  3. public void receiveOrderMsg(@Headers Channel channel, Message message) {
  4. log.info("订单队列接收到消息:"+new String(message.getBody()));
  5. OrderMessageDto orderMessageDto = JSON.parseObject(new String(message.getBody()), OrderMessageDto.class);
  6. //捕捉异常,方便业务失败时进行回滚
  7. try {
  8. //业务代码逻辑
  9. //确认消息消费成功
  10. channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
  11. }catch (Exception e){
  12. log.info("订单处理异常");
  13. e.printStackTrace();
  14. }
  15. }

此处省略路由异常callback相关代码,需要请参考:Springboot集成rabbitmq——消息持久化-CSDN博客

一般情况下,我们会在最后创建订单+减数据库库存时使用mq进行异步处理。

6.使用限流算法

可以通过计数器法限流,或是springcloud gateway使用令牌桶算法限制请求数。

计数器算法实现方式:基于redis计数器算法对端口进行限流处理。使用redis缓存访问次数,并定义过期时间,过期时间内达到对应值,则限制访问。主要通过自定义注解+aop的形式对接口进行限流,限制每秒请求数。

demo:

注解:

  1. @Retention(RetentionPolicy.RUNTIME)
  2. @Target({ElementType.METHOD})
  3. @Documented
  4. public @interface RequestLimit {
  5. //允许访问次数
  6. int permitRequest();
  7. //过期时间
  8. int second();
  9. }

 aop切面:

  1. @Slf4j
  2. @Aspect
  3. @Component
  4. public class RequestLimitAop {
  5. @Resource
  6. private AuthJwtProperties authJwtProperties;
  7. @Resource
  8. private RedisUtil redisUtil;
  9. @Resource
  10. private JwtTokenUtil tokenUtil;
  11. @Around("@annotation(com.seven.seckill.annotation.RequestLimit)")
  12. public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
  13. MethodSignature signature = (MethodSignature) joinPoint.getSignature();
  14. Method method = signature.getMethod();
  15. //拿limit的注解
  16. RequestLimit limit = method.getAnnotation(RequestLimit.class);
  17. if (limit != null) {
  18. //允许的请求数
  19. int requestCount = limit.permitRequest();
  20. //key过期时间
  21. int second = limit.second();
  22. //获取用户id
  23. ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
  24. String token = attributes.getRequest().getHeader(authJwtProperties.getHeader());
  25. //处理前缀
  26. if (StringUtils.isNotEmpty(token) && token.startsWith(TokenConstants.PREFIX))
  27. {
  28. token = token.replaceFirst(TokenConstants.PREFIX, StringUtils.EMPTY);
  29. }
  30. //从token中获取用户id
  31. String userId = tokenUtil.getUserIdFromToken(token);
  32. Integer flag = (Integer) redisUtil.get("cloud-court-seckill:"+method.getName()+":"+userId);
  33. if (flag==null){
  34. //设置计数器,second 秒过期
  35. redisUtil.set("cloud-court-seckill:"+method.getName()+":"+userId, 1, second);
  36. }else if (flag < requestCount){
  37. //请求数+1
  38. redisUtil.incr("cloud-court-seckill:"+method.getName()+":"+userId,1);
  39. }else {
  40. //限流
  41. log.warn("请求过于频繁");
  42. throw new GlobalException(ExceptionCodeMsg.SYSTEM_BUSY);
  43. }
  44. }
  45. return joinPoint.proceed();
  46. }
  47. }

将注解放于接口上:

  1. @RequestLimit(permitRequest = 5,second = 2) //限制用户每2秒点击次数为5
  2. public Result<?> test(){
  3. return new Result<>(ResultEnum.SUCCESS);
  4. }

注意:次方法一般用于限制单个用户的频繁点击(所以代码中使用用户id做redis中点击次数的key),需要用户先登录才可以访问接口。

基于springcloud gateway的令牌桶限流算法可以参考:springcloud——gateway功能拓展_tang_seven的博客-CSDN博客

7.使用分布式锁

最后,如果需要对抢购活动进行进一步保障,可以使用分布式锁。

具体可以参考:Springboot集成Redis——实现分布式锁-CSDN博客

可以确定的是,上锁必定会影响代码的效率(由于需要一个一个排队进行处理),所以上锁的代码段不易过长,需要自行进行设计。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/天景科技苑/article/detail/1022274
推荐阅读
相关标签
  

闽ICP备14008679号