秒杀系统设计架构与实现

Posted Java知音

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了秒杀系统设计架构与实现相关的知识,希望对你有一定的参考价值。

个人主页:https://blog.csdn.net/qq_27631217

知音专栏

 


最近做了一个点餐的平台,其中涉及到一个很重要的问题,活动期间的秒杀系统的实现。


抢购/秒杀是如今很常见的一个应用场景,是高并发编程的一个挑战,在网上也找了一些资料,大部分都是理论,关于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包下有两个类

public interface SecKillConfig {
   String productId = "1234568";
//这是我数据库中的要秒杀的商品id
}


这个类的作用主要是配置RedisTemplate,否则使用默认的RedisTemplate会使key和value乱码。

@Configuration
@EnableCaching
public class RedisConfig {
   @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();
       return template;
   }
   @Bean
   public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory)
{
       StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
       stringRedisTemplate.setConnectionFactory(factory);
       return stringRedisTemplate;
   }
}


下面是util包

public class KeyUtil {

   public static   synchronized String   getUniqueKey(){
       Random random = new Random();
       Integer num = random.nextInt(100000);
       return  num.toString()+System.currentTimeMillis();
   }
}


public class SecUtils {
   
   /*
   创建虚拟订单
    */

   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;
   }
   
  /*
  伪支付
   */

   public static  boolean dummyPay(){
       Random random = new Random();
       int result = random.nextInt(1000) % 2;
       if (result == 0){
           return true;
       }
       return false;
   }
}


下面是重点,分布式锁的解决

/**
* 分布式乐观锁
*/

@Component
@Slf4j
public class RedisLock {

   @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)){
           return true;
       }
       //两个问题,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)){
               return true;
           }
       }
       return false;
   }

   public void unlock(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){
           throw new 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")
public class SecKillController {

   @Autowired
   private SecKillService secKillService;

   @Autowired
   private StringRedisTemplate stringRedisTemplate;

   @Resource
   private RedisTemplate<String,SecOrder> redisTemplate;
 
   /*
   下单,同时将订单信息保存在redis中,随后将数据持久化
    */

   @GetMapping("/order/{productId}")
   public String skill(@PathVariable String 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}")
   public String  refreshStock(@PathVariable String productId) throws Exception{
       SecProductInfo secProductInfo = secKillService.refreshStock(productId);
       return "库存id为 "+productId +" <br>  库存总量为 "+secProductInfo.getStock();
   }

}


Service

@Service
public interface SecKillService {

    long orderProductMockDiffUser(String productId,SecOrder secOrder);
   
    SecProductInfo refreshStock(String productId);
}


Impl

@Service
@Slf4j
public class SecKillServiceImpl implements SecKillService {

   @Autowired
   private RedisLock redisLock;

   @Autowired
   private SecOrderService secOrderService;

   @Autowired
   private StringRedisTemplate stringRedisTemplate;

   @Resource
   private RedisTemplate<String,SecOrder> redisTemplate;

   private static final int TIMEOUT = 10 * 1000;

   @Override
   public  long orderProductMockDiffUser(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) {
           throw new 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,和实体类,不过多解释,一起贴出来吧,方便大家测试

public interface SecOrderService {

    List<SecOrder> findByProductId(String productId);

    SecOrder save(SecOrder secOrder);

}


@Service
public class SecOrderServiceImpl implements SecOrderService {

   @Autowired
   private SecOrderRepository secOrderRepository;

   @Override
   public List<SecOrder> findByProductId(String productId) {

       return secOrderRepository.findByProductId(productId);
   }

   public SecOrder save(SecOrder secOrder){

       return secOrderRepository.save(secOrder);
   }

}


public interface SecOrderRepository extends JpaRepository<SecOrder,String> {

   List<SecOrder> findByProductId(String productId);


   SecOrder save(SecOrder secOrder);

}


@Entity
@Data
public class ProductInfo {

   @Id
   private String productId;
   /**
    * 产品名
    */

   private String productName;
   /**
    * 单价
    */

   private BigDecimal productPrice;
   /**
    * 库存
    */

   private Integer productStock;
   /**
    * 产品描述
    */

   private String productDescription;
   /**
    * 小图
    */

   private String productIcon;
   /**
    * 商品状态 0正常 1下架
    */

   private Integer productStatus = ProductStatusEnum.Up.getCode();
   /**
    * 类目编号
    */

   private Integer categoryType;

   /** 创建日期*/
   @JsonSerialize(using = Date2LongSerializer.class)
   private Date createTime;
   /**更新时间 */
   @JsonSerialize(using = Date2LongSerializer.class)
   private Date 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
public class SecOrder  implements Serializable{

   private static final long serialVersionUID = 1724254862421035876L;

       @Id
   private String id;
   private String userId;
   private String productId;
   private BigDecimal productPrice;
   private BigDecimal amount;

   public SecOrder(String productId) {
       String utilId = KeyUtil.getUniqueKey();
       this.id = utilId;
       this.userId = "userId"+utilId;
       this.productId = productId;
   }

   public SecOrder() {
   }

   @Override
   public String toString() {
       return "SecOrder{" +
               "id='" + id + '\'' +
               ", userId='" + userId + '\'' +
               ", productId='" + productId + '\'' +
               ", productPrice=" + productPrice +
               ", amount=" + amount +
               '}';
   }
}


@Data
public class SecProductInfo {

   private String productId;
   private String stock;

}


【Java知音】实时干货推荐栏


以上是关于秒杀系统设计架构与实现的主要内容,如果未能解决你的问题,请参考以下文章

互联网 Java 秒杀系统设计与实现

实战:剖析基于SpringCloud“秒杀”架构(附代码)

电商商品秒杀系统架构分析与实战

大神开源秒杀系统设计与实现!

八个维度讲解秒杀系统架构分析与实战

秒杀架构