Springboot秒杀案例

使用技术栈: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;
}

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值