栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 前沿技术 > 大数据 > 大数据系统

SpringBoot+redis+rabbitmq实现高并发商品秒杀

SpringBoot+redis+rabbitmq实现高并发商品秒杀

SpringBoot+redis+rabbitmq实现高并发商品秒杀
  • 思路分析
  • 表设计
  • 部分代码分析
    • controller层
      • 几个注解说明
    • service层
    • 配置类
    • 工具类
    • 启动类
  • 测试
    • jMeter测试
  • 源码

最近学习了rabbitmq相关知识,想着用redis+rabbitmq模拟做个高并发情况下的秒杀接口。结合自己的一些想法和参考了网上的一些经验,做了下面这个接口,与大家分享。由于本人没有做过商品秒杀之类的项目,所以本文只是自己的见解,文中的可能有许多错误的地方,欢迎大家批评指正。
文末有源代码。

思路分析

秒杀是个高并发的过程,短时间内后端访问量巨大,可能会压垮系统,而且只有少许人能秒杀成功,因此首先要做的应该是限流,既只让部分用户进入后台业务逻辑,处理方式为限定每秒的访问量不能超过指定的访问量,实现技术用的是拦截器做请求拦截,redis做访问量统计。其次,对于秒杀成功的用户来说,下一步应该要做的是削峰和异步处理。对用户而言,只关心是否秒杀成功,不需要关心数据库的读写,因此数据库的读写可以做异步处理,此外异步的数据读写也可以分批进行,做到削峰,减小数据库压力。具体实现技术则是利用消息队列rabbitmq。最后应该限定用户每件商品只能购买指定的数量,这个功能可以利用redis做,在用户购买之前先判断是否具有购买权限,这部分可用拦截器做,此处用的是AOP来处理。

表设计

秒杀系统至少应该包含两张表,一个是秒杀商品表,一个是订单表,这里只写了一些简单的字段作为测试用。

CREATE TABLE `stock` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `stock` int DEFAULT NULL,
  `remarks` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
  `update_date` datetime DEFAULT NULL COMMENT '最后更新时间',
  `create_date` datetime DEFAULT NULL COMMENT '创建时间',
  `update_by` varchar(64) NOT NULL DEFAULT '',
  `create_by` varchar(64) NOT NULL DEFAULT '',
  `del_flag` char(1) NOT NULL DEFAULT '0' COMMENT '0正常,1删除',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb3 ROW_FORMAT=DYNAMIC COMMENT='商品库存表';

CREATE TABLE `stock` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `stock` int DEFAULT NULL,
  `remarks` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
  `update_date` datetime DEFAULT NULL COMMENT '最后更新时间',
  `create_date` datetime DEFAULT NULL COMMENT '创建时间',
  `update_by` varchar(64) NOT NULL DEFAULT '',
  `create_by` varchar(64) NOT NULL DEFAULT '',
  `del_flag` char(1) NOT NULL DEFAULT '0' COMMENT '0正常,1删除',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb3 ROW_FORMAT=DYNAMIC COMMENT='商品库存表';
部分代码分析

项目是maven项目,项目结构如下

配置文件

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/test? characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=false
    username: root
    password: 1234
    # 使用Druid数据源
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
  redis:
    database: 1   # redis数据库索引(默认为0)
    host: 127.0.0.1
    port: 6379
  rabbitmq:  #mq配置
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest

server:
  port: 8888
logging:
  config: classpath:logback-spring.xml
  file:
    name: seckill.log
    path: /log


