介绍
所谓秒杀,从业务角度看,是短时间内多个用户“争抢”资源,这里的资源在大部分秒杀场景是商品;将业务抽象,技术角度看,秒杀就是多个线程对资源进行操作,所以实现秒杀,就必须控制线程对资源的争抢,既要保证高效并发,也要保证操作的正确。
开发环境
SpringBoot+mysql+maven+JDK8+RabbitMQ+Redis
测试工具
Jmeter测试工具
工具安装参考地址
安装RabbitMQ
docker与windows版本安装与卸载:https://blog.csdn.net/SoulNone/article/details/126902764
RabbitMQ可视化工具
安装Redis
本地安装:https://blog.csdn.net/SoulNone/article/details/126681910
安装Jmeter测试工具
本地安装:https://blog.csdn.net/SoulNone/article/details/127141672
安装Maven
安装与使用:https://blog.csdn.net/SoulNone/article/details/127140413
代码模块
数据库表设计
-- 商品库存表:stock
CREATE TABLE `stock` (
`id` varchar(64) NOT NULL,
`name` varchar(255) DEFAULT NULL,
`stock` varchar(255) 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 DEFAULT CHARSET=utf8mb3 ROW_FORMAT=DYNAMIC COMMENT='商品库存表';
-- 秒杀订单表:t_order
CREATE TABLE `t_order` (
`id` varchar(64) NOT NULL,
`order_name` varchar(255) DEFAULT NULL,
`order_user` varchar(255) 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 DEFAULT CHARSET=utf8mb3 ROW_FORMAT=DYNAMIC COMMENT='秒杀订单表';
代码编码
pom.xml
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.hn.yuan</groupId>
<artifactId>demo_seckill</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo_seckill</name>
<description>Demo project for Spring Boot</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<druid.version>1.1.14</druid.version>
<mysql-connector-java.version>5.1.35</mysql-connector-java.version>
<mybatis-plus-boot-starter.version>3.1.1</mybatis-plus-boot-starter.version>
<mybatis-plus-generator.version>3.1.1</mybatis-plus-generator.version>
<swagger.version>2.9.2</swagger.version>
<fastjson.version>1.2.47</fastjson.version>
<commons-text.version>1.6</commons-text.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--依赖管理-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.1.8.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--mysql依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql-connector-java.version}</version>
</dependency>
<!--阿里巴巴fastjosn依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<!-- Druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<!-- MyBatis增强工具-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus-boot-starter.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>${mybatis-plus-generator.version}</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- websocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!--工具类包-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>${commons-text.version}</version>
</dependency>
<!--swagger-ui-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${swagger.version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${swagger.version}</version>
</dependency>
<!--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>
<!--代码生成器-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.3.0</version>
</dependency>
<!--模板引擎-->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/testtable?autoReconnect=true&useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=false
username: root
password: root
# 使用Druid数据源
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
druid:
filters: stat
maxActive: 20
initialSize: 1
maxWait: 60000
minIdle: 1
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: select 'x'
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
maxOpenPreparedStatements: 20
data:
redis:
repositories:
enabled: false
redis:
database: 0 # redis数据库索引(默认为0),我们使用索引为其他(0-15)的数据库,避免和其他数据库冲突
host: localhost
port: 6379
# password: ***
rabbitmq: #mq配置
host: localhost
port: 5672
username: guest
password: guest
mybatis-plus:
mapper-locations: classpath*:com.example.demo.mapper/*.xml
global-config:
db-config:
id-type: uuid
field-strategy: not_null
refresh: true
configuration:
map-underscore-to-camel-case: false
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
server:
port: 8090
logging:
level:
root: info # 指定日志基本
file:
path: F:\logs\zm-sms # 指定日志输出的文件路径
实体类
package com.hn.yuan.entity;
import java.io.Serializable;
import java.util.Date;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* <p>
* 商品库存表
* </p>
*
* @author XIAOCAO
* @since 2022-09-30
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class Stock implements Serializable {
private static final long serialVersionUID=1L;
private String id;
private String name;
private String stock;
/**
* 备注
*/
private String remarks;
/**
* 最后更新时间
*/
private Date updateDate;
/**
* 创建时间
*/
private Date createDate;
private String updateBy;
private String createBy;
/**
* 0正常,1删除
*/
private String delFlag;
}
package com.hn.yuan.entity;
import java.time.LocalDateTime;
import java.io.Serializable;
import java.util.Date;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* <p>
* 秒杀订单表
* </p>
*
* @author XIAOCAO
* @since 2022-09-30
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class TOrder implements Serializable {
private static final long serialVersionUID=1L;
private String id;
private String orderName;
private String orderUser;
/**
* 备注
*/
private String remarks;
/**
* 最后更新时间
*/
private Date updateDate;
/**
* 创建时间
*/
private Date createDate;
private String updateBy;
private String createBy;
/**
* 0正常,1删除
*/
private String delFlag;
}
服务层
package com.hn.yuan.service;
import com.hn.yuan.entity.Stock;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 商品库存表 服务类
* </p>
*
* @author XIAOCAO
* @since 2022-09-30
*/
public interface StockService extends IService<Stock> {
/**
* 秒杀商品后-减少库存
* @param name 商品名称
*/
void decrByStock(String name);
/**
* 秒杀商品前判断是否有库存
* @param name 商品名称
* @return
*/
Integer selectByName(String name);
/**
* 实现纯数据库操作实现秒杀操作
* @param userName 用户名称
* @param stockName 商品名称
* @return String
*/
String secDataBase(String userName,String stockName);
}
package com.hn.yuan.service;
import com.hn.yuan.entity.TOrder;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 秒杀订单表 服务类
* </p>
*
* @author XIAOCAO
* @since 2022-09-30
*/
public interface TOrderService extends IService<TOrder> {
/**
* 订单保存
* @param order 实体
*/
void saveOrder(TOrder order) throws Exception;
}
接口实现类
package com.hn.yuan.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.hn.yuan.entity.Stock;
import com.hn.yuan.entity.TOrder;
import com.hn.yuan.mapper.StockMapper;
import com.hn.yuan.mapper.TOrderMapper;
import com.hn.yuan.service.StockService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hn.yuan.service.TOrderService;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
/**
* <p>
* 商品库存表 服务实现类
* </p>
*/
@Service
@Slf4j
public class StockServiceImpl extends ServiceImpl<StockMapper, Stock> implements StockService {
@Autowired
private StockMapper stockMapper;
@Autowired
private TOrderService orderService;
/**
* 秒杀商品后-减少库存
* @param name 商品名称
*/
@Override
public void decrByStock(String name) {
List<Stock> stockList = stockMapper.selectList(new QueryWrapper<Stock>().lambda().eq(Stock::getName, name));
stockList.forEach(stock -> {
//货物购买
stock.setStock(String.valueOf(Integer.parseInt(stock.getStock())-1));
stockMapper.updateById(stock);
});
}
/**
* 秒杀商品前判断是否有库存
* @param name 商品名称
* @return
*/
@Override
public Integer selectByName(String name) {
//查询存货数量
Integer stockNum = 0;
List<Stock> stockList = stockMapper.selectList(new QueryWrapper<Stock>().lambda().eq(Stock::getName, name));
if(stockList.size() > 0){
stockNum = Integer.parseInt(stockList.get(0).getStock());
}
return stockNum;
}
/**
* 实现纯数据库操作实现秒杀操作
* @param userName 用户名称
* @param stockName 商品名称
* @return String
*/
@Override
public String secDataBase(String userName, String stockName) {
log.info("参加秒杀的用户是:{},秒杀的商品是:{}", userName, stockName);
String message = null;
//查找该商品库存
Integer stockCount = selectByName(stockName);
log.info("用户:{}参加秒杀,当前商品库存量是:{}", userName, stockCount);
if (stockCount > 0) {
/**
* 还有库存,可以进行继续秒杀,库存减一,下订单
*/
//1、库存减一
decrByStock(stockName);
//2、下订单
TOrder order = new TOrder();
order.setOrderUser(userName);
order.setOrderName(stockName);
order.setCreateBy(userName);
order.setCreateDate(new Date());
order.setUpdateBy(userName);
order.setUpdateDate(new Date());
order.setDelFlag("0");
// order.setId(IdGenerate.generateId());
try {
orderService.saveOrder(order);
}catch (Exception e){
}
log.info("用户:{}.参加秒杀结果是:成功", userName);
message = userName + "参加秒杀结果是:成功";
} else {
log.info("用户:{}.参加秒杀结果是:秒杀已经结束", userName);
message = userName + "参加秒杀活动结果是:秒杀已经结束";
}
return message;
}
}
package com.hn.yuan.service.impl;
import com.hn.yuan.entity.TOrder;
import com.hn.yuan.mapper.TOrderMapper;
import com.hn.yuan.service.TOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* <p>
* 秒杀订单表 服务实现类
* </p>
*
* @author XIAOCAO
* @since 2022-09-30
*/
@Service
public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> implements TOrderService {
@Autowired
private TOrderMapper tOrderMapper;
@Override
public void saveOrder(TOrder order) throws Exception {
if(tOrderMapper.insert(order) <= 0){
throw new Exception("数据为空");
}
}
}
配置rabbitmq的实现方式以及redis的实现方式
1.订单的消费队列
package com.hn.yuan.service.impl;
import com.hn.yuan.common.RabbitMqConfig;
import com.hn.yuan.entity.TOrder;
import com.hn.yuan.service.TOrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;
@Service
@Slf4j
public class MQOrderServiceImpl {
@Autowired
private TOrderService orderService;
/**
* MQ监听订单消息队列,并消费
* @param order
*/
@RabbitListener(queues = RabbitMqConfig.ORDER_QUEUE)
public void saveOrder(TOrder order) {
log.info("收到订单消息,订单用户为:{},商品名称为:{}", order.getOrderUser(), order.getOrderName());
/**
* 调用数据库orderService创建订单信息
*/
try {
order.setCreateBy(order.getOrderUser());
order.setCreateDate(new Date());
order.setUpdateBy(order.getOrderUser());
order.setUpdateDate(new Date());
order.setDelFlag("0");
// order.setId(IdGenerate.generateId());
orderService.saveOrder(order);
}catch (Exception e){
e.printStackTrace();
}
}
}
2.库存得消费队列
package com.hn.yuan.service.impl;
import com.hn.yuan.common.RabbitMqConfig;
import com.hn.yuan.common.RedisUtil;
import com.hn.yuan.entity.TOrder;
import com.hn.yuan.service.StockService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;
/**
* @Description: MQ存货实现层
*/
@Service
@Slf4j
public class MQStockServiceImpl {
@Autowired
private StockService stockService;
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private RedisUtil redisUtil;
/**
* 秒杀商品后-减少库存
* @param name 商品名称
*/
@RabbitListener(queues = RabbitMqConfig.STORY_QUEUE)
public void decrByStock(String name) {
log.info("库存消息队列收到的消息商品信息是:{}", name);
// 调用数据库service给数据库对应商品库存减一
stockService.decrByStock(name);
}
/**
* 使用redis+消息队列进行秒杀实现
*
* @param userName 用户
* @param stockName 商品
*/
public String secKill(String userName, String stockName) {
log.info("参加秒杀的用户是:{},秒杀的商品是:{}", userName, stockName);
String message = "";
// 1.调用redis给相应商品库存量减1
Long decrByResult = redisUtil.decrBy(stockName);
if (decrByResult >= 0) {
// 2.说明该商品的库存量有剩余,可以进行下订单操作
log.info("用户:{}秒杀该商品:{}库存有余,可以进行下订单操作", userName, stockName);
// 3.发消息给库存消息队列,将库存数据减一
rabbitTemplate.convertAndSend(RabbitMqConfig.STORY_EXCHANGE, RabbitMqConfig.STORY_ROUTING_KEY, stockName);
// 4.发消息给订单消息队列,创建订单
TOrder order = new TOrder();
order.setOrderName(stockName);
order.setOrderUser(userName);
rabbitTemplate.convertAndSend(RabbitMqConfig.ORDER_EXCHANGE, RabbitMqConfig.ORDER_ROUTING_KEY, order);
message = "用户" + userName + "秒杀" + stockName + "成功";
} else {
// 5.说明该商品的库存量没有剩余,直接返回秒杀失败的消息给用户
log.info("用户:{}秒杀时商品的库存量没有剩余,秒杀结束", userName);
message = "用户:" + userName + "商品的库存量没有剩余,秒杀结束";
}
return message;
}
}
工具类
package com.hn.yuan.common;
import org.springframework.amqp.core.*;
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;
//RabbitMQConfig插件配置
@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
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.hn.yuan.common;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.ListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @ClassName: RedisCacheConfig
* @Description: redis 缓存配置;
* 注意:RedisCacheConfig这里也可以不用继承:CachingConfigurerSupport,
* 也就是直接一个普通的Class就好了 这里主要我们之后要重新实现
* key的生成策略,只要这里修改KeyGenerator,其它位置不用修改就生效了。
* 普通使用普通类的方式的话,那么在使用@Cacheable的时候还需要指定KeyGenerator的名称;
* 这样编码的时候比较麻烦。
* @author: dispark
* @date: 2019年12月25日 下午3:30:19
*/
@Configuration
@EnableCaching // 启用缓存,这个注解很重要;
public class RedisCacheConfig {
@Autowired
private RedisConnectionFactory redisConnectionFactory;
/**
* 缓存配置初始化一个cacheManager
* @param connectionFactory
* @return
*/
@Bean
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheManager redisCacheManager = RedisCacheManager.builder(connectionFactory).build();
return redisCacheManager;
}
/**
* 防止redis入库序列化乱码的问题
* @param redisConnectionFactory
* @return RedisTemplate
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 使用Jackson2JsonRedisSerialize 替换默认序列化
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// 设置value的序列化规则和 key的序列化规则
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new StringRedisSerializer());
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
/**
* 重写hashOperations
* @param redisTemplate
* @return
*/
@Bean
public HashOperations<String, String, Object> hashOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForHash();
}
/**
* 重写listOperations
* @param redisTemplate
* @return
*/
@Bean
public ListOperations<String, Object> listOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForList();
}
/**
* redisMessageListenerContainer
* @return
*/
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer() {
RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();
redisMessageListenerContainer.setConnectionFactory(redisConnectionFactory);
return redisMessageListenerContainer;
}
}
package com.hn.yuan.common;
import com.alibaba.fastjson.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.ListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
/**
* @Description: redis工具类
*/
@Component
public class RedisUtil<T> {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private HashOperations<String, String, Object> hashOperations;
@Autowired
private ListOperations<String, Object> listOperations;
/**
* 默认过期时长,单位:秒/ 三十分钟
*/
public final static long DEFAULT_EXPIRE = 180 * 10;
/**
* 不设置过期时长
*/
public final static long NOT_EXPIRE = -1;
/**
* 普通缓存放入
*
* @param key 键
* @param value 值
*/
public void setValue(String key, T value) {
redisTemplate.opsForValue().set(key, value);
}
/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
*/
public void setValue(String key, T value, Long time) {
setValue(key, value);
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
}
/**
* 普通缓存放入并设置时间和单位
*
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @param unit 缓存设置的时间单位
*/
public void setValue(String key, T value, Long time, TimeUnit unit) {
setValue(key, value);
redisTemplate.expire(key, time, unit);
}
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public T getValue(String key) {
ValueOperations<String, T> valueOperations = redisTemplate.opsForValue();
return valueOperations.get(key);
}
/**
* 删除缓存
*
* @param key 键
*/
public void deleteValue(String key) {
redisTemplate.delete(key);
}
/**
* 判断key是否存在
*
* @param key 键
* @return true 存在 false 不存在
*/
public boolean exists(String key) {
if (getValue(key) == null) {
return false;
} else {
return true;
}
}
/**
* 放入一个map对象
*
* @param key 键
* @param map 对象
* @return true 存在 false 不存在
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
return false;
}
}
/**
* HashSet 并设置时间
*
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* Map缓存获取
*
* @param key 键
* @return map
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* 重复提交
*
* @param key 键
* @return
*/
public long repeatSubmit(String key) {
ValueOperations<String, Integer> valueOperations = redisTemplate.opsForValue();
long ret = valueOperations.increment(key, 1);
redisTemplate.expire(key, 10L, TimeUnit.SECONDS);
return ret;
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
return false;
}
}
/**
* 获取list缓存的内容
*
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
* @return
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
return null;
}
}
/**
* 删除hash值
*
* @param key key
* @param hashKeys hashKeys
*/
public void delHash(String key, String... hashKeys) {
Arrays.stream(hashKeys).forEach(hashKey -> hashOperations.delete(key, hashKeys));
}
/**
* 先根据key删除
* 然后在把list放入缓存中
*
* @param key key
* @param getHashKeyFunction 获取hashKey值的方法
* @param list list实体类
* @param expire 时间
*/
public <T> void putHashList(String key, Function<T, String> getHashKeyFunction, List<T> list, Long expire) {
putHashList(key, getHashKeyFunction, list);
if (expire != NOT_EXPIRE) {
redisTemplate.expire(key, expire, TimeUnit.SECONDS);
}
}
/**
* 先根据key删除
* 然后在把list放入缓存中
*
* @param key 键
* @param getHashKeyFunction
* @param list
* @param <T>
*/
public <T> void putHashList(String key, Function<T, String> getHashKeyFunction, List<T> list) {
deleteValue(key);
list.stream().filter(distinctByValue(getHashKeyFunction)).forEach(model -> putHashModel(key, getHashKeyFunction, model));
}
/**
* 将实体类放到Hash里面,并设置时间
*
* @param key key
* @param getHashKeyFunction 获取hashKey值的方法
* @param model 实体类
* @param <T> T
*/
public <T> void putHashModel(String key, Function<T, String> getHashKeyFunction, T model, Long expire) {
putHashModel(key, getHashKeyFunction, model);
if (expire != NOT_EXPIRE) {
redisTemplate.expire(key, expire, TimeUnit.SECONDS);
}
}
/**
* 将实体类放到Hash里面
*
* @param key key
* @param getHashKeyFunction 获取hashKey值的方法
* @param model 实体类
* @param <T> T
*/
public <T> void putHashModel(String key, Function<T, String> getHashKeyFunction, T model) {
hashOperations.put(key, getHashKeyFunction.apply(model), JSONObject.toJSON(model).toString());
}
/**
* 根据有效时间,key 获取List
*
* @param key key
* @param clazz clazz
* @param <T> T
* @return List<T>
*/
public <T> List<T> getHashList(String key, Class<T> clazz, Long expire) {
if (expire != NOT_EXPIRE) {
redisTemplate.expire(key, expire, TimeUnit.SECONDS);
}
return getHashList(key, clazz);
}
/**
* 获取全部list
*
* @param key
* @param clazz
* @param <T>
* @return
*/
public <T> List<T> getHashList(String key, Class<T> clazz) {
Optional<List<Object>> objectList = Optional.ofNullable(hashOperations.values(key));
return objectList.map(objects -> objects.stream()
.map(t -> JSONObject.parseObject(JSONObject.toJSON(t).toString(), clazz))
.collect(Collectors.toList())).orElse(null);
}
/**
* 根据 key 和 hashKey 获取值
*
* @param key key
* @param hashKey hashKey
* @param clazz clazz
* @param <T> T
* @return T
*/
public <T> T getHashObject(String key, String hashKey, Class<T> clazz) {
Optional<Object> object = Optional.ofNullable(hashOperations.get(key, hashKey));
if (object.isPresent()) {
return JSONObject.parseObject(JSONObject.toJSON(object).toString(), clazz);
}
return null;
}
/**
* 去除重复
*
* @param keyExtractor
* @param <T>
* @return
*/
public static <T> Predicate<T> distinctByValue(Function<? super T, ?> keyExtractor) {
ConcurrentHashMap<Object, Boolean> map = new ConcurrentHashMap<>();
return t -> map.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
}
/**
* 判断key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
return false;
}
}
/**
* 获取有效全部list
*
* @param key key
* @param clazz clazz
* @param <T> T
* @param expire 超时时间
* @return return
*/
public <T> List<T> getList(String key, Class<T> clazz, Long expire) {
if (expire != NOT_EXPIRE) {
redisTemplate.expire(key, expire, TimeUnit.SECONDS);
}
return getList(key, clazz);
}
/**
* 获取全部list
*
* @param key
* @param clazz
* @param <T>
* @return
*/
public <T> List<T> getList(String key, Class<T> clazz) {
Optional<List<Object>> list = Optional.ofNullable(listOperations.range(key, 0, listOperations.size(key)));
return list.map(objects -> objects.stream()
.map(t -> JSONObject.parseObject(JSONObject.toJSON(t).toString(), clazz))
.collect(Collectors.toList())).orElse(null);
}
/**
* 先根据key删除
* 然后在把list放入缓存中
*
* @param key 键
* @param getValueFunction 获取value值的方法
* @param list list
* @param <T> T
* @param expire 超时时间
*/
public <T> void leftPushList(String key, List<T> list, Function<T, String> getValueFunction, Long expire) {
leftPushList(key, list, getValueFunction);
if (expire != NOT_EXPIRE) {
redisTemplate.expire(key, expire, TimeUnit.SECONDS);
}
}
/**
* 实体类去重放入listOperations
*
* @param key
* @param list
* @param getValueFunction 获取value值的方法
* @param <T>
*/
public <T> void leftPushList(String key, List<T> list, Function<T, String> getValueFunction) {
deleteValue(key);
list.stream().filter(distinctByValue(getValueFunction)).forEach(model -> leftPushModel(key, model, getValueFunction));
}
/**
* 实体类放入
*
* @param key key
* @param model model
* @param getValueFunction 获取value值的方法
* @return <T>
* @author dispark
* @date 2021/9/10 9:54 上午
*/
public <T> void leftPushModel(String key, T model, Function<T, String> getValueFunction) {
listOperations.leftPush(key, getValueFunction.apply(model));
}
/**
* 对指定key的键值减1
*
* @param key key
* @return {@link java.lang.Long}
* @author dispark
* @date 2021/9/10 9:53 上午
*/
public Long decrBy(String key) {
return redisTemplate.opsForValue().decrement(key);
}
}
package com.hn.yuan.common;
import lombok.Data;
@Data
public class Result {
private int code;
private Object data;
private String msg;
public Result(int code, Object data, String msg) {
this.code = code;
this.data = data;
this.msg = msg;
}
}
控制层
package com.hn.yuan.controller;
import com.hn.yuan.common.Result;
import com.hn.yuan.service.StockService;
import com.hn.yuan.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;
/**
* @Description: 秒杀
*/
@RestController
@Api(value = "SecKillController", tags = "秒杀控制层")
@Slf4j
public class SecKillController {
@Autowired
private MQStockServiceImpl mqStockService;
@Autowired
private StockService stockService;
/**
* 使用redis+消息队列进行秒杀实现
*
* @param userName 用户名称
* @param stockName 商品名称
*/
@PostMapping(value = "sec-kill", produces = "application/json")
@ApiOperation(value = "redis+消息队列进行秒杀实现", notes = "redis+消息队列进行秒杀实现", produces = "application/json")
public Result secKill(@RequestParam(value = "userName") String userName, @RequestParam(value = "stockName") String stockName) {
return new Result(200,mqStockService.secKill(userName, stockName),"ok");
}
/**
* 实现纯数据库操作实现秒杀操作
* @param userName 用户名称
* @param stockName 商品名称
*/
@PostMapping(value = "sec-data-base", produces = "application/json;")
@ApiOperation(value = "实现纯数据库操作实现秒杀操作", notes = "实现纯数据库操作实现秒杀操作", produces = "application/json")
public Result secDataBase(@RequestParam(value = "userName") String userName, @RequestParam(value = "stockName") String stockName) {
return new Result(200,stockService.secDataBase(userName, stockName),"ok");
}
}
启动项目
打开JMeter并发环境测试
测试计划右键,添加一个线程组
给这个线程组的数量为40,这个线程组的作用就是模拟40个用户发送请求,去秒杀;然后再在线程组右键,添加一个Http请求,这个就是我们用来发送请求的组件了
这个请求唯一要说得就是,随机参数了,因为用户名肯定不可能给40个相同得名字,这边我们利用JMeter给用户名得值为随机数
点击上方得白色小书本,选择random,1-99得随机数。
开始测试
log日志
待完善开发-后续补充
各位看官》创作不易,点个赞!!!
诸君共勉:万事开头难,只愿肯放弃。
版权声明:本文为SoulNone原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。