• 极客专栏正式上线!欢迎访问 https://www.jikewenku.com/topic.html
  • 极客专栏正式上线!欢迎访问 https://www.jikewenku.com/topic.html

秒杀系统设计架构与实现

技术杂谈 勤劳的小蚂蚁 3个月前 (01-30) 73次浏览 已收录 0个评论 扫描二维码

最近做了一个点餐的平台,其中涉及到一个很重要的问题,活动期间的秒杀系统的实现。
抢购/秒杀是如今很常见的一个应用场景,是高并发编程的一个挑战,在网上也找了一些资料,大部分都是理论,关于java的实现也是很少,就算有也是很简单的demo,为此,决定将此次实现的秒杀系统整理一番,发布出来。
架构思路
Question1: 由于要承受高并发,mysql在高并发情况下的性能下降尤其严重,下图为Mysql性能瓶颈测试。
而且硬盘持久化的io操作将耗费大量资源。所以决定采用基于内存操作的redis,redis的密集型io
Question2: 秒杀系统必然是一个集群系统,在硬件不提升的情况下利用nginx做负载均衡也是不错的选择。
实现难点
1. 超买超卖问题的解决。
2. 订单持久化,多线程将订单信息写入数据库
解决方案
1. 采用redis的分布式乐观锁,解决高并发下的超买超卖问题.
2. 使用countDownLatch作为计数器,将数据四线程写入数据库,订单的持久化过程在我的机器上效率提升了1000倍。
进阶方案
1.访问量还是大。系统还是撑不住。
2.防止用户刷新页面导致重复提交。
3.脚本攻击
解决思路
1.访问量还是过大的话,要看性能瓶颈在哪里,一般来说首先撑不住的是tomcat,考虑优化tomcat,单个tomcat经过实践并发量撑住1000是没有问题的。先搭建tomcat集群,如果瓶颈出现在redis上的话考虑集群redis,这时候消息队列也是必须的,至于采用哪种消息队列框架还是根据实际情况。
2.问题2和问题3其实属于同一个问题。这个问题其实属于网络问题的范畴,和我们的秒杀系统不在一个层面上。因此不应该由我们来解决。很多交换机都有防止一个源IP发起过多请求的功能。开源软件也有不少能实现这点。如linux上的TC可以控制。流行的Web服务器Nginx(它也可以看做是一个七层软交换机)也可以通过配置做到这一点。一个IP,一秒钟我就允许你访问我2次,其他软件包直接给你丢了,你还能压垮我吗?
交换机也不行了呢?
可能你们的客户并发访问量实在太大了,交换机都撑不住了。 这也有办法。我们可以用多个交换机为我们的秒杀系统服务。 原理就是DNS可以对一个域名返回多个IP,并且对不同的源IP,同一个域名返回不同的IP。如网通用户访问,就返回一个网通机房的IP;电信用户访问,就返回一个电信机房的IP。也就是用CDN了! 我们可以部署多台交换机为不同的用户服务。 用户通过这些交换机访问后面数据中心的Redis Cluster进行秒杀作业。
我是在springboot + SpringData JPA的环境下实现的系统。引入了spring-data-redis的依赖
<dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-data-redis</artifactId>
 </dependency>
config包下有两个类
publicinterface SecKillConfig {
   String productId = “1234568”;
//这是我数据库中的要秒杀的商品id

}

这个类的作用主要是配置RedisTemplate,否则使用默认的RedisTemplate会使key和value乱码。
@Configuration
@EnableCaching
publicclassRedisConfig {
   @Bean
   public CacheManager cacheManager(RedisTemplate<?, ?> redisTemplate){
       CacheManager cacheManager = new RedisCacheManager(redisTemplate);
       return cacheManager;
   }
   // 以下两种redisTemplate自由根据场景选择
   @Bean
   public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
       RedisTemplate<Object, Object> template = new RedisTemplate<>();
       template.setConnectionFactory(connectionFactory);
       Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);
       ObjectMapper mapper = new ObjectMapper();
       mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
       mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
       serializer.setObjectMapper(mapper);
       template.setValueSerializer(serializer);
       //使用StringRedisSerializer来序列化和反序列化redis的key值
       template.setKeySerializer(new StringRedisSerializer());
       //这两句是关键
       template.setHashKeySerializer(new StringRedisSerializer());
       //这两句是关键
       template.afterPropertiesSet();
       returntemplate;
   }
   @Bean
   public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory){
       StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
       stringRedisTemplate.setConnectionFactory(factory);
       return stringRedisTemplate;
   }
}
下面是util包
publicclassKeyUtil{
   publicstatic   synchronized String   getUniqueKey(){
       Random random = new Random();
       Integer num = random.nextInt(100000);
       return  num.toString()+System.currentTimeMillis();
   }

}