pom文件大部分依赖包均已说明其作用



    4.0.0

    com.orange
    seckill_demo
    1.0-SNAPSHOT

    
        8
        8
    
    
        org.springframework.boot
        spring-boot-starter-parent
        2.5.5
         
    

    

        
        
            org.springframework.boot
            spring-boot-starter-data-redis
        
        
        
            org.springframework.boot
            spring-boot-starter-amqp
        

        
        
            redis.clients
            jedis
        

        
        
            com.alibaba
            druid-spring-boot-starter
            1.2.8
        

        
        
            com.baomidou
            mybatis-plus-boot-starter
            3.4.2
        

        
        
            mysql
            mysql-connector-java
            runtime
        

        
        
            org.projectlombok
            lombok
            true
        

        
            org.springframework.boot
            spring-boot-starter-web
        
        
            org.springframework.boot
            spring-boot-starter-aop
        
        
            org.assertj
            assertj-core
        
        
            com.baomidou
            mybatis-plus-boot-starter
            3.4.2
        
        
        
            io.springfox
            springfox-swagger2
            3.0.0
        
        
            io.springfox
            springfox-swagger-ui
            3.0.0
        
        
        
            org.quartz-scheduler
            quartz
        
        
        
            com.alibaba
            fastjson
            1.2.76
        

    


controller层
package com.orange.controller;

import com.orange.annotation.AccessLimit;
import com.orange.annotation.LimitNumber;
import com.orange.service.impl.MqStockServiceImpl;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;


@RestController
@Slf4j
@Api(value = "SecKillController",  tags = "秒杀控制层")
public class SecKillController {

    @Autowired
    private MqStockServiceImpl mqStockService;


    
    @PostMapping(value = "sec-kill")
    @ApiOperation(value = "redis+消息队列进行秒杀实现", notes = "redis+消息队列进行秒杀实现")
    @LimitNumber(value = 2)
    @AccessLimit(seconds = 1,maxCount = 800)
    public String secKill(@RequestParam(value = "userName") String userName, @RequestParam(value = "stockName") String stockName) {
        return mqStockService.secKill(userName, stockName);
    }

}
几个注解说明

@LimitNumber(value = 2):自定义注解,用于限制用户购买数量,value代表购买数量
@AccessLimit(seconds = 1,maxCount = 800):自定义注解,用于限流,限定指定时间范围内的访问量。
在执行service之前,会先执行者两个注解,其实现方式是通过拦截器和AOP实现的,下面先说明者两个注解再说明service中的逻辑。
以AccessLimit为例,该注解的实现原理是拦截器,当有新请求到来时,拦截器先判断controller上是否有该注解,如有则执行指定的逻辑。

package com.orange.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.RetentionPolicy.RUNTIME;


@Retention(RUNTIME)
@Target(ElementType.METHOD)
public @interface AccessLimit {
    
    int seconds();

    
    int maxCount();
}

拦截器如下

package com.orange.interceptor;

import com.alibaba.fastjson.JSONObject;
import com.orange.annotation.AccessLimit;
import com.orange.entity.AjaxResult;
import com.orange.utils.RedisCache;
import com.orange.utils.ServletUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Objects;
import java.util.concurrent.TimeUnit;


