高并发秒杀Kafka+Lua+redis实现(二)

缓存预热,秒杀商品设置到Redis中,同时提供静态页面给用户使用

@RestController
@RequestMapping("/seckill")
@Slf4j
public class SeckillController {

    @Resource
    private RedisTemplate redisTemplate;

    @Autowired
    private ProductService productService;

    @Autowired
    private OrderService orderService;

    @Resource
    private KafkaTemplate<String, String> kafkaTemplate;

    @Autowired
    private RedisClient redisClient;

    @Autowired
    private DefaultRedisScript<Long> defaultRedisScript;

    /**
     * 秒杀商品设置到Redis中,同时提供静态页面给用户使用
     * @author fan
     * @date 2022/5/9 17:56
     * @return java.util.List<com.fan.li.entity.Product>
     */
    @RequestMapping(value = "/queryAll")
    @ResponseBody
    //@MyAcessLimter(count = 1000,timeout = 1)
    public List<Product> queryAll(@RequestParam("seckillDate") String seckillDate) {
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        try {
            ObjectMapper objectMapper = new ObjectMapper();
            List<Product> productList = null;
            String key = "userList_plan_all";
            boolean hasKey = redisTemplate.hasKey(key);//判断redis中是否有键为key的缓存
            if (hasKey) {
                productList = redisClient.getStringList(key, 0, -1);
                log.info("redis的数据list.size()-->" + productList.size());
            } else {
                productList = productService.queryAll();
                redisClient.setStringList(key, productList);//把商品信息存入缓存,列表展示用
                //list = userService.findPage(cp,ps);
                redisTemplate.expire(key, 1_000 * 60 * 60, TimeUnit.MILLISECONDS);//设置过期时间1个小时
                log.info("mysql的数据list.size()-->" + productList.size());
            }
            if (productList == null) {
                return null;
            }
            for (Product product : productList) {
                Long productId = product.getId();
                redisTemplate.opsForValue().set("product_" + productId, objectMapper.writeValueAsString(product));
                // 一个用户只买一件商品
                // 商品购买用户Set
                redisTemplate.opsForSet().add("product_buyers_" + product.getId(), "");
                for (int i = 0; i < product.getStock(); i++) {
                    redisTemplate.opsForList().leftPush("product_stock_key_" + product.getId(), String.valueOf(i));
                }
                log.info("[queryAll()] 商品product:" + objectMapper.writeValueAsString(product));
            }
            redisTemplate.opsForValue().set("seckill_plan_" + seckillDate, objectMapper.writeValueAsString(productList));//把商品信息存入缓存,列表展示用
            return productList;
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 开始秒杀商品,并削峰限流
     * @author fan
     * @date 2022/5/10 1:03
     * @param userId
     * @param productId
     * @return java.lang.String
    */
    @RequestMapping(value = "/seckillProduct")
    //@PostMapping(value = "seckillProduct")
    @ResponseBody
    @MyAcessLimter(count = 100,timeout = 1)
    public synchronized String seckillProduct( String userId,  String productId) throws JsonProcessingException {
        List<String> list = Lists.newArrayList("product_stock_key_" + productId , "product_buyers_" + productId , userId );
        Long code = (Long) redisTemplate.execute(defaultRedisScript, list, "");
        if (code == -1) {
            return "库存不足";
        } else if (code == 2) {
            return "不允许重复秒杀";
        } else if (code == 1) {//整个秒杀过程在缓存中进行,秒杀结束后从缓存中拿数据库加入队列同步到数据库中
            ObjectMapper objectMapper = new ObjectMapper();
            String productJson = (String) redisTemplate.opsForValue().get("product_" + productId);
            Gson gson = new Gson();
            Product product = objectMapper.readValue(productJson , Product.class);
                    //gson.fromJson(productJson , Product.class);
            Order order = new Order();
            order.setProductId(Long.parseLong(productId));
            order.setUserId(Long.parseLong(userId));
            String id = String.valueOf(UUID.randomUUID());
            order.setId(id);
            order.setOrderName("抢购" + product.getName());
            order.setProductName(product.getName());
            MessageObject messageObject = new MessageObject(order, product);//MessageObject中status为1表示定时任务要处理的数据
            //redisClient.setString("messageObject_" + productId, gson.toJson(messageObject));
            // 把要发送的数据messageObject放到同一个表中,这里演示就放到Redis
            String message = objectMapper.writeValueAsString(messageObject);
            redisClient.putHash("messageObject_" , productId + "_" + userId , message);
            kafkaTemplate.send("seckill_order", JSONUtil.objToString(order));
            return "sueccss";
        }
        return "error";
    }


}

消费者

@Component
@Slf4j
public class OrderSeckillConsumer {

    @Autowired
    private RedisClient redisClient;

    @Autowired
    private OrderService orderService;
    @Autowired
    private ProductService productService;

    private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");


    @KafkaListener(topics = "seckill_order")
    public void run(ConsumerRecord<?, ?> record){
        Optional<?> kafkaMessage = Optional.ofNullable(record.value());
        try {
            if (kafkaMessage.isPresent()) {
                String message = (String) kafkaMessage.get();
                log.info("[run()-->]message: {}", message);
                ObjectMapper objectMapper = new ObjectMapper();
                Order order = objectMapper.readValue(message, Order.class);
                if (order != null) { //先调通知成功的接口给用户后对订单数据入库
                    List<Order> orderList = orderService.selectOne(order.getId());
                    if (!CollectionUtils.isEmpty(orderList)){ // 防止消息重复消费,保证幂等性(存个key到Redis指定过期时间也可以,但要保证是同一条消息)
                        log.info("[该订单已存在]");
                        return;
                    }
                    log.info("当前时间:" + sdf.format(new Date()));
                    Long productId = order.getProductId();
                    int p = productService.updateProduct(productId);//开启事务
                    if (p > 0) {
                        int i = orderService.saveOrder(order);//开启事务
                        if (i > 0){
                            //日志写入略
                            String productJson = (String) redisClient.getString("product_" + productId);//product_
                            Product product = objectMapper.readValue(productJson, Product.class);
                            //Product product = productService.selectById(productId);
                            MessageObject messageObject = new MessageObject(order, product);//MessageObject中status为1表示定时任务要处理的数据
                            messageObject.setStatus("0");
                            String msg = objectMapper.writeValueAsString(messageObject);
                            redisClient.putHash("messageObject_" , productId + "_" + order.getUserId() , msg);//把状态更新回缓存(表)
                        }
                    }
                }
            }
        } catch (Exception e){
            e.printStackTrace();
            log.error(e.getMessage());
        }
    }

}

定时任务

@Component
@Slf4j
public class MyKafkaTask {

    private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    @Resource
    private KafkaTemplate<String, String> kafkaTemplate;

    @Autowired
    private ProductService productService;

    @Autowired
    private OrderService orderService;

    @Autowired
    private RedisClient redisClient;

    @Autowired
    KafkaProducers kafkaProducers;


    //@Scheduled(cron = "0/30 * * * * ?")
    @Scheduled(initialDelay=2000, fixedRate=60000)
    public void excuteTask() throws JsonProcessingException {
        List<String> keySet = redisClient.getHash("messageObject_");
        if (keySet != null && keySet.size() > 0) {
            ObjectMapper objectMapper = new ObjectMapper();
            for (String jonsonObjStr : keySet) {
                MessageObject messageObject = objectMapper.readValue(jonsonObjStr , MessageObject.class);
                String status = messageObject.getStatus();
                if (!"1".equals(status) && !StringUtils.isEmpty(status)){
                    continue;
                }
                String id = messageObject.getId();
                Long userId = messageObject.getUserId();
                Long productId = messageObject.getProductId();
                String productName = messageObject.getProductName();
                String orderName = messageObject.getOrderName();
                Order order = new Order();
                order.setProductId(productId);
                order.setUserId(userId);
                order.setId(id);
                order.setOrderName(orderName);
                order.setProductName(productName);
                List<Order> orderList = orderService.selectOne(id);
                if (!CollectionUtils.isEmpty(orderList)){ // 防止消息重复消费,保证幂等性(存个key到Redis指定过期时间也可以,但要保证是同一条消息)
                    log.info("[该订单已存在] date={}" , sdf.format(new Date()));
                    continue;
                }
                Integer i = orderService.saveOrder(order);
                Product product =  new Product();
                String name = messageObject.getName();
                Integer stock = messageObject.getStock();
                Date creatTime = messageObject.getCreatTime();
                Date startTime = messageObject.getStartTime();
                Date endTime = messageObject.getEndTime();
                product.setId(productId);
                product.setStock(stock);
                product.setName(name);
                product.setStartTime(startTime);
                product.setEndTime(endTime);
                product.setCreatTime(creatTime);
                int p = productService.updateProduct(productId);
                if (p > 0 && i > 0){ //更新到Redis(表)
                    messageObject.setStatus("0");
                    String message = objectMapper.writeValueAsString(messageObject);
                    redisClient.putHash("messageObject_" , productId + "_" + userId , message);//把状态更新回缓存
                    kafkaTemplate.send("seckill_order", JSONUtil.objToString(order));//重新发送到队列
                }
            }
        }
    }


}

 限流开始

@Component
@Aspect
@Slf4j
public class MyAcessLimiterAspect {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Resource(name = "ipLimitLua")
    private DefaultRedisScript<Boolean> ipLimitLua;

    @Resource
    StringRedisTemplate stringRedisTemplate;

    // 1: 切入点   创建的注解类
    @Pointcut("@annotation(com.fan.li.myspringboot.limit.MyAcessLimter)")
    public void myLimiterPonicut() {
    }

    @Before("myLimiterPonicut()")
    public void limiter(JoinPoint joinPoint) {
        log.info("限流进来了......." + LocalDate.now());
        // 1:获取方法的签名作为key
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        String classname = methodSignature.getMethod().getDeclaringClass().getName();
        String packageName = methodSignature.getMethod().getDeclaringClass().getPackage().getName();
        log.info("method:{},classname:{},packageName:{}",method,classname,packageName);
        // 4: 读取方法的注解信息获取限流参数
        MyAcessLimter annotation = method.getAnnotation(MyAcessLimter.class);
        // 5:获取注解方法名
        String methodNameKey = method.getName();
        log.info("获取注解方法名:{}" , methodNameKey);
        // 6:获取服务请求的对象
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = requestAttributes.getRequest();
        HttpServletResponse response = requestAttributes.getResponse();
        String userIp = MyIPUtils.getIpAddr(request);
        log.info("用户IP是:.......{}" , userIp);
        // 7:通过方法反射获取注解的参数
        Integer count = annotation.count();
        Integer timeout = annotation.timeout();

        /*Object[] args = joinPoint.getArgs();
        for (int i = 0; i < args.length; i++) {
            log.info("参数id--> " + request.getParameter("userId") + "---" + args[i]);
        }*/
        String redisKey =  userIp;
        log.info("当前的key-->" + redisKey);
        // 8: 请求lua脚本
        Boolean acquired =  stringRedisTemplate.execute(ipLimitLua, Lists.newArrayList(redisKey) , count.toString() , timeout.toString());
        // 如果超过限流限制
        if (!acquired) {
            // 抛出异常,然后让全局异常去处理
            response.setCharacterEncoding("UTF-8");
            response.setContentType("text/html;charset=UTF-8");
            try (PrintWriter writer = response.getWriter()) {
                writer.print("<h1>操作频繁,请稍后在试</h1>");
            } catch (Exception ex) {
                throw new RuntimeException("操作频繁,请稍后在试");
            }
        }
    }
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface MyAcessLimter {

    /**
     * 限流唯一标示
     * @author fan
     * @date 2022/5/7 1:55
     * @return java.lang.String
    */
    String key() default "";

    /**
     * 每timeout限制请求的个数
     * @author fan
     * @date 2022/5/7 1:54
     * @return int
    */
    int count() default 5;

    /**
     * 超时时间,单位默认是秒
     * @author fan
     * @date 2022/5/7 1:54
     * @return int
    */
    int timeout() default 10;

    /**
     * 访问间隔
     * @author fan
     * @date 2022/5/7 1:54
     * @return int
    */
    int waits() default 20;
}

配置类

@Configuration
public class MyLuaConfiguration {

    /**
     * 将IP-lua脚本的内容加载出来放入到DefaultRedisScript
     * @author fan
     * @date 2022/5/7 9:35
     * @return org.springframework.data.redis.core.script.DefaultRedisScript<java.lang.Boolean>
    */
    @Bean(name = "ipLimitLua")
    public DefaultRedisScript<Boolean> ipLimitLua() {
        DefaultRedisScript<Boolean> defaultRedisScript = new DefaultRedisScript<>();
        defaultRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("myLimit_ip.lua")));
        defaultRedisScript.setResultType(Boolean.class);
        return defaultRedisScript;
    }

    /**
     * 将秒杀lua脚本的内容加载出来放入到DefaultRedisScript
     * @return
     */
    @Bean
    public DefaultRedisScript<Boolean> seckillLimiterLuaScript() {
        DefaultRedisScript<Boolean> defaultRedisScript = new DefaultRedisScript<>();
        defaultRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("seckill.lua")));
        defaultRedisScript.setResultType(Boolean.class);
        return defaultRedisScript;
    }

    @Bean
    public DefaultRedisScript<Long> seckillLimiterLuaScript2() {
        DefaultRedisScript<Long> defaultRedisScript = new DefaultRedisScript<Long>();
        defaultRedisScript.setResultType(Long.class);
        defaultRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("buyone.lua")));
        return defaultRedisScript;
    }

}

