秒杀系统设计架构与实现

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

抢购/秒杀是如今很常见的一个应用场景,是高并发编程的一个挑战,在网上也找了一些资料,大部分都是理论,关于java的实现也是很少,就算有也是很简单的demo,为此,决定将此次实现的秒杀系统整理一番,发布出来。

架构思路

Question1: 由于要承受高并发,mysql在高并发情况下的性能下降尤其严重,下图为Mysql性能瓶颈测试。

秒杀系统设计架构与实现_第1张图片

而且硬盘持久化的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的依赖

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

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 redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate 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 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 +" 
库存总量为 "+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 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 members = redisTemplate.opsForSet().members("order"+productId);
                List memberList = new ArrayList<>(members);
                CountDownLatch countDownLatch = new CountDownLatch(4);
                new Thread(() -> {
                    for (int i = 0; i  {
                    for (int i = memberList.size() /4; i  {
                    for (int i = memberList.size() /2; i  {
                    for (int i = memberList.size() * 3 / 4; i 

还有一些辅助的service,和实体类,不过多解释,一起贴出来吧,方便大家测试

public interface SecOrderService {

     List findByProductId(String productId);

     SecOrder save(SecOrder secOrder);

}
@Service
public class SecOrderServiceImpl implements SecOrderService {

    @Autowired
    private SecOrderRepository secOrderRepository;

    @Override
    public List findByProductId(String productId) {

        return secOrderRepository.findByProductId(productId);
    }

    public SecOrder save(SecOrder secOrder){

        return secOrderRepository.save(secOrder);
    }

}
public interface SecOrderRepository extends JpaRepository {

    List 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;

}





你可能感兴趣的:(秒杀系统设计架构与实现)