@Slf4j
@Component
public class AccessLimitInterceptor implements HandlerInterceptor {
    @Autowired
    private RedisCache redisCache;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        if(handler instanceof HandlerMethod){
            //查看该方法上是否有@AcessLimit注解
            HandlerMethod hm= (HandlerMethod) handler;
            AccessLimit accessLimit=hm.getMethodAnnotation(AccessLimit.class);
            //没有@AcessLimit注解,证明无限流操作,直接放行
            if(Objects.isNull(accessLimit)){
                return true;
            }
            //获取注解的参数值
            int seconds=accessLimit.seconds();
            int maxCount=accessLimit.maxCount();
            //该请求的路径
            String key=request.getRequestURI();
            //在该时间范围内已经访问的次数
            Integer count= redisCache.getCacheObject(key);
            if(Objects.isNull(count)){
                redisCache.setCacheObject(key,0,seconds, TimeUnit.SECONDS);
                log.info("地址{},在{}秒内已第一次被访问次数",key,seconds);
            }else if(count 

拦截器拦截有该注解的请求,没有的则放行,有的则将该请求地址加入redis,以url作为key,并设置过期时间。每有一次求情则让该key的值增大一次。代码中redisCache、AjaxResult、ServletUtils为封装的工具类。
拦截器逻辑控制配置完成之后,应注册该拦截器,使之生效。

package com.orange.config;

import com.orange.interceptor.AccessLimitInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;



@Configuration
public class WebConfig  implements WebMvcConfigurer {


    @Autowired
    private AccessLimitInterceptor accessLimitInterceptor;


    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(accessLimitInterceptor).addPathPatterns("
@Slf4j
@Aspect
@AllArgsConstructor
@Component
public class LimitNumberAspect {
    @Autowired
    private RedisCache redisCache;
    @Around("@annotation(limitNumber)")
    public Object aopInterceptor(ProceedingJoinPoint pjp, LimitNumber limitNumber) throws Throwable {
        int value = limitNumber.value();

        Signature signature = pjp.getSignature();
        MethodSignature methodSignature = (MethodSignature)signature;

        //获取当前执行的方法
        Method targetMethod = methodSignature.getMethod();
        log.info("当前执行的方法:{}",targetMethod.getName());
        // 参数名数组
        String[] parameterNames = ((MethodSignature) signature).getParameterNames();

        //组装幂等性唯一key
        //获取参数
        Object[] objs = pjp.getArgs();
        String extApiKey = "";
        for (Object obj:objs){
            extApiKey= extApiKey.concat(obj.toString()+":");
        }
        extApiKey = extApiKey.concat("number");
        Long number = redisCache.incrBy(extApiKey);
        if(Objects.nonNull(number) && number>=value+1){
            //代理方法的返回值
            log.info("该用户已没有抢购机会");
            throw new CustomException("您已没有抢购机会");

        }
        return pjp.proceed();
    }
}

其中有一点需要注意的是该处直接让这个缓存的值自增后再判断该值与定义限购次数的值+1的大小,这样做的原因是为了保证数据的一致性,因为如果同一个用户在同一时刻在不同的设备上进行秒杀,,直接读取该值是不加锁的,这样会导致脏读,而写该值则是加锁的。

service层
package com.orange.service.impl;

import com.orange.config.RabbitMqConfig;
import com.orange.entity.Order;
import com.orange.utils.RedisCache;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Objects;


@Service
@Slf4j
public class MqStockServiceImpl {
    @Autowired
    private RedisCache redisCache;
    @Autowired
    private RabbitTemplate rabbitTemplate;

    
    public String secKill(String userName, String stockName) {
        log.info("参加秒杀的用户是:{},秒杀的商品是:{}", userName, stockName);
        String message = "";
        Long decrByResult =  redisCache.decrBy(stockName);
        if (Objects.nonNull(decrByResult) && decrByResult >= 0) {
            
            log.info("用户:{}, 秒杀该商品:{},库存余量{},可以进行下订单操作", userName, stockName,decrByResult);
            //发消息给库存消息队列,将库存数据减一
//            rabbitTemplate.convertAndSend(RabbitMqConfig.STORY_EXCHANGE, RabbitMqConfig.STORY_ROUTING_KEY, stockName);

            //发消息给订单消息队列,创建订单
            Order order = new Order();
            order.setOrderName(stockName);
            order.setOrderUser(userName);
            rabbitTemplate.convertAndSend(RabbitMqConfig.ORDER_EXCHANGE, RabbitMqConfig.ORDER_ROUTING_KEY, order);
            message = "用户" + userName + "秒杀" + stockName + "成功";
            //将订单保存到redis 实现限购功能
            limitNumber(userName,stockName);
        } else {
            
            log.info("用户:{}秒杀时商品的库存量没有剩余,秒杀结束", userName);
            message = "用户:"+ userName + "商品的库存量没有剩余,秒杀结束";
        }
        return message;
    }

    private void limitNumber(String userName, String stockName){
        String key = userName + ":" + stockName + ":number";
       redisCache.incrBy(key);
    }
}

服务层的思路比较简单,第一讲该商品的库存量从数据库中减1再判断其值是否大于等于0,如果为真则还有库存,那就发送一条消息到rabbitmq中 并记录该用户购买该商品的件数。
当rabbitmq有新的消息之后,消费者自动消费该消息

package com.orange.service.impl;

import com.orange.config.RabbitMqConfig;
import com.orange.entity.Order;
import com.orange.service.OrderService;
import com.orange.service.StockService;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.IOException;
import java.util.Date;


@Service
@Slf4j
public class MqOrderServiceImpl {

    @Autowired
    private OrderService orderService;
    @Autowired
    private StockService stockService;

    
    @RabbitListener(queues = RabbitMqConfig.ORDER_QUEUE ,containerFactory = "rabbitListenerContainerFactory")
    @Transactional(rollbackFor = Exception.class)
    public void saveOrder(Message message,Order order, Channel channel) throws IOException {
        log.info("收到订单消息,订单用户为:{},商品名称为:{}", order.getOrderUser(), order.getOrderName());
        
        String orderUser = order.getOrderUser();
        String orderName = order.getOrderName();
        order.setCreateBy(orderUser);
        order.setCreateDate(new Date());
        order.setUpdateBy(orderUser);
        order.setUpdateDate(new Date());
        order.setDelFlag("0");
        int i = orderService.saveOrder(order);
        int j = stockService.decrByStock(orderName);
        //事务判断
//        int e = 1/0;
        if (i>0 && j>0){
            //消费成功
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
            log.info("消费订单成功,订单用户为:{},商品名称为:{}", order.getOrderUser(), order.getOrderName());
        }else {
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,true);
            log.info("消费订单失败,订单用户为:{},商品名称为:{}", order.getOrderUser(), order.getOrderName());
        }
    }
}

@RabbitListener(queues = RabbitMqConfig.ORDER_QUEUE ,containerFactory = "rabbitListenerContainerFactory")
这条代码定义了t它是一个消费者,并且为手动确认消费该消息。在rabbitListenerContainerFactory中定义了该消费者的相关信息。

配置类
package com.orange.config;

import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


@Configuration
public class RabbitMqConfig {
    

    public static final String STORY_EXCHANGE = "STORY_EXCHANGE";

    
    public static final String ORDER_EXCHANGE = "ORDER_EXCHANGE";

    
    public static final String STORY_QUEUE = "STORY_QUEUE";

    
    public static final String ORDER_QUEUE = "ORDER_QUEUE";

    
    public static final String STORY_ROUTING_KEY = "STORY_ROUTING_KEY";

    
    public static final String ORDER_ROUTING_KEY = "ORDER_ROUTING_KEY";


    
    @Bean(name = "rabbitListenerContainerFactory")
    public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
        factory.setConnectionFactory(connectionFactory);
        factory.setMessageConverter(new Jackson2JsonMessageConverter());
        //消费数量
        factory.setPrefetchCount(50);
        return factory;
    }

    @Bean
    public MessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter();
    }

    
    @Bean
    public Exchange getStoryExchange() {
        return ExchangeBuilder.directExchange(STORY_EXCHANGE).durable(true).build();
    }

    
    @Bean
    public Queue getStoryQueue() {
        return new Queue(STORY_QUEUE,true);
    }

    
    @Bean
    public Binding bindStory() {
        return BindingBuilder.bind(getStoryQueue()).to(getStoryExchange()).with(STORY_ROUTING_KEY).noargs();
    }

    
    @Bean
    public Queue getOrderQueue() {
        return new Queue(ORDER_QUEUE);
    }

    
    @Bean
    public Exchange getOrderExchange() {
        return ExchangeBuilder.directExchange(ORDER_EXCHANGE).durable(true).build();
    }

    
    @Bean
    public Binding bindOrder() {
        return BindingBuilder.bind(getOrderQueue()).to(getOrderExchange()).with(ORDER_ROUTING_KEY).noargs();
    }

}
package com.orange.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypevalidator;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;



@Configuration
@EnableCaching
public class RedisCacheConfig extends CachingConfigurerSupport {

    @Bean
    @SuppressWarnings(value = { "unchecked", "rawtypes" })
    public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory)
    {
        RedisTemplate template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);

        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.activateDefaultTyping(LaissezFaireSubTypevalidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        serializer.setObjectMapper(mapper);

        // 使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);

        // Hash的key也采用StringRedisSerializer的序列化方式
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);

        template.afterPropertiesSet();
        return template;
    }
}
package com.orange.config;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import org.springframework.util.Assert;