publicclassSecUtils{
   
   /*
   创建虚拟订单
    */
   public  static SecOrder createDummyOrder(ProductInfo productInfo){
       String key = KeyUtil.getUniqueKey();
       SecOrder secOrder = new SecOrder();
       secOrder.setId(key);
       secOrder.setUserId(“userId=”+key);
       secOrder.setProductId(productInfo.getProductId());
       secOrder.setProductPrice(productInfo.getProductPrice());
       secOrder.setAmount(productInfo.getProductPrice());
       return secOrder;
   }
   
  /*
  伪支付
   */
   publicstatic  booleandummyPay(){
       Random random = new Random();
       int result = random.nextInt(1000) % 2;
       if (result == 0){
           returntrue;
       }
       returnfalse;
   }
}
下面是重点,分布式锁的解决
/**
* 分布式乐观锁
*/
@Component
@Slf4j
publicclassRedisLock {
   @Autowired
   private StringRedisTemplate redisTemplate;
   @Autowired
   private ProductService productService;
   /*
   加锁
    */
   public boolean lock(String key,String value){
       //setIfAbsent对应redis中的setnx,key存在的话返回false,不存在返回true
       if ( redisTemplate.opsForValue().setIfAbsent(key,value)){
           returntrue;
       }
       //两个问题,Q1超时时间
       String currentValue = redisTemplate.opsForValue().get(key);
       if (!StringUtils.isEmpty(currentValue) && Long.parseLong(currentValue) < System.currentTimeMillis()){
           //Q2 在线程超时的时候,多个线程争抢锁的问题
           String oldValue = redisTemplate.opsForValue().getAndSet(key, value);
           if (!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue)){
               returntrue;
           }
       }
       returnfalse;
   }
   publicvoidunlock(String key ,String value){
       try{
           String currentValue = redisTemplate.opsForValue().get(key);
           if (!StringUtils.isEmpty(currentValue) && currentValue.equals(value)){
               redisTemplate.opsForValue().getOperations().delete(key);
           }
       }catch(Exception e){
           log.error(“redis分布上锁解锁异常, {}”,e);
       }
   }
   public SecProductInfo refreshStock(String productId){
       SecProductInfo secProductInfo = new SecProductInfo();
       ProductInfo productInfo = productService.findOne(productId);
       if (productId == null){
           thrownew SellException(203,“秒杀商品不存在”);
       }
       try{
           redisTemplate.opsForValue().set(“stock”+productInfo.getProductId(),String.valueOf(productInfo.getProductStock()));
           String value = redisTemplate.opsForValue().get(“stock”+productInfo.getProductId());
           secProductInfo.setProductId(productId);
           secProductInfo.setStock(value);
       }catch(Exception e){
           log.error(e.getMessage());
       }
       return secProductInfo;
   }

}

