秒杀demo

秒杀demo

**b站秒杀demo**
有些代码可能有些错误和不妥,欢迎指正

秒杀方案

在这里插入图片描述
数据库需要新的秒杀商品及秒杀订单表(方便后期更改维护)

秒杀倒计时设置:

例子:
在这里插入图片描述
秒杀基本流程(未优化):
在这里插入图片描述

压力测试:

使用jmeter进行压力测试,下文链接有详细的描述:
Jmeter性能测试

linux安装mysql

ubuntu安装mysql8

服务优化

页面缓存,对象缓存

前后端未分离时做页面缓存。
对象缓存,相比于页面缓存,具有更细的粒度。

页面静态化

库存超卖问题

超卖问题产生在秒杀商品减少库存部分。
秒杀实现步骤为:

减少库存->生成订单->生成秒杀订单

减少库存前判断库存是否大于0,
为防止一个人重复抢购(同时发起两个请求)可以加锁(乐观锁)/加唯一索引:
将用户id和商品id绑定成唯一索引,(开启事务:@Transactional)
在这里插入图片描述

rabbitmq安装使用

安装:windows下安装rabbitmq

springboot集成rabbitmq

引入依赖:

  <!--amqp-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

在yml中配置:

#在spring的下级
  #RabbitMQ
  rabbitmq:
    #服务器
    host: 127.0.0.1
    #用户名
    username: guest
    #密码
    password: guest
    #虚拟主机
    virtual-host: /
    #端口
    port: 5672
    listener:
      simple:
        #消费者最小数量
        concurrency: 10
        #消费者最大数量
        max-concurrency: 10
        #限制消费者每次只能处理一条消息,处理完在继续下一条消息
        prefetch: 1
        #启动是默认启动容器
        auto-startup: true
        #被拒绝时重新进入队列
        default-requeue-rejected: true
    template:
      retry:
        #发布重试,默认false
        enabled: true
        #重试时间,默认1000ms
        initial-interval: 1000ms
        #重试最大次数,默认3次
        max-attempts: 3
        #最大重试间隔时间
        max-interval: 10000ms
        #重试的间隔乘数,比如配2。0  第一等10s 第二次等20s 第三次等40s
        multiplier: 1

创建配置类:
在这里插入图片描述定义简单的发送者和接收者:

@Service
@Slf4j
public class MQReceiver {

    @RabbitListener(queues = "queue")
    public void receive(Object msg) {
        System.out.println("接收到的消息" + msg);
    }

}

@Service
@Slf4j
public class MQSender {

    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    public void send(Object msg) {
        log.info("发送消息:" + msg);
//        rabbitTemplate.convertAndSend("queue", msg);
        rabbitTemplate.convertAndSend("queue", msg);
    }
}

测试代码:

@RestController
@RequestMapping("/user")
@Api(value = "用户表", tags = "用户表")
public class TUserController {
    @Autowired
    private MQSender mqSender;

    @RequestMapping(value = "/mq", method = RequestMethod.GET)
    @ResponseBody
    public void mq() {
        mqSender.send("Hello");
    }
}

访问下面的接口就可以在mq的management中看到相关信息:
在这里插入图片描述

rabbitmq交换机模式

Rabbit中的消息传递模型中,生产者是发送消息的用户应用程序。队列是存储淌息的缓冲区。使用者是接收消息的用户应用程序。RabbitMQ消息传递模型的核心思想是生产者从不直接向队列发送任何消息。生产者只能向交换器发送消息。交换是—件很简单的事情。它一边接收来自生产者的消息,另一边将消息推送到队列。交换器必须确切知道如何处理它接收到的消息。它应该被附加到一个特定的队列吗?它应该被附加到多个队列吗?或者它应该被丢弃。它的规则由交换类型定义。
交换机类型:direct(直连),Fanout(广播模式),topic模式(使用最频繁),Headers模式(效率低,很少用)
下文有详细介绍:
交换机模式介绍

