使用技术栈:Springboot+mybatis-plus+redis
本文介绍了秒杀案例入门级的案例,欢迎各位大佬观看!
1.配置环境 1.导入依赖2.配置配置文件org.springframework.boot spring-boot-starter-data-redisorg.projectlombok lomboktrue org.springframework.boot spring-boot-starter-testtest com.google.guava guava27.1-jre com.baomidou mybatis-plus3.4.1 mysql mysql-connector-javaorg.springframework.boot spring-boot-starter-web
# 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); }
总结:
-
悲观锁与乐观锁都能解决并发问题;
-
悲观锁的优点:
-
可以保证商品不会少卖。
-
-
悲观锁的缺点:
-
每次请求都上了一把锁,效率比较低。
-
-
乐观锁的优点:
-
出问题才不允许 *** 作,效率比悲观锁高。
-
-
乐观锁的缺点:
-
请求量少的情况下会出现商品剩余问题。
-
-
利用redis的事务,以及原子性保证秒杀不会出错
3.接口限流使用令牌桶,限制用户一次性访问的请求。
还可以指定超过请求时间返回连接超时。
1.导入依赖2.测试com.google.guava guava30.1.1-jre
//使用令牌桶创建实例 桶大小 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解决
-
限时抢购
-
接口隐藏
-
单用户限制
在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 QueryWrapper2.接口隐藏().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; }
根据用户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; }
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)