import java.nio.charset.Charset;


public class FastJson2JsonRedisSerializer implements RedisSerializer
{
    @SuppressWarnings("unused")
    private ObjectMapper objectMapper = new ObjectMapper();

    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

    private Class clazz;

    static
    {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
    }

    public FastJson2JsonRedisSerializer(Class clazz)
    {
        super();
        this.clazz = clazz;
    }

    @Override
    public byte[] serialize(T t) throws SerializationException
    {
        if (t == null)
        {
            return new byte[0];
        }
        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
    }

    @Override
    public T deserialize(byte[] bytes) throws SerializationException
    {
        if (bytes == null || bytes.length <= 0)
        {
            return null;
        }
        String str = new String(bytes, DEFAULT_CHARSET);

        return JSON.parseObject(str, clazz);
    }

    public void setObjectMapper(ObjectMapper objectMapper)
    {
        Assert.notNull(objectMapper, "'objectMapper' must not be null");
        this.objectMapper = objectMapper;
    }

    protected JavaType getJavaType(Class clazz)
    {
        return TypeFactory.defaultInstance().constructType(clazz);
    }
}

工具类
package com.orange.utils;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.concurrent.TimeUnit;


@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisCache
{
    @Autowired
    public RedisTemplate redisTemplate;

    
    public  void setCacheObject(final String key, final T value)
    {
        redisTemplate.opsForValue().set(key, value);
    }

    
    public  void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
    {
        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
    }

    
    public boolean expire(final String key, final long timeout)
    {
        return expire(key, timeout, TimeUnit.SECONDS);
    }

    
    public boolean expire(final String key, final long timeout, final TimeUnit unit)
    {
        return redisTemplate.expire(key, timeout, unit);
    }

    
    public  T getCacheObject(final String key)
    {
        ValueOperations operation = redisTemplate.opsForValue();
        return operation.get(key);
    }

    
    public boolean deleteObject(final String key)
    {
        return redisTemplate.delete(key);
    }

    
    public long deleteObject(final Collection collection)
    {
        return redisTemplate.delete(collection);
    }

    
    public  long setCacheList(final String key, final List dataList)
    {
        Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
        return count == null ? 0 : count;
    }

    
    public  List getCacheList(final String key)
    {
        return redisTemplate.opsForList().range(key, 0, -1);
    }

    
    public  BoundSetOperations setCacheSet(final String key, final Set dataSet)
    {
        BoundSetOperations setOperation = redisTemplate.boundSetOps(key);
        Iterator it = dataSet.iterator();
        while (it.hasNext())
        {
            setOperation.add(it.next());
        }
        return setOperation;
    }

    
    public  Set getCacheSet(final String key)
    {
        return redisTemplate.opsForSet().members(key);
    }

    
    public  void setCacheMap(final String key, final Map dataMap)
    {
        if (dataMap != null) {
            redisTemplate.opsForHash().putAll(key, dataMap);
        }
    }

    
    public  Map getCacheMap(final String key)
    {
        return redisTemplate.opsForHash().entries(key);
    }

    
    public  void setCacheMapValue(final String key, final String hKey, final T value)
    {
        redisTemplate.opsForHash().put(key, hKey, value);
    }

    
    public  T getCacheMapValue(final String key, final String hKey)
    {
        HashOperations opsForHash = redisTemplate.opsForHash();
        return opsForHash.get(key, hKey);
    }

    
    public  List getMultiCacheMapValue(final String key, final Collection hKeys)
    {
        return redisTemplate.opsForHash().multiGet(key, hKeys);
    }

    
    public Collection keys(final String pattern)
    {
        return redisTemplate.keys(pattern);
    }

    
    public Long decrBy(String key) {
        return redisTemplate.opsForValue().decrement(key);
    }

    
    public Long incrBy(String key) {
        return redisTemplate.opsForValue().increment(key);
    }
    public Long getExpire(Object key){
        return redisTemplate.getExpire(key);
    }
}
 