如下图direct模式下可以选择不同的路由从而转发到对应的队列。
在这里插入图片描述如下图为topic模式,也是有路由key,但为了方便管理路由key引入了通配符
在这里插入图片描述*匹配精确的一个,#匹配0个或者多个
在这里插入图片描述

redis预减库存

为减轻数据库压力,对redis进行预减库存操作。
系统初始化时,将商品库存加载到redis中,当收到秒杀请求时,redis预先减少库存(避免对数据库进行操作),库存不足则直接返回。库存充足则进行下一步->
将请求封装成对象,发送给rabbitmq(进行异步操作,快速处理前期的大量请求。流量削峰)->返回结果为排队中。下一步->
客户端轮询,判断是否真正秒杀成功->存入数据库->redis。

预加载库存

首先需要预加载库存
controller中实现初始化接口,并实现初始化方法

//实现InitializingBean 接口
public class SeKillController implements InitializingBean {
...........
...........

    /**
     * 系统初始化,把商品库存数量加载到Redis
     *
     * @param
     * @return void

     **/
    @Override
    public void afterPropertiesSet() throws Exception {
        List<GoodsVo> list = itGoodsService.findGoodsVo();
        if (CollectionUtils.isEmpty(list)) {
            return;
        }
        list.forEach(goodsVo -> {
            redisTemplate.opsForValue().set("seckillGoods:" + goodsVo.getId(), goodsVo.getStockCount());
            //内存标记商品,后续操作减少redis访问
            EmptyStockMap.put(goodsVo.getId(), false);
        });
    }
预减库存
ValueOperations valueOperations = redisTemplate.opsForValue();
 //判断是否重复抢购
        TSeckillOrder tSeckillOrder = (TSeckillOrder) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsId);
        if (tSeckillOrder != null) {
            return RespBean.error(RespBeanEnum.REPEATE_ERROR);
        }
        //预减库存,返回递减之后的库存
              Long stock = valueOperations.decrement("seckillGoods:" + goodsId);
                if (stock < 0) {
                //标记商品减轻redis压力操作
            EmptyStockMap.put(goodsId, true);
            //将库存置为0(上次递减为-1)
            valueOperations.increment("seckillGoods:" + goodsId);
            return RespBean.error(RespBeanEnum.EMPTY_STOCK);
        }

在判断是否重复抢购时,如果相同用户的请求在排队中还未写入 redis,代码判断能通过,但因为数据库中加入了用户+商品的唯一索引,所以不会出错。
在减少库存时,不直接获取值判断大于0 是因为 它不是原子性的.如果另外的请求刚好拿到还没减1,这时候你拿到了还是1,但是另外的预减了,会造成脏读。

下单

…(在后续的mq中进行)

 生成order
  return   order;
优化:加入mq的秒杀步骤

在这里插入图片描述

封装秒杀信息:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class SeckillMessage {

    private TUser tUser;

    private Long goodsId;
}

mq配置
定义配置类:

@Configuration
public class RabbitMQTopicConfig {

    private static final String QUEUE = "seckillQueue";
    private static final String EXCHANGE = "seckillExchange";

    @Bean
    public Queue queue() {
        return new Queue(QUEUE);
    }

    @Bean
    public TopicExchange topicExchange() {
        return new TopicExchange(EXCHANGE);
    }

    @Bean
    public Binding binding() {
        return BindingBuilder.bind(queue()).to(topicExchange()).with("seckill.#");
    }
}

定义发送者接收者:
发送者将订单发送至消息队列

@Service
@Slf4j
public class MQSender {

