SSM整合Redis模拟简单高并发秒杀
引言
1. 开发环境
工具 | 版本 |
---|---|
SpringBoot | 2.3.0.RELEASE |
JDK | 1.8 |
MySQL | 8.0 |
Redis | 3.2 |
2. 数据库设计
CREATE TABLE `curriculum` (
`currId` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`currName` varchar(30) COLLATE utf8mb4_0900_as_ci NOT NULL,
`volumeMax` int(255) NOT NULL,
`teacherNo` varchar(20) COLLATE utf8mb4_0900_as_ci NOT NULL,
PRIMARY KEY (`currId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_as_ci;
INSERT INTO `curriculum` VALUES ('1101', '深度学习', 60, '01');
CREATE TABLE `result` (
`currId` varchar(30) COLLATE utf8mb4_0900_as_ci NOT NULL,
`stuNo` varchar(11) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_as_ci;
3. 业务实现
3.1 添加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 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.3.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.redis</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.16</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</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>
3.2 配置YML文件
server:
port: 8080
spring:
application:
name: redis_demo
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
url: jdbc:mysql://127.0.0.1:3306/curricula_variable_test?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
host: localhost
port: 6379
jedis:
pool:
max-active: 1024
max-wait: -1s
max-idle: 200
mybatis:
type-aliases-package: com.redis.demo.model
3.3 映射实体类
@Data
public class Curriculum {
private String currId;
private String currName;
private int volumeMax;
private String teacherNo;
}
@Data
public class Result {
@NonNull
private String currId;
@NonNull
private String stuNo;
}
3.4 Dao层实现
package com.redis.demo.mapper;
import com.redis.demo.model.Curriculum;
import org.apache.ibatis.annotations.*;
import org.springframework.stereotype.Repository;
/**
* @author C_dinghai
*/
@Mapper
@Repository
public interface CurrMapper {
@Select("select * from curriculum where currId = #{currId} limit 1")
Curriculum currInfo(@Param("currId") String currId);
@Select("select volumeMax from curriculum2 where currId = #{currId} limit 1")
int sqlcurrInfo(@Param("currId") String currId);
@Update("update curriculum2 set volumeMax = volumeMax - 1 where currId = #{currId}")
void updateInfo(@Param("currId")String currId);
//更新剩余量
@Update("update curriculum set volumeMax = volumeMax - 1 where currId = #{currId}")
void updateRedisInfo(@Param("currId")String currId);
}
package com.redis.demo.mapper;
import com.redis.demo.model.Result;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
/**
* @author C_dinghai
*/
@Mapper
@Repository
public interface ResultMapper {
//插入选课结果
@Insert("insert into result (currId,stuNo) values (#{currId}, #{stuNo})")
void insertResult(Result result);
@Insert("insert into result2 (currId,stuNo) values (#{currId}, #{stuNo})")
void insertResult2(Result result);
}
3.5 RedisConfig配置类
package com.redis.demo.redis;
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.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @author C_dinghai
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
template.setConnectionFactory(redisConnectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setHashKeySerializer(new GenericJackson2JsonRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
3.6 service层业务类实现
3.6.1 mybatis整合redis做高并发处理
package com.redis.demo.service;
import com.redis.demo.mapper.CurrMapper;
import com.redis.demo.mapper.ResultMapper;
import com.redis.demo.model.Curriculum;
import com.redis.demo.model.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* @author C_dinghai
*/
@Service
@Slf4j
public class RedisCurrService {
@Resource
private RedisTemplate redisTemplate;
@Autowired
private CurrMapper currMapper;
@Autowired
private ResultMapper resultMapper;
public String readInfo(String currId){
Curriculum curriculum = currMapper.currInfo(currId);
//读取数据库数据至缓存;
redisTemplate.opsForValue().set(curriculum.getCurrId(),curriculum.getVolumeMax());
if (redisTemplate.opsForValue().get(currId) != null) {
log.info("读取数据库数据至缓存成功");
return "读取数据库数据至缓存成功";
}
else{
log.info("读取数据库数据至缓存失败");
return "读取数据库数据至缓存失败";
}
}
public String sec(Result result){
log.info("学号为:"+ result.getStuNo() + "提交选择的课程编号:" + result.getCurrId());
if((int)redisTemplate.opsForValue().get(result.getCurrId()) > 0){
//缓存减1
redisTemplate.opsForValue().decrement(result.getCurrId());
resultMapper.insertResult(result);
//更新数据库
currMapper.updateRedisInfo(result.getCurrId());
log.info("选课成功!" + result.getStuNo() + "选择的课程为:" + result.getCurrId() + "当前还剩:"+redisTemplate.opsForValue().get(result.getCurrId()));
return "选课成功";
}
else {
log.info("课程人数已满");
return "课程人数已满";
}
}
}
3.6.2 只使用mybatis做高并发处理
package com.redis.demo.service;
import com.redis.demo.mapper.CurrMapper;
import com.redis.demo.mapper.ResultMapper;
import com.redis.demo.model.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @author C_dinghai
*/
@Service
@Slf4j
public class SqlCurrService {
@Autowired
private CurrMapper currMapper;
@Autowired
private ResultMapper resultMapper;
public int sqlcurrInfo(String currId){
int num = currMapper.sqlcurrInfo(currId);
log.info("当前剩余:" + num);
return num;
}
public void updateInfo(String currId){
currMapper.updateInfo(currId);
log.info("更新剩余量");
}
public void insertResult(Result result){
resultMapper.insertResult2(result);
log.info("生成结果");
}
}
3.7 controller层实现
package com.redis.demo.controller;
import com.redis.demo.model.Result;
import com.redis.demo.service.RedisCurrService;
import org.apache.ibatis.annotations.Param;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
/**
* @author C_dinghai
*/
@RestController
public class SecController {
@Autowired
private RedisCurrService currService;
@GetMapping("updataInfo")
public String updateInfo(@Param("currId")String currId){
return currService.readInfo(currId);
}
@PostMapping("/sec")
public String sec(@RequestBody Result result){
return currService.sec(result);
}
}
package com.redis.demo.controller;
import com.redis.demo.model.Result;
import com.redis.demo.service.SqlCurrService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
/**
* @author C_dinghai
*/
@RestController
public class SqlSecController {
@Autowired
SqlCurrService sqlCurrService;
@PostMapping("/sqlSec")
public String secInfo(@RequestBody Result result){
if(sqlCurrService.sqlcurrInfo(result.getCurrId()) > 0){
sqlCurrService.insertResult(result);
sqlCurrService.updateInfo(result.getCurrId());
return "选课成功!" + result.getStuNo() + "选了课程编号:" + result.getCurrId();
}
else {
return "选课失败!课程剩余为0";
}
}
}
4. jmeter压测
创建一个线程组,设置1s内100个线程的并发量,让这100个线程去抢60个名额
然后添加http请求
后台运行程序,jmeter发送请求;
控制台打印出了日志
打开MySQL数据库查看result表,
在并发量骤增的情况下,整合redis作缓存的结果没有发生超量的现象。
清空控制台,同样用100个线程去访问提交只用了mybatis作高并发处理的接口
查看数据库的result2表;
发现一共插入了96条结果记录,而课程容量最大只有60,所以出现了超量现象。