秒杀是一种高并发场景,通常指的是在短时间内(秒级别)有大量用户同时访问某个商品或服务,争相抢购的情景。在这种情况下,系统需要处理大量并发请求,确保公平性、一致性,并防止因并发而导致的问题,例如超卖、恶意请求等。以下是在高并发秒杀场景下需要考虑的一些关键问题和解决方案:
超卖问题: 大量用户同时抢购同一商品可能导致超卖(卖出超过库存数量)的问题。为了解决这个问题,可以采用悲观锁或乐观锁的方式来控制库存的访问。数据库的行级锁、分布式锁等技术都可以用来防止超卖。
性能优化: 高并发场景下,系统性能是关键。使用缓存、异步处理、CDN 加速等手段可以显著提升系统的性能。缓存可以存储商品信息、用户状态等,减轻数据库压力。异步处理可以将一些不需要即时返回结果的操作异步执行,减轻请求的响应时间。
并发控制: 在高并发场景下,为了防止系统崩溃或服务不可用,需要对并发进行控制。可以使用队列、限流等技术,确保系统在承受能力范围内处理请求,防止系统超负荷崩溃。
秒杀令牌和时间窗口: 可以在系统中引入秒杀令牌,只有携带有效令牌的用户才能参与秒杀。同时,可以设置一个时间窗口,只在特定的时间范围内允许秒杀操作,有效控制请求的涌入。
用户鉴权和防刷: 针对恶意请求,需要进行用户鉴权,并采用防刷策略。例如,限制同一用户在短时间内的请求次数,通过验证码等方式增加用户请求的成本,防止恶意请求。
队列和异步处理: 使用消息队列将用户的秒杀请求进行排队,然后异步处理。这样可以有效地削峰填谷,减轻系统瞬时的压力,提高系统的容错能力。
分布式事务: 如果系统是分布式的,需要考虑分布式事务的问题。确保在秒杀过程中的各个阶段,包括扣减库存、生成订单等,能够保持事务的一致性。
实时监控和日志记录: 在高并发场景下,实时监控是及时发现问题、解决问题的关键。记录详细的日志信息,包括用户请求日志、系统性能日志等,便于事后分析和优化。
Redis 秒杀
Mysql数据库设计
/* SQLyog Community v11.26 (32 bit) MySQL - 8.0.33 : Database - test ********************************************************************* */ /*!40101 SET NAMES utf8 */; /*!40101 SET SQL_MODE=''*/; /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; CREATE DATABASE /*!32312 IF NOT EXISTS*/`test` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci */ /*!80016 DEFAULT ENCRYPTION='N' */; USE `test`; /*Table structure for table `stock` */ DROP TABLE IF EXISTS `stock`; CREATE TABLE `stock` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `name` VARCHAR(20) DEFAULT NULL, `count` INT DEFAULT NULL, `create_time` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=INNODB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*Data for the table `stock` */ INSERT INTO `stock`(`id`,`name`,`count`,`create_time`) VALUES (1,'apple',500,'2023-11-28 19:02:04'),(2,'huawei',500,'2023-11-28 19:02:26'); /*Table structure for table `stock_order` */ DROP TABLE IF EXISTS `stock_order`; CREATE TABLE `stock_order` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `name` VARCHAR(20) DEFAULT NULL, `price` INT DEFAULT NULL, `create_time` TIMESTAMP NULL DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=INNODB AUTO_INCREMENT=1729467951815541250 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*Data for the table `stock_order` */ /*Table structure for table `article_select` */ DROP TABLE IF EXISTS `article_select`; /*!50001 DROP VIEW IF EXISTS `article_select` */; /*!50001 DROP TABLE IF EXISTS `article_select` */; /*!50001 CREATE TABLE `article_select`( `a` bigint , `b` varchar(11) , `c` varchar(20) , `d` bigint )*/; /*View structure for view article_select */ /*!50001 DROP TABLE IF EXISTS `article_select` */; /*!50001 DROP VIEW IF EXISTS `article_select` */; /*!50001 CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`localhost` SQL SECURITY DEFINER VIEW `article_select` (`a`,`b`,`c`,`d`) AS select `article`.`id` AS `id`,`article`.`name` AS `name`,`article`.`des` AS `des`,`article`.`categoryid` AS `categoryid` from `article` */; /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
Mysql秒杀实现
秒杀代码设计初步代码如下:
@RestController public class MyController { @Autowired StockMapper stockMapper; @Autowired StockOrderMapper stockOrderMapper; @Transactional @GetMapping("/order/{id}") public String order(@PathVariable("id") Long id){ Stock stock = stockMapper.selectById(id); Integer count = stock.getCount(); if(count<=0){ throw new RuntimeException("库存不足"); } StockOrder stockOrder=new StockOrder(); stockOrder.setName(stock.getName()); stockOrderMapper.insert(stockOrder); UpdateWrapper<Stock> updateWrapper=new UpdateWrapper<>(); updateWrapper.setSql("count = count - 1 where count > 0 and id ="+id); //在mysql这里执行的时候,数据库会加行锁,所以相对是安全的 int update = stockMapper.update(null, updateWrapper); if(update<=0){ throw new RuntimeException("库存不足"); } return "success"; } }
由于业务代码直接与mysql数据库进行交互,mysql一秒支持的并发量低,性能较低,然后下面进行压测:
压测得到的汇总报告如下图:
Mysql+Redis秒杀实现
使用redis修改代码如下:
@RestController public class MyController { @Autowired StringRedisTemplate stringRedisTemplate; @Autowired StockMapper stockMapper; @Autowired StockOrderMapper stockOrderMapper; @PostConstruct public void init(){ List<Stock> stocks = stockMapper.selectList(null); for (Stock stock : stocks) { stringRedisTemplate.opsForValue().set("product_"+stock.getId(),stock.getCount()+""); } } @GetMapping("/order/{id}") public String order(@PathVariable("id") Long id){ Long decrement = stringRedisTemplate.opsForValue().decrement("product_" + id); if(decrement<0){ stringRedisTemplate.opsForValue().increment("product_"+id); return "库存不足"; } try { ((MyController)AopContext.currentProxy()).mys_order(id); }catch (Exception e){ stringRedisTemplate.opsForValue().increment("product_"+id); return "库存不足"; } return "购买成功"; } @Transactional public void mys_order(Long id){ Stock stock = stockMapper.selectById(id); if(stock.getCount()<=0){ throw new RuntimeException("库存不足"); } StockOrder stockOrder=new StockOrder(); stockOrder.setName(stock.getName()); stockOrderMapper.insert(stockOrder); UpdateWrapper<Stock> updateWrapper=new UpdateWrapper<>(); updateWrapper.setSql("count = count - 1 where count > 0 and id ="+id); //在mysql这里执行的时候,数据库会加行锁,所以相对是安全的 int update = stockMapper.update(null, updateWrapper); if(update<=0){ throw new RuntimeException("库存不足"); } } }
压测结果吞吐量如下图,使用redis作为缓存相对于仅仅使用mysql数据库吞吐量提升了不少,性能得到了提升。