    @Autowired
    private RabbitTemplate rabbitTemplate;
    /**
     * 发送秒杀信息
     * @operation add
     * @param message
     * @return void
     **/
    public void sendSeckillMessage(String message) {
        log.info("发送消息" + message);
        rabbitTemplate.convertAndSend("seckillExchange", "seckill.message", message);

    }

消费者取出订单信息,接下来进行库存判断,重复抢购判断,并下单。

@Service
@Slf4j
public class MQReceiver {
//商品service
    @Autowired
    private ITGoodsService itGoodsServicel;
    @Autowired
    private RedisTemplate redisTemplate;
    //订单service
    @Autowired
    private ITOrderService itOrderService;
    /**
     * 下单操作
     *
     * @param
     * @return void
     * @operation add
     **/
    @RabbitListener(queues = "seckillQueue")
    public void receive(String message) {
        log.info("接收消息:" + message);
        SeckillMessage seckillMessage = JsonUtil.jsonStr2Object(message, SeckillMessage.class);
        Long goodsId = seckillMessage.getGoodsId();
        TUser user = seckillMessage.getTUser();
        //判断库存
        GoodsVo goodsVo = itGoodsServicel.findGoodsVobyGoodsId(goodsId);
        if (goodsVo.getStockCount() < 1) {
            return;
        }
        //判断是否重复抢购
        TSeckillOrder tSeckillOrder = (TSeckillOrder) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsId);
        if (tSeckillOrder != null) {
            return;
        }
        //下单操作
        itOrderService.secKill(user, goodsVo);

    }
    }

给mq发送消息,返回信息:

SeckillMessage seckillMessag = new SeckillMessage(user, goodsId);
        mqSender.sendSeckillMessage(JsonUtil.object2JsonStr(seckillMessag));
        return RespBean.success(0);

附json工具类

package com.example.seckill.util;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;
import java.util.List;

/**
 * Json工具类
 *
 */
public class JsonUtil {
    private static ObjectMapper objectMapper = new ObjectMapper();