分布式锁的实现思路
线程进来之后先执行redis的setnx,若是key存在就返回0,否则返回1.返回1即代表拿到锁,开始执行代码,执行完毕之后将key删除即为解锁。
存在两个问题,有可能存在死锁,就是一个线程执行拿到锁之后,解锁之前的代码时出现bug,导致锁释放不出来,下一个线程进来之后一直等待上一个线程释放锁。解决方案就是加上超时时间,超时过后自行无论执行是否成功都将锁释放出来。但是又会出现第二个问题,在超时的情况下,多个线程同时等待锁释放出来,然后竞争拿到锁,此时又会出现线程不安全现象,解决方案是使用redis的getandset方法,其中一个线程拿到锁之后立即将value值改变,同时将oldvalue与原来的value值比较,这样就保证了多线程竞争锁的安全性。
下面是业务逻辑部分的代码。
先是controller
@RestController
@Slf4j
@RequestMapping(“/skill”)
publicclass SecKillController {
   @Autowired
   private SecKillService secKillService;
   @Autowired
   private StringRedisTemplate stringRedisTemplate;
   @Resource
   private RedisTemplate<String,SecOrder> redisTemplate;
 
   /*
   下单,同时将订单信息保存在redis中,随后将数据持久化
    */
   @GetMapping(“/order/{productId}”)
   publicString skill(@PathVariableString productId) throws Exception{
       //判断是否抢光
       int amount = Integer.valueOf(stringRedisTemplate.opsForValue().get(“stock”+productId));
       if (amount >= 2000){
           return“不好意思,活动结束啦”;
       }
       //初始化抢购商品信息,创建虚拟订单。
       ProductInfo productInfo = new ProductInfo(productId);
       SecOrder secOrder = SecUtils.createDummyOrder(productInfo);
       //付款,付款时时校验库存,如果成功redis存储订单信息,库存加1
       if (!SecUtils.dummyPay()){
           log.error(“付款慢啦抢购失败,再接再厉哦”);
           return“抢购失败,再接再厉哦”;
       }
       log.info(“抢购成功 商品id=:”+ productId);
       //订单信息保存在redis中
       secKillService.orderProductMockDiffUser(productId,secOrder);
       return“订单数量: “+redisTemplate.opsForSet().size(“order”+productId)+
               ”  剩余数量:”+(2000 – Integer.valueOf(stringRedisTemplate.opsForValue().get(“stock”+productId)));
   }
   /*
    在redis中刷新库存
    */
   @GetMapping(“/refresh/{productId}”)
   publicString  refreshStock(@PathVariableString productId) throws Exception{
       SecProductInfo secProductInfo = secKillService.refreshStock(productId);
       return“库存id为 “+productId +” <br>  库存总量为 “+secProductInfo.getStock();
   }
}
Service
@Service
publicinterfaceSecKillService{
    longorderProductMockDiffUser(String productId,SecOrder secOrder);
   
    SecProductInfo refreshStock(String productId);

}

