使用技术栈:Springboot+mybatis-plus+redis
本文介绍了秒杀案例入门级的案例,欢迎各位大佬观看!
1.配置环境
1.导入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--引入令牌桶算法依赖-->
<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>27.1-jre</version>
</dependency>
<!--引入mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>3.4.1</version>
</dependency>
<!--引入mysql依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
2.配置配置文件
# tomcat端口号
server:
port: XXXX
spring:
# 配置mysql连接
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://XXXXXX
username: XXXX
password: XXXX
# 配置redis
redis:
host: XXXX
port: XXXX
password:
# 开启mybatis-plus日志
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #开启sql日志
3.配置Druid配置类
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Author XXX
* @Date 2022/1/3 16:48
* @Version 1.0
*/
@Configuration
public class DruidConfig {
@ConfigurationProperties("spring.datasource")
@Bean
public DruidDataSource druidDataSource(){
return new DruidDataSource();
}
}
4.编写mapper文件进行测试
@Repository
public interface StockMapper extends BaseMapper<Stock> {
Stock selectAll();
}
mapper.xml
<select id="selectAll" resultType="cn.hmc.springbootms.pojo.Stock">
select * from stock;
</select>
测试类:
@SpringBootTest
class Springbootms2ApplicationTests {
@Autowired
private StockMapper stockMapper;
@Autowired
private StringRedisTemplate redisTemplate;
//测试使用mapper配置文件连接数据库
@Test
void contextLoads() {
Stock stock = stockMapper.selectAll();
System.out.println(stock);
}
//测试连接redis
@Test
void testReadid(){
redisTemplate.opsForValue().set("a","123");
System.out.println(redisTemplate.opsForValue().get("a"));
}
}
2.防止超卖
1.使用悲观锁
在controller增加同步代码块
/***
* 秒杀,使用悲观锁解决
* @param userId
* @param stockId
* @return
*/
@GetMapping("/kill")
public String kill(Integer userId,Integer stockId) {
Integer kill = 0;
synchronized (this){
kill = stockService.kill(userId, stockId);
}
return "恭喜用户["+userId+"]秒杀成功!订单号为["+kill+"]!";
}
2.使用乐观锁
需要根据version字段进行更新
<update id="updateStock">
update stock set
sale = #{sale} , version = #{version} + 1
where
id = #{id} and version = #{version};
</update>
service
@Override
public Integer killCAS(Integer userId, Integer stockId) {
Stock check = check(userId, stockId);
//将出售量+1
check.setSale(check.getSale()+1);
//在sql层次将version+1,带着version作为条件进行修改
int update = stockMapper.updateStock(check);
if (update==0){
throw new StockException("抢票失败,请重试!");
}
//创建商品订单
Integer orderId = saveOrder(check.getId(), check.getName());
return orderId;
}
conreoller
/**
* 秒杀,使用乐观锁解决并发问题
* @param userId
* @param stockId
* @return
*/
@GetMapping("/killCAS")
public String killCAS(Integer userId,Integer stockId){
Integer kill = stockService.killCAS(userId, stockId);
return retrue(userId,kill);
}
总结:
-
悲观锁与乐观锁都能解决并发问题;
-
悲观锁的优点:
-
可以保证商品不会少卖。
-
-
悲观锁的缺点:
-
每次请求都上了一把锁,效率比较低。
-
-
乐观锁的优点:
-
出问题才不允许操作,效率比悲观锁高。
-
-
乐观锁的缺点:
-
请求量少的情况下会出现商品剩余问题。
-
-
3.使用redis
利用redis的事务,以及原子性保证秒杀不会出错
3.接口限流
使用令牌桶,限制用户一次性访问的请求。
还可以指定超过请求时间返回连接超时。
1.导入依赖
<!--导入令牌桶依赖-->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.1.1-jre</version>
</dependency>
2.测试
//使用令牌桶创建实例 桶大小
private RateLimiter rateLimiter = RateLimiter.create(40);
@GetMapping("/killtoken")
public String killtoken(Integer userId,Integer stockId){
//1.没有获取到token请求就一直等待,直到获取到token令牌
// double acquire = rateLimiter.acquire();
// System.out.println("开始秒杀,等待时间:"+acquire);
//2.设置等待时间,如果等待时间内没有获取令牌就丢弃
if (!rateLimiter.tryAcquire(3, TimeUnit.SECONDS)){
System.out.println("请求超时!请重新连接!");
return "请求超时!请重新连接!";
}
//模拟处理业务
System.out.println("处理业务中...");
return retrue(userId,stockId);
}
4.隐藏秒杀接口
使用redis解决
-
限时抢购
-
接口隐藏
-
单用户限制
1.限时抢购
在redis中加入商品id,并指定过期时间。
在java业务代码中判断该商品id是否存在,存在就进行秒杀, 不存在就提示错误信息!
1.在redis 中加入,并设置过期时间
set KILL_[商品id]_TIME EX [过期时间,单位秒]
2.java代码
@Autowired
private StringRedisTemplate redisTemplate;
// 判断商品是否过期
private void pastDue(Integer stockId){
String key = "KILL_"+stockId+"_TIME";
String s = redisTemplate.opsForValue().get(key);
if (s==null){
throw new StockException("秒杀时间已过,请关注下一次秒杀时间!");
}
}
@Override
public Stock check(Integer userId, Integer stockId) {
//1、判断用户id是否存在
User user = userMapper.selectOne(new QueryWrapper<User>().eq("id", userId));
if (user==null){
throw new StockException("请输入正确的用户ID!!");
}
//2、判断商品是否存在
Stock stock = stockMapper.selectOne(new QueryWrapper<Stock>().eq("id", stockId));
if (stock==null){
throw new StockException("请输入正确的商品ID!!");
}
pastDue(stockId);
//3、判断库存是否存在
if (stock.getCount().equals(stock.getSale())){
throw new StockException("库存不足!!");
}
return stock;
}
2.接口隐藏
根据用户id与商品id生成一个MD5的加密字符串,存入redis中,用户在进行秒杀的时候判断一下密匙是否正确。
1.编写生成密匙的方法
@Override
public String getMd5(Integer userId, Integer stockId) {
//验证数据的合法性·
check(userId,stockId);
//生成hashKey
String hashKey = "KEY_"+userId+"_"+stockId;
//生成md5
String md5 = DigestUtils.md5DigestAsHex((userId + stockId + " ").getBytes());
//写入redis中
redisTemplate.opsForValue().set(hashKey,md5,10, TimeUnit.SECONDS);
return md5;
}
2.使用controller调用
/**
* 获取密匙
* @param userId
* @param stockId
* @return
*/
@GetMapping("/getMd5")
public String getMd5(Integer userId,Integer stockId){
return stockService.getMd5(userId,stockId);
}
3.验证密匙是否正确
//判断密匙是否正确
private void isMD(Integer userId,Integer stockId,String md5){
//从redis中拿,判断是否有密匙
String hashkey = "KEY_"+userId+"_"+stockId;
String md5s = redisTemplate.opsForValue().get(hashkey);
if (md5s==null||!md5.equals(md5s)){
throw new StockException("密匙错误!");
}
}
4.使用密匙+用户id+商品id测试
service
//判断密匙是否正确
private void isMD(Integer userId,Integer stockId,String md5){
//从redis中拿,判断是否有密匙
String hashkey = "KEY_"+userId+"_"+stockId;
String md5s = redisTemplate.opsForValue().get(hashkey);
if (md5s==null||!md5.equals(md5s)){
throw new StockException("密匙错误!");
}
}
@Override
public Stock check(Integer userId, Integer stockId, String md5) {
//1、判断用户id是否存在
User user = userMapper.selectOne(new QueryWrapper<User>().eq("id", userId));
if (user==null){
throw new StockException("请输入正确的用户ID!!");
}
//2、判断商品是否存在
Stock stock = stockMapper.selectOne(new QueryWrapper<Stock>().eq("id", stockId));
if (stock==null){
throw new StockException("请输入正确的商品ID!!");
}
//判断秒杀是否开始
pastDue(stockId);
//判断密匙是否正确
isMD(userId,stockId,md5);
//3、判断库存是否存在
if (stock.getCount().equals(stock.getSale())){
throw new StockException("库存不足!!");
}
return stock;
}
@Override
public Integer killCASTokenMD5(Integer userId, Integer stockId, String md5) {
Stock check = check(userId, stockId,md5);
//将出售量+1
check.setSale(check.getSale()+1);
//在sql层次将version+1,带着version作为条件进行修改
int update = stockMapper.updateStock(check);
if (update==0){
throw new StockException("抢票失败,请重试!");
}
//创建商品订单
Integer orderId = saveOrder(check.getId(), check.getName());
return orderId;
}
controller
/**
* 秒杀,使用乐观锁,令牌桶限流,md5隐藏接口
* @param userId
* @param stockId
* @param md5
* @return
*/
@GetMapping("/killCASTokenMD5")
public String killCASTokenMD5(Integer userId,Integer stockId,String md5){
Integer integer = stockService.killCASTokenMD5(userId, stockId, md5);
return retrue(userId,integer);
}
3.单用户限制频率
在用户抢购的时候根据用户id+商品id生成一个键值对存入redis中,设置过期时间。
如果在过期时间内请求次数超过多少次就提示繁忙。
//判断用户同一时间访问次数是否超时
private void bindingHours(Integer userId,Integer stockId){
//1.生成键值对
String hashKey = "KILL_"+userId+"_"+stockId+"_COUNT";
//2.判断redis是否存在该键,
String key = redisTemplate.opsForValue().get(hashKey);
// 如果不存在则新增键,值为0,设置过期时间;
if (key==null){
redisTemplate.opsForValue().set(hashKey,"0",10,TimeUnit.SECONDS);
return;
}
//如果存在则判断次数是否超过10次
Integer count = Integer.parseInt(key);
if (count>=10){
throw new StockException("请求超时,请重新再试!");
}
count++;
redisTemplate.opsForValue().set(hashKey,String.valueOf(count));
}
在判断方法中加入
@Override
public Stock check(Integer userId, Integer stockId, String md5) {
//1、判断用户id是否存在
User user = userMapper.selectOne(new QueryWrapper<User>().eq("id", userId));
if (user==null){
throw new StockException("请输入正确的用户ID!!");
}
//2、判断商品是否存在
Stock stock = stockMapper.selectOne(new QueryWrapper<Stock>().eq("id", stockId));
if (stock==null){
throw new StockException("请输入正确的商品ID!!");
}
//判断秒杀是否开始
pastDue(stockId);
//判断密匙是否正确
isMD(userId,stockId,md5);
//判断用户在指定时间内是否超出点击次数
bindingHours(userId,stockId);
//3、判断库存是否存在
if (stock.getCount().equals(stock.getSale())){
throw new StockException("库存不足!!");
}
return stock;
}