    /**
     * 将对象转换成json字符串
     *
     * @param obj
     * @return
     */
    public static String object2JsonStr(Object obj) {
        try {
            return objectMapper.writeValueAsString(obj);
        } catch (JsonProcessingException e) {
            //打印异常信息
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 将字符串转换为对象
     *
     * @param <T> 泛型
     */
    public static <T> T jsonStr2Object(String jsonStr, Class<T> clazz) {
        try {
            return objectMapper.readValue(jsonStr.getBytes("UTF-8"), clazz);
        } catch (JsonParseException e) {
            e.printStackTrace();
        } catch (JsonMappingException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 将json数据转换成pojo对象list
     * <p>Title: jsonToList</p>
     * <p>Description: </p>
     *
     * @param jsonStr
     * @param beanType
     * @return
     */
    public static <T> List<T> jsonToList(String jsonStr, Class<T> beanType) {
        JavaType javaType = objectMapper.getTypeFactory().constructParametricType(List.class, beanType);
        try {
            List<T> list = objectMapper.readValue(jsonStr, javaType);
            return list;
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }
}

redis优化
因为预减库存时,如果库存预减完了,再次查询数据库会返回负数,预减失败,再次的大量操作会增加redis消耗。
以通过内存进行标记,减少redis压力。

在contoller中定义一个标记map
  private Map<Long, Boolean> EmptyStockMap = new HashMap<>();

在系统初始化加载库存时,对商品进行标记:

list.forEach(goodsVo -> {
            redisTemplate.opsForValue().set("seckillGoods:" + goodsVo.getId(), goodsVo.getStockCount());
            //内存标记
            EmptyStockMap.put(goodsVo.getId(), false);
        });

库存为空时标记为true

预减库存前对内存标记进行判断:
 //内存标记,减少Redis的访问
        if (EmptyStockMap.get(goodsId)) {
            return RespBean.error(RespBeanEnum.EMPTY_STOCK);
        }
                //预减库存
       Long stock = valueOperations.decrement("seckillGoods:" + goodsId);
        if (stock < 0) {
            EmptyStockMap.put(goodsId, true);
            valueOperations.increment("seckillGoods:" + goodsId);
            return RespBean.error(RespBeanEnum.EMPTY_STOCK);
        }

因为前面用mq发送消息后返回0,所以还需要查询是否下单成功

客户端轮询秒杀结果

在这里插入图片描述

客户端查询结果:

    function getResult(goodsId) {
        g_showLoading();
        $.ajax({
            url: "/seckill/getResult",
            type: "GET",
            data: {
                goodsId: goodsId
            },
            success: function (data) {
                if (data.code == 200) {
                    var result = data.object;
                    if (result < 0) {
                        layer.msg("对不起,秒杀失败");
                    } else if (result == 0) {
                        setTimeout(function () {
                            getResult(goodsId)
                        });
                    } else {
                        layer.confirm("恭喜您,秒杀成功!查看订单?", {btn: ["确定", "取消"]},
                            function () {
                                window.location.href = "/orderDetail.html?orderId=" + result;
                            },
                            function () {
                                layer.close();
                            }
                        )
                    }
                }
            },
            error: function () {
                layer.msg("客户端请求错误");
            }
        });
    }

controller增加获取结果方法

    /**
     * 获取秒杀结果
     *
     * @param tUser
     * @param goodsId
     * @return orderId 成功 ;-1 秒杀失败 ;0 排队中
     * @operation add
     **/
    @ApiOperation("获取秒杀结果")
    @GetMapping("getResult")
    @ResponseBody
    public RespBean getResult(TUser tUser, Long goodsId) {
        if (tUser == null) {
            return RespBean.error(RespBeanEnum.SESSION_ERROR);
        }
        Long orderId = itSeckillOrderService.getResult(tUser, goodsId);
        return RespBean.success(orderId);
    }


//service层的方法实现:
    @Override
    public Long getResult(TUser tUser, Long goodsId) {

        TSeckillOrder tSeckillOrder = tSeckillOrderMapper.selectOne(new QueryWrapper<TSeckillOrder>().eq("user_id", tUser.getId()).eq("goods_id", goodsId));
        if (null != tSeckillOrder) {
            return tSeckillOrder.getOrderId();
        } else if (redisTemplate.hasKey("isStockEmpty:" + goodsId)) {
            return -1L;
        } else {
            return 0L;
        }

    }

上面的service层中,对redis商品为空的判断需要在下单逻辑中进行设置:

 @Transactional
    @Override
    public TOrder secKill(TUser user, GoodsVo goodsVo) {
        ValueOperations valueOperations = redisTemplate.opsForValue();
                //减少库存
        TSeckillGoods seckillGoods = itSeckillGoodsService.getOne(new QueryWrapper<TSeckillGoods>().eq("goods_id", goodsVo.getId()));
        seckillGoods.setStockCount(seckillGoods.getStockCount() - 1);
                boolean seckillGoodsResult = itSeckillGoodsService.update(new UpdateWrapper<TSeckillGoods>()
                .setSql("stock_count = " + "stock_count-1")
                .eq("goods_id", goodsVo.getId())
                .gt("stock_count", 0)
        );
        if (seckillGoods.getStockCount() < 1) {
            //判断是否还有库存
            valueOperations.set("isStockEmpty:" + goodsVo.getId(), "0");
            return null;
        }
        .............

redis分布式锁

redisson是实现分布式锁较好的redis客户端,但这里用redisTemplate测试下分布式锁:
f分布式锁的思想就是占位,当别的进程也要占位时发现已经被占位就会放弃或者稍后再试。(setnx)

@Test
    public void testLock01() {
        ValueOperations valueOperations = redisTemplate.opsForValue();
        //占位,如果key不存在才可以设置成功
        Boolean isLock = valueOperations.setIfAbsent("k1", "v1");
        if (isLock) {
            valueOperations.set("name", "xxx");
            String name = (String) valueOperations.get("name");
            System.out.println("name=" + name);
            //操作结束,删除锁
            redisTemplate.delete("k1");
        } else {
            System.out.println("有线程在使用,请稍后再试");
        }
    }

但这样如果前面的线程没有释放锁(发生异常等情况)就会使后面的线程无法获得锁。
简单方法:上锁时配置超时时间,超时会自动释放锁。
-> 新的问题:可能发生现有线程会将其他线程的锁误删(过期时间设置较短,但线程执行时间长,后面的线程的锁会被前面的线程删掉),造成一连串的问题。
->上锁时每个线程对自己的锁加上特殊标记(例如:每个线程key对应的value是一个独有的随机值),只有在判断是自己的锁的情况下才能删锁。 (自己的锁自己删)
->但这个过程是不连续的,有加锁,获取锁,比较锁,删除锁的过程
->使用乐观锁或者lua脚本
->lua脚本可以原子性的执行多个redis命令
例:

if redis.call("get",KEYS[1])==ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end
//配置类中配置lua脚本的调用
 @Bean
    public DefaultRedisScript<Long> script() {
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        //lock.lua脚本位置和application.yml同级目录
        redisScript.setLocation(new ClassPathResource("stock.lua"));
        redisScript.setResultType(Long.class);
        return redisScript;
    }

test

    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private RedisScript<Boolean> redisScript;
  @Test
    public void testLock3() {
        ValueOperations valueOperations = redisTemplate.opsForValue();
        String value = UUID.randomUUID().toString();
        Boolean isLock = valueOperations.setIfAbsent("k1", value, 5, TimeUnit.SECONDS);
        if (isLock) {
            valueOperations.set("name", "xxx");
            String name = (String) valueOperations.get("name");
            System.out.println("name=" + name);
            //操作结束,删除锁
            System.out.println(valueOperations.get("k1"));
            Boolean result = (Boolean) redisTemplate.execute(redisScript, Collections.singletonList("k1"), value);
            System.out.println(result);
        } else {
            System.out.println("有线程在使用,请稍后再试");
        }
    }

这样将获取锁,比较锁,删除锁三个过程原子性的执行。

分布式锁优化预减库存

保障线程安全
lua:

if(redis.call('exists',KEYS[1])==1) then
    local stock =tonumber(redis.call('get',KEYS[1]));
    if(stock>0) then
        redis.call('incrby',KEYS[1],-1);
        return stock;
    end;
        return -1;
end;

controller中修改:

        //内存标记,减少Redis的访问
        if (EmptyStockMap.get(goodsId)) {
            return RespBean.error(RespBeanEnum.EMPTY_STOCK);
        }
        //预减库存,所用lua脚本执行
//        Long stock = valueOperations.decrement("seckillGoods:" + goodsId);
        Long stock = (Long) redisTemplate.execute(redisScript, Collections.singletonList("seckillGoods:" + goodsId), Collections.EMPTY_LIST);
        if (stock < 0) {
            EmptyStockMap.put(goodsId, true);
           // valueOperations.increment("seckillGoods:" + goodsId);
            return RespBean.error(RespBeanEnum.EMPTY_STOCK);
        }
        SeckillMessage seckillMessag = new SeckillMessage(user, goodsId);
        mqSender.sendSeckillMessage(JsonUtil.object2JsonStr(seckillMessag));
        return RespBean.success(0);

在订单处理模块,写入数据库时是不安全的,可以加入分布式锁(redissonClient).

安全优化

秒杀接口地址隐藏

在这里插入图片描述

前端获取秒杀路径

    function getSeckillPath() {
        var goodsId = $("#goodsId").val();
       //验证码
        var captcha = $("#captcha").val();
        g_showLoading();
        $.ajax({
            url: "/seckill/path",
            type: "GET",
            data: {
                goodsId: goodsId,
                captcha:captcha
            },
            success: function (data) {
                if (data.code == 200) {
                    var path = data.object;
                    doSecKill(path);
                } else {
                    layer.msg(data.message);
                }
            },
            error: function () {
                layer.msg("客户端请求错误");
            }
        });
    }

    //秒杀方法
    function doSecKill(path) {
        $.ajax({
            url: 'seckill/' + path + '/doSeckill',
            type: "POST",
            data: {
                goodsId: $('#goodsId').val()
            },
            success: function (data) {
                if (data.code == 200) {
                    // window.location.href="/orderDetail.html?orderId="+data.object.id;
                    getResult($("#goodsId").val());
                } else {
                    layer.msg(data.message);
                }
            }, error: function () {
                layer.msg("客户端请求出错");
            }

        });
    }

控制层(seckillController)

    @ApiOperation("获取秒杀地址")
    @AccessLimit(second = 5, maxCount = 5, needLogin = true)
    @GetMapping(value = "/path")
    @ResponseBody
    public RespBean getPath(TUser tuser, Long goodsId, String captcha, HttpServletRequest request) {
        if (tuser == null) {
            return RespBean.error(RespBeanEnum.SESSION_ERROR);
        }
        //校验验证码
             boolean check = orderService.checkCaptcha(tuser, goodsId, captcha);
        if (!check) {
            return RespBean.error(RespBeanEnum.ERROR_CAPTCHA);
        }
        String str = orderService.createPath(tuser, goodsId);
        return RespBean.success(str);
        }
------------------------------------------------------------------------------------
//秒杀方法中加入随机path
 @ApiOperation("秒杀功能")
    @RequestMapping(value = "/{path}/doSeckill", method = RequestMethod.POST)
    @ResponseBody
    public RespBean doSecKill(@PathVariable String path, TUser user,  Long goodsId) {
.............
  boolean check = orderService.checkPath(user, goodsId, path);
        if (!check) {
        //请求非法
            return RespBean.error(RespBeanEnum.REQUEST_ILLEGAL);
        }
.............
}

Service层

//获取秒杀路径
//uuid生成唯一的秒杀地址
    @Override
    public String createPath(TUser user, Long goodsId) {
        String str = MD5Util.md5(UUIDUtil.uuid() + "123456");
        redisTemplate.opsForValue().set("seckillPath:" + user.getId() + ":" + goodsId, str, 1, TimeUnit.MINUTES);
        return str;
    }
-----------------------------------------------
//校验秒杀地址
    @Override
    public boolean checkPath(TUser user, Long goodsId, String path) {
        if (user == null || goodsId < 0 || StringUtils.isEmpty(path)) {
            return false;
        }
        String redisPath = (String) redisTemplate.opsForValue().get("seckillPath:" + user.getId() + ":" + goodsId);
        return path.equals(redisPath);
    }

验证码

作用:防止脚本;拉长短时间内的并发时间长度。
(前端省略)
可以在码云等找开源的工具:
导入依赖:

   <!--验证码依赖-->
        <dependency>
            <groupId>com.github.whvcse</groupId>
            <artifactId>easy-captcha</artifactId>
            <version>1.6.2</version>
        </dependency>

获取验证码

    @ApiOperation("获取验证码")
    @GetMapping(value = "/captcha")
    public void verifyCode(TUser tUser, Long goodsId, HttpServletResponse response) {

   
        if (tUser == null || goodsId < 0) {
            throw new GlobalException(RespBeanEnum.REQUEST_ILLEGAL);
        }
        //设置请求头为输出图片的类型
        response.setContentType("image/jpg");
        response.setHeader("Pargam", "No-cache");
        // 不需要缓存,确保每次获取的验证码不重复
        response.setHeader("Cache-Control", "no-cache");
        response.setDateHeader("Expires", 0);
        //生成验证码
        ArithmeticCaptcha captcha = new ArithmeticCaptcha(130, 32, 3);
        redisTemplate.opsForValue().set("captcha:" + tUser.getId() + ":" + goodsId, captcha.text(), 300, TimeUnit.SECONDS);
        try {
            captcha.out(response.getOutputStream());
        } catch (IOException e) {
            log.error("验证码生成失败", e.getMessage());
        }
    }

校验验证码
传递一个验证码进去进行校验

// getPath方法
 boolean check = orderService.checkCaptcha(tuser, goodsId, captcha);
        if (!check) {
            return RespBean.error(RespBeanEnum.ERROR_CAPTCHA);
        }

//service层
 @Override
    public boolean checkCaptcha(TUser user, Long goodsId, String captcha) {
        if (user == null || goodsId < 0 || StringUtils.isEmpty(captcha)) {
            return false;
        }
        String redisCaptcha = (String) redisTemplate.opsForValue().get("captcha:" + user.getId() + ":" + goodsId);
        return captcha.equals(redisCaptcha);
    }

接口限流

常见的限流算法:计数器,漏斗,令牌桶
限流规则为设置为最大可承受qps的70%-80%
令牌桶(队列机制)容易造成请求失效
计数器方法:

    @ApiOperation("获取秒杀地址")
    @AccessLimit(second = 5, maxCount = 5, needLogin = true)
    @GetMapping(value = "/path")
    @ResponseBody
    public RespBean getPath(TUser tuser, Long goodsId, String captcha, HttpServletRequest request) {
        if (tuser == null) {
            return RespBean.error(RespBeanEnum.SESSION_ERROR);
        }
        ValueOperations valueOperations = redisTemplate.opsForValue();
        //限制访问次数,5秒内访问5次
        String uri = request.getRequestURI();
        //默认一个验证码
        captcha = "0";
        Integer count = (Integer) valueOperations.get(uri + ":" + tuser.getId());
        if (count == null) {
            valueOperations.set(uri + ":" + tuser.getId(), 1, 5, TimeUnit.SECONDS);
        } else if (count < 5) {
            valueOperations.increment(uri + ":" + tuser.getId());
        } else {
            return RespBean.error(RespBeanEnum.ACCESS_LIMIT_REACHED);
        }


        boolean check = orderService.checkCaptcha(tuser, goodsId, captcha);
        if (!check) {
            return RespBean.error(RespBeanEnum.ERROR_CAPTCHA);
        }
        String str = orderService.createPath(tuser, goodsId);
        return RespBean.success(str);
    }

优化针对不同的接口有不同的请求限流规则,所以需要通用的接口限流。
自定义注解实现(aop)

/**
 * 自定义注解
 * @ClassName: AccessLimit
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AccessLimit {
//每秒最大访问量
    int second();

    int maxCount();
//是否需要登录
    boolean needLogin() default true;
}

拦截器


/**
 * @ClassName: AccessLimitInterceptor
 */
@Component
public class AccessLimitInterceptor implements HandlerInterceptor {

    @Autowired
    private ITUserService itUserService;
    @Autowired
    private RedisTemplate redisTemplate;
//执行之前
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof HandlerMethod) {
            TUser tUser = getUser(request, response);
            //将用户放入Threadlocal
            UserContext.setUser(tUser);
            HandlerMethod hm = (HandlerMethod) handler;
            //获取方法上的注解
            AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);
            if (accessLimit == null) {
                return true;
            }
            int second = accessLimit.second();
            int maxCount = accessLimit.maxCount();
            boolean needLogin = accessLimit.needLogin();

            String key = request.getRequestURI();
            if (needLogin) {
                if (tUser == null) {
                    render(response, RespBeanEnum.SESSION_ERROR);
                }
                key += ":" + tUser.getId();
            }
            ValueOperations valueOperations = redisTemplate.opsForValue();
            Integer count = (Integer) valueOperations.get(key);
            if (count == null) {
                valueOperations.set(key, 1, second, TimeUnit.SECONDS);
            } else if (count < maxCount) {
                valueOperations.increment(key);
            } else {
                render(response, RespBeanEnum.ACCESS_LIMIT_REACHED);
                return false;
            }
        }
        return true;
    }
//构建返回对象
    private void render(HttpServletResponse response, RespBeanEnum respBeanEnum) throws IOException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        PrintWriter printWriter = response.getWriter();
        RespBean bean = RespBean.error(respBeanEnum);
        printWriter.write(new ObjectMapper().writeValueAsString(bean));
        printWriter.flush();
        printWriter.close();
    }

    private TUser getUser(HttpServletRequest request, HttpServletResponse response) {
        String userTicket = CookieUtil.getCookieValue(request, "userTicket");
        if (StringUtils.isEmpty(userTicket)) {
            return null;
        }
        return itUserService.getUserByCookie(userTicket, request, response);
    }
}

拦截器中使用TreadLocal存储用户信息,在之后的需求中就可以直接用UserContext调用,防止线程冲突(线程私有数据)
mvconfig中添加拦截器:

    @Autowired
    private AccessLimitInterceptor accessLimitInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(accessLimitInterceptor);
    }

mm秒杀controller上添加注解:

@AccessLimit(second = 5, maxCount = 5, needLogin = true)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值