package com.orange.utils;

import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;


public class ServletUtils
{
    
    public static String getParameter(String name)
    {
        return getRequest().getParameter(name);
    }



    
    public static HttpServletRequest getRequest()
    {
        return getRequestAttributes().getRequest();
    }

    
    public static HttpServletResponse getResponse()
    {
        return getRequestAttributes().getResponse();
    }

    
    public static HttpSession getSession()
    {
        return getRequest().getSession();
    }

    public static ServletRequestAttributes getRequestAttributes()
    {
        RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
        return (ServletRequestAttributes) attributes;
    }

    
    public static String renderString(HttpServletResponse response, String string)
    {
        try
        {
            response.setStatus(200);
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.getWriter().print(string);
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
        return null;
    }

}
package com.orange.utils;


public class HttpStatus
{
    
    public static final int SUCCESS = 200;

    
    public static final int CREATED = 201;

    
    public static final int ACCEPTED = 202;

    
    public static final int NO_ConTENT = 204;

    
    public static final int MOVED_PERM = 301;

    
    public static final int SEE_OTHER = 303;

    
    public static final int NOT_MODIFIED = 304;

    
    public static final int BAD_REQUEST = 400;

    
    public static final int UNAUTHORIZED = 401;

    
    public static final int FORBIDDEN = 403;

    
    public static final int NOT_FOUND = 404;

    
    public static final int BAD_METHOD = 405;

    
    public static final int ConFLICT = 409;

    
    public static final int UNSUPPORTED_TYPE = 415;

    
    public static final int ERROR = 500;

    
    public static final int NOT_IMPLEMENTED = 501;
}
启动类

服务开启时添加秒杀商品

package com.orange;

import com.orange.entity.Stock;
import com.orange.service.StockService;
import com.orange.utils.RedisCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;


import java.util.List;
import java.util.concurrent.TimeUnit;


@SpringBootApplication
public class SecKillDemoApplication implements ApplicationRunner {
    public static void main(String[] args) {
        SpringApplication.run(SecKillDemoApplication.class,args);
    }