订单接口略

buyone.lue

--商品库存Key
local product_stock_key = KEYS[1]
--商品购买用户记录Key
local buyersKey = KEYS[2]
--用户ID
local uid = KEYS[3]

--校验用户是否已经购买
local result = redis.call("sadd" , buyersKey , uid )
if(tonumber(result) == 1)
then
    --没有购买过,可以购买
    local stock = redis.call("lpop" , product_stock_key )
    --除了nil和false,其他值都是真(包括0)
    if(stock)
    then
        --有库存
        return 1
    else
        --没有库存
        return -1
    end
else
    --已经购买过
    return 2
end

myLimit_ip.lua

-- 为某个接口的请求IP设置计数器,比如:127.0.0.1请求课程接口
-- KEYS[1] = 127.0.0.1 也就是用户的IP
-- ARGV[1] = 过期时间 30m
-- ARGV[2] = 限制的次数
local count = redis.call('incr',KEYS[1]);
if count == 1 then
    redis.call("expire",KEYS[1],ARGV[2])
end
-- 如果次数还没有过期,并且还在规定的次数内,说明还在请求同一接口
if count > tonumber(ARGV[1]) then
    return false
end

return true

seckill.lua

--商品库存Key product_one_stock_XXX  仅售一件
local product_id = KEYS[1]
--用户ID
local user_id = ARGV[1]
-- 商品库存key
local product_stock_key = 'seckill:{' .. product_id .. '}:stock'
-- 商品秒杀结束标识的key
local end_product_key = 'seckill:{' .. product_id .. '}:end'

-- 存储秒杀成功的用户id的集合的key
local bought_users_key = 'seckill:{' .. product_id .. '}:uids'

--判断该商品是否秒杀结束
local is_end = redis.call('get',product_stock_key)

if  is_end and tonumber(is_end) ~=1 then
    return -2
end
-- 判断用户是否秒杀过
local is_in = redis.call('sismember',bought_users_key,user_id)

if is_in > 0 then
    return 0
end

-- 获取商品当前库存
local stock = redis.call('get',product_stock_key)

-- 如果库存<=0,则返回-1
if not stock or tonumber(stock) <=0
then
    redis.call("set",end_product_key,"1")
    return -1
end

-- 减库存,并且把用户的id添加进已购买用户set里
redis.call("decr",product_stock_key)
redis.call("sadd",bought_users_key,user_id)

return 1

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值