最近学习了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文件大部分依赖包均已说明其作用
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.orange</groupId>
<artifactId>seckill_demo</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--RabbitMQ-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!-- Jedis客户端依赖 -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<!-- druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<!-- swagger2 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>3.0.0</version>
</dependency>
<!--定时任务-->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
</dependency>
<!--fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
</dependencies>
</project>
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;
/**
* @author cc
*/
@RestController
@Slf4j
@Api(value = "SecKillController", tags = "秒杀控制层")
public class SecKillController {
@Autowired
private MqStockServiceImpl mqStockService;
/**
* 使用redis+消息队列进行秒杀实现
* @param userName 用户名称
* @param stockName 商品名称
* @return String
*/
@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;
/**
* 请求限流
* @author cc
*/
@Retention(RUNTIME)
@Target(ElementType.METHOD)
public @interface AccessLimit {
/**
* 时间范围 (单位:秒)
* @return
*/
int seconds();
/**
* 在这个时间范围内最大访问次数
* @return
*/
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;
/**
* 限流拦截器
* @author 陈诚
*/
@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<maxCount){
redisCache.incrBy(key);
redisCache.expire(key,seconds,TimeUnit.SECONDS);
log.info("地址{},在{}秒内第{}次被访问次数",key,seconds,count+1);
}else {
log.info("地址{},在{}秒内已达到最大访问次数",key,seconds);
AjaxResult ajaxResult = AjaxResult.error("抢购失败,再接再厉!");
ServletUtils.renderString(response, JSONObject.toJSONString(ajaxResult));
return false;
}
}
return true;
}
}
拦截器拦截有该注解的请求,没有的则放行,有的则将该请求地址加入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;
/**
* @author cc
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private AccessLimitInterceptor accessLimitInterceptor;
/**
* 配置拦截器
*
* @param registry
* @author yuqingquan
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(accessLimitInterceptor).addPathPatterns("/**");
}
}
LimitNumberAspect
用的是AOP实现的,按照逻辑来说用户秒杀是需要用户登录的,但本例中没有设计这么多功能,因此只是提供了一个限制购买数量的思路,所以该注解写得耦合度很高,下面只贴出代码,不作过多说明。
package com.orange.aspectj;
import com.orange.annotation.LimitNumber;
import com.orange.exception.CustomException;
import com.orange.utils.RedisCache;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Objects;
/**
* 限购数量切面
* @author cc
*/
@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;
/**
* @author cc
*/
@Service
@Slf4j
public class MqStockServiceImpl {
@Autowired
private RedisCache redisCache;
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 使用redis+消息队列进行秒杀实现
* @param userName 用户名称
* @param stockName 商品名称
* @return String
*/
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;
/**
* @author cc
*/
@Service
@Slf4j
public class MqOrderServiceImpl {
@Autowired
private OrderService orderService;
@Autowired
private StockService stockService;
/**
* MQ监听订单消息队列,并消费
* @param order
*/
@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());
/**
* 调用数据库orderService创建订单信息
*/
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;
/**
* @author cc
*/
@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";
/**
*
* @param connectionFactory
* @return SimpleRabbitListenerContainerFactory
*/
@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();
}
/**
* 创建库存交换机
* @return
*/
@Bean
public Exchange getStoryExchange() {
return ExchangeBuilder.directExchange(STORY_EXCHANGE).durable(true).build();
}
/**
* 创建库存队列
* @return
*/
@Bean
public Queue getStoryQueue() {
return new Queue(STORY_QUEUE,true);
}
/**
* 库存交换机和库存队列绑定
* @return
*/
@Bean
public Binding bindStory() {
return BindingBuilder.bind(getStoryQueue()).to(getStoryExchange()).with(STORY_ROUTING_KEY).noargs();
}
/**
* 创建订单队列
* @return
*/
@Bean
public Queue getOrderQueue() {
return new Queue(ORDER_QUEUE);
}
/**
* 创建订单交换机
* @return
*/
@Bean
public Exchange getOrderExchange() {
return ExchangeBuilder.directExchange(ORDER_EXCHANGE).durable(true).build();
}
/**
* 订单队列与订单交换机进行绑定
* @return
*/
@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;
/**
* @author cc
*/
@Configuration
@EnableCaching
public class RedisCacheConfig extends CachingConfigurerSupport {
@Bean
@SuppressWarnings(value = { "unchecked", "rawtypes" })
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
{
RedisTemplate<Object, Object> 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;
/**
* Redis使用FastJson序列化
*
* @author cc
*/
public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
{
@SuppressWarnings("unused")
private ObjectMapper objectMapper = new ObjectMapper();
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
private Class<T> clazz;
static
{
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
}
public FastJson2JsonRedisSerializer(Class<T> 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;
/**
* spring redis 工具类
*
* @author cc
**/
@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisCache
{
@Autowired
public RedisTemplate redisTemplate;
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
*/
public <T> void setCacheObject(final String key, final T value)
{
redisTemplate.opsForValue().set(key, value);
}
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
* @param timeout 时间
* @param timeUnit 时间颗粒度
*/
public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
{
redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
}
/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @return true=设置成功;false=设置失败
*/
public boolean expire(final String key, final long timeout)
{
return expire(key, timeout, TimeUnit.SECONDS);
}
/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @param unit 时间单位
* @return true=设置成功;false=设置失败
*/
public boolean expire(final String key, final long timeout, final TimeUnit unit)
{
return redisTemplate.expire(key, timeout, unit);
}
/**
* 获得缓存的基本对象。
*
* @param key 缓存键值
* @return 缓存键值对应的数据
*/
public <T> T getCacheObject(final String key)
{
ValueOperations<String, T> operation = redisTemplate.opsForValue();
return operation.get(key);
}
/**
* 删除单个对象
*
* @param key
*/
public boolean deleteObject(final String key)
{
return redisTemplate.delete(key);
}
/**
* 删除集合对象
*
* @param collection 多个对象
* @return
*/
public long deleteObject(final Collection collection)
{
return redisTemplate.delete(collection);
}
/**
* 缓存List数据
*
* @param key 缓存的键值
* @param dataList 待缓存的List数据
* @return 缓存的对象
*/
public <T> long setCacheList(final String key, final List<T> dataList)
{
Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
return count == null ? 0 : count;
}
/**
* 获得缓存的list对象
*
* @param key 缓存的键值
* @return 缓存键值对应的数据
*/
public <T> List<T> getCacheList(final String key)
{
return redisTemplate.opsForList().range(key, 0, -1);
}
/**
* 缓存Set
*
* @param key 缓存键值
* @param dataSet 缓存的数据
* @return 缓存数据的对象
*/
public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet)
{
BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
Iterator<T> it = dataSet.iterator();
while (it.hasNext())
{
setOperation.add(it.next());
}
return setOperation;
}
/**
* 获得缓存的set
*
* @param key
* @return
*/
public <T> Set<T> getCacheSet(final String key)
{
return redisTemplate.opsForSet().members(key);
}
/**
* 缓存Map
*
* @param key
* @param dataMap
*/
public <T> void setCacheMap(final String key, final Map<String, T> dataMap)
{
if (dataMap != null) {
redisTemplate.opsForHash().putAll(key, dataMap);
}
}
/**
* 获得缓存的Map
*
* @param key
* @return
*/
public <T> Map<String, T> getCacheMap(final String key)
{
return redisTemplate.opsForHash().entries(key);
}
/**
* 往Hash中存入数据
*
* @param key Redis键
* @param hKey Hash键
* @param value 值
*/
public <T> void setCacheMapValue(final String key, final String hKey, final T value)
{
redisTemplate.opsForHash().put(key, hKey, value);
}
/**
* 获取Hash中的数据
*
* @param key Redis键
* @param hKey Hash键
* @return Hash中的对象
*/
public <T> T getCacheMapValue(final String key, final String hKey)
{
HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
return opsForHash.get(key, hKey);
}
/**
* 获取多个Hash中的数据
*
* @param key Redis键
* @param hKeys Hash键集合
* @return Hash对象集合
*/
public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
{
return redisTemplate.opsForHash().multiGet(key, hKeys);
}
/**
* 获得缓存的基本对象列表
*
* @param pattern 字符串前缀
* @return 对象列表
*/
public Collection<String> keys(final String pattern)
{
return redisTemplate.keys(pattern);
}
/**
* 对指定key的键值减一
* @param key 键
* @return Long
*/
public Long decrBy(String key) {
return redisTemplate.opsForValue().decrement(key);
}
/**
* 对指定key的键值增一
* @param key 键
* @return Long
*/
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;
/**
* 客户端工具类
*
* @author cc
*/
public class ServletUtils
{
/**
* 获取String参数
*/
public static String getParameter(String name)
{
return getRequest().getParameter(name);
}
/**
* 获取request
*/
public static HttpServletRequest getRequest()
{
return getRequestAttributes().getRequest();
}
/**
* 获取response
*/
public static HttpServletResponse getResponse()
{
return getRequestAttributes().getResponse();
}
/**
* 获取session
*/
public static HttpSession getSession()
{
return getRequest().getSession();
}
public static ServletRequestAttributes getRequestAttributes()
{
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
return (ServletRequestAttributes) attributes;
}
/**
* 将字符串渲染到客户端
*
* @param response 渲染对象
* @param string 待渲染的字符串
* @return null
*/
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;
/**
* 返回状态码
*
* @author cc
*/
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;
/**
* 不允许的http方法
*/
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;
/**
* @author cc
*/
@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<Stock> 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;
/**
* @author cc
*/
@Component
@Slf4j
public class DisposableBeanImpl implements DisposableBean {
@Autowired
private RedisCache redisCache;
@Override
public void destroy() {
Collection<String> keys = redisCache.keys("*");
redisCache.deleteObject(keys);
log.info("销毁:DisposableBeanImpl.destroy");
}
}
测试
在数据库中插入数据
jMeter测试
打开 jmeter
设置为中文
右击添加线程组
设置相关数据
添加HTTP请求,填入参数
点击此处可添加参数
由于我们的接口需要用户字段,所以可以随机产生用户
点击这个小白书
在弹出的窗口中选择如下变量
填入数据 点击生成 则会自动复制生成的表达式
在参数的值一列中填入表达式
在HTTP请求上右击添加一个观察树和汇总报告 查看测试结果
点击运行按钮即可运行
当测试开始之后可以看到控制台输出如下
说明我们的限流起到了作用
当用户抢购数量达到2之后就会提示该用户没有抢购机会,说明限制抢购数量起到了作用
消费的消息基本都在后面,说明异步起到了作用。
我们在数据库中对订单表进行统计之后发现限购数量确实成功了,此时库存已被抢空。
源码
https://download.youkuaiyun.com/download/qazxcvbnm_/40237176.