Impl
@Service
@Slf4j
publicclassSecKillServiceImplimplementsSecKillService{
   @Autowired
   private RedisLock redisLock;
   @Autowired
   private SecOrderService secOrderService;
   @Autowired
   private StringRedisTemplate stringRedisTemplate;
   @Resource
   private RedisTemplate<String,SecOrder> redisTemplate;
   privatestaticfinalint TIMEOUT = 10 * 1000;
   @Override
   public  longorderProductMockDiffUser(String productId,SecOrder secOrder){
       //加锁 setnx
       long orderSize = 0;
       long time = System.currentTimeMillis()+ TIMEOUT;
       boolean lock = redisLock.lock(productId, String.valueOf(time));
       if (!lock){
           throw  new SellException(200,“哎呦喂,人太多了”);
       }
     //获得库存数量
       int stockNum = Integer.valueOf(stringRedisTemplate.opsForValue().get(“stock”+productId));
       if (stockNum >= 2000) {
           thrownew SellException(150, “活动结束”);
       } else {
           //仓库数量减一
           stringRedisTemplate.opsForValue().increment(“stock”+productId,1);
           //redis中加入订单
           redisTemplate.opsForSet().add(“order”+productId,secOrder);
           orderSize = redisTemplate.opsForSet().size(“order”+productId);
           if (orderSize >= 1000){
               //订单信息持久化,多线程写入数据库(效率从单线程的9000s提升到了9ms)
               Set<SecOrder> members = redisTemplate.opsForSet().members(“order”+productId);
               List<SecOrder> memberList = new ArrayList<>(members);
               CountDownLatch countDownLatch = new CountDownLatch(4);
               new Thread(() -> {
                   for (int i = 0; i <memberList.size() /4 ; i++) {
                       secOrderService.save(memberList.get(i));
                       countDownLatch.countDown();
                   }
               }, “therad1”).start();
               new Thread(() -> {
                   for (int i = memberList.size() /4; i <memberList.size() /2 ; i++) {
                       secOrderService.save(memberList.get(i));
                       countDownLatch.countDown();
                   }
               }, “therad2”).start();
               new Thread(() -> {
                   for (int i = memberList.size() /2; i <memberList.size() * 3 / 4 ; i++) {
                       secOrderService.save(memberList.get(i));
                       countDownLatch.countDown();
                   }
               }, “therad3”).start();
               new Thread(() -> {
                   for (int i = memberList.size() * 3 / 4; i <memberList.size(); i++) {
                       secOrderService.save(memberList.get(i));
                       countDownLatch.countDown();
                   }
               }, “therad4”).start();
               try {
                   countDownLatch.await();
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
              log.info(“订单持久化完成”);
           }
       }
       //解锁
       redisLock.unlock(productId,String.valueOf(time));
       return orderSize;
   }
   @Override
   public SecProductInfo refreshStock(String productId){
       return redisLock.refreshStock(productId);
   }
}
还有一些辅助的service,和实体类,不过多解释,一起贴出来吧,方便大家测试
publicinterfaceSecOrderService {
    List<SecOrder> findByProductId(String productId);
    SecOrder save(SecOrder secOrder);

}

@Service
publicclassSecOrderServiceImplimplementsSecOrderService{
   @Autowired
   private SecOrderRepository secOrderRepository;
   @Override
   public List<SecOrder> findByProductId(String productId){
       return secOrderRepository.findByProductId(productId);
   }
   public SecOrder save(SecOrder secOrder){
       return secOrderRepository.save(secOrder);
   }
}
publicinterfaceSecOrderRepositoryextendsJpaRepository<SecOrder,String> {
   List<SecOrder> findByProductId(String productId);
   SecOrder save(SecOrder secOrder);

}

@Entity
@Data
publicclass ProductInfo {
   @Id
   privateString productId;
   /**
    * 产品名
    */
   privateString productName;
   /**
    * 单价
    */
   private BigDecimal productPrice;
   /**
    * 库存
    */
   private Integer productStock;
   /**
    * 产品描述
    */
   privateString productDescription;
   /**
    * 小图
    */
   privateString productIcon;
   /**
    * 商品状态 0正常 1下架
    */
   private Integer productStatus = ProductStatusEnum.Up.getCode();
   /**
    * 类目编号
    */
   private Integer categoryType;
   /** 创建日期*/
   @JsonSerialize(using = Date2LongSerializer.class)
   privateDate createTime;
   /**更新时间 */
   @JsonSerialize(using = Date2LongSerializer.class)
   privateDate updateTime;
   @JsonIgnore
   public ProductStatusEnum getProductStatusEnum(){
       return EnumUtil.getBycode(productStatus,ProductStatusEnum.class);
   }
   public ProductInfo(String productId) {
       this.productId = productId;
       this.productPrice = new BigDecimal(3.2);
   }
   public ProductInfo() {
   }
}
@Data
@Entity
publicclassSecOrder  implementsSerializable{
   privatestaticfinallong serialVersionUID = 1724254862421035876L;
       @Id
   private String id;
   private String userId;
   private String productId;
   private BigDecimal productPrice;
   private BigDecimal amount;
   publicSecOrder(String productId){
       String utilId = KeyUtil.getUniqueKey();
       this.id = utilId;
       this.userId = “userId”+utilId;
       this.productId = productId;
   }
   publicSecOrder(){
   }
   @Override
   public String toString(){
       return“SecOrder{“ +
               “id='” + id + ”’ +
               “, userId='” + userId + ”’ +
               “, productId='” + productId + ”’ +
               “, productPrice=” + productPrice +
               “, amount=” + amount +
               ‘}’;
   }

}

@Data
publicclass SecProductInfo {
   privateString productId;
   privateString stock;
}

丨极客文库, 版权所有丨如未注明 , 均为原创丨
本网站采用知识共享署名-非商业性使用-相同方式共享 3.0 中国大陆许可协议进行授权
转载请注明原文链接:秒杀系统设计架构与实现
喜欢 (0)
[247507792@qq.com]
分享 (0)
勤劳的小蚂蚁
关于作者:
温馨提示:本文来源于网络,转载文章皆标明了出处,如果您发现侵权文章,请及时向站长反馈删除。

您必须 登录 才能发表评论!

  • 精品技术教程
  • 编程资源分享
  • 问答交流社区
  • 极客文库知识库

客服QQ


QQ:2248886839


工作时间:09:00-23:00