Springboot秒杀案例

Springboot秒杀案例,第1张

Springboot秒杀案例

使用技术栈:Springboot+mybatis-plus+redis

本文介绍了秒杀案例入门级的案例,欢迎各位大佬观看!

1.配置环境 1.导入依赖

   
      org.springframework.boot
      spring-boot-starter-data-redis
   

   
      org.projectlombok
      lombok
      true
   
   
      org.springframework.boot
      spring-boot-starter-test
      test
   
   
   
   
      com.google.guava
      guava
      27.1-jre
   
   
   
      com.baomidou
      mybatis-plus
      3.4.1
   
    
		
			mysql
			mysql-connector-java
		
    		
			org.springframework.boot
			spring-boot-starter-web
		
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;


@Configuration
public class DruidConfig {

    @ConfigurationProperties("spring.datasource")
    @Bean
    public DruidDataSource druidDataSource(){
        return new DruidDataSource();
    }
}
4.编写mapper文件进行测试 
@Repository
public interface StockMapper extends baseMapper {
    Stock selectAll();
}

mapper.xml

测试类:

@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增加同步代码块

@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 stock set
    sale = #{sale} , version = #{version} + 1
    where
    id = #{id} and version = #{version};

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

@GetMapping("/killCAS")
public String killCAS(Integer userId,Integer stockId){
    Integer kill = stockService.killCAS(userId, stockId);
    return retrue(userId,kill);
}

总结:

  • 悲观锁与乐观锁都能解决并发问题;

    • 悲观锁的优点:

      • 可以保证商品不会少卖。

    • 悲观锁的缺点:

      • 每次请求都上了一把锁,效率比较低。

    • 乐观锁的优点:

      • 出问题才不允许 *** 作,效率比悲观锁高。

    • 乐观锁的缺点:

      • 请求量少的情况下会出现商品剩余问题。

3.使用redis

利用redis的事务,以及原子性保证秒杀不会出错

3.接口限流

使用令牌桶,限制用户一次性访问的请求。

还可以指定超过请求时间返回连接超时。

1.导入依赖

   com.google.guava
   guava
   30.1.1-jre
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().eq("id", userId));
        if (user==null){
            throw new StockException("请输入正确的用户ID!!");
        }
        //2、判断商品是否存在
        Stock stock = stockMapper.selectOne(new QueryWrapper().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调用

@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().eq("id", userId));
    if (user==null){
        throw new StockException("请输入正确的用户ID!!");
    }
    //2、判断商品是否存在
    Stock stock = stockMapper.selectOne(new QueryWrapper().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

@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().eq("id", userId));
    if (user==null){
        throw new StockException("请输入正确的用户ID!!");
    }
    //2、判断商品是否存在
    Stock stock = stockMapper.selectOne(new QueryWrapper().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;
}

欢迎分享,转载请注明来源:内存溢出

原文地址: http://www.outofmemory.cn/zaji/5695439.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存