    @Autowired
    private RedisCache redisCache;
    @Autowired
    private StockService stockService;
    @Override
    public void run(ApplicationArguments args) throws Exception {
        List stocks = stockService.selectList();
        for (Stock stock : stocks) {
            redisCache.setCacheObject(stock.getName(), stock.getStock(), 3600, TimeUnit.SECONDS);
        }

    }
}

服务关闭后删除缓存

package com.orange.job;

import com.orange.utils.RedisCache;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Collection;


@Component
@Slf4j
public class DisposableBeanImpl implements DisposableBean {
    @Autowired
    private RedisCache redisCache;
    @Override
    public void destroy() {
        Collection keys = redisCache.keys("*");
        redisCache.deleteObject(keys);
        log.info("销毁:DisposableBeanImpl.destroy");
    }
}
测试

在数据库中插入数据

jMeter测试

打开 jmeter

设置为中文

右击添加线程组

设置相关数据

添加HTTP请求,填入参数

点击此处可添加参数

由于我们的接口需要用户字段,所以可以随机产生用户
点击这个小白书

在弹出的窗口中选择如下变量

填入数据 点击生成 则会自动复制生成的表达式

在参数的值一列中填入表达式

在HTTP请求上右击添加一个观察树和汇总报告 查看测试结果

点击运行按钮即可运行

当测试开始之后可以看到控制台输出如下

说明我们的限流起到了作用

当用户抢购数量达到2之后就会提示该用户没有抢购机会,说明限制抢购数量起到了作用

消费的消息基本都在后面,说明异步起到了作用。
我们在数据库中对订单表进行统计之后发现限购数量确实成功了,此时库存已被抢空。


源码

源码下载地址如下,创作不易,也想利用积分寻找他人的资源,所以设置了需要1积分下载,如过你有需要,也有积分,欢迎下载,如果你需要,但是没有积分,可以私聊我发给你。
https://download.csdn.net/download/qazxcvbnm_/40237176.

转载请注明:文章转载自 www.mshxw.com
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号