伍佰目录 短网址
  当前位置:海洋目录网 » 站长资讯 » 站长资讯 » 文章详细 订阅RssFeed

秒杀系统设计架构与实现 **

来源:本站原创 浏览:162次 时间:2021-08-01

作者:小亮

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

知音专栏

程序员的出路

写程序时该追求什么,什么是次要的?

如何准备Java初级和高级的技术面试

算法的力量,李开复聊算法的重要性

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

抢购/秒杀是如今很常见的一个应用场景,是高并发编程的一个挑战,在网上也找了一些资料,大部分都是理论,关于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@EnableCachingpublic 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@Slf4jpublic 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 SecKillServic���ϳ���,���ﳹ��e 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

@Servicepublic interface SecKillService {     long orderProductMockDiffUser(String productId,SecOrder secOrder);          SecProductInfo refreshStock(String productId);}

Impl

@Service@Slf4jpublic 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);}
@Servicepublic 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@Datapublic 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@Entitypublic 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 +                '}';    }}
@Datapublic class SecProductInfo {    private String productId;    private String stock;}

  推荐站点

  • At-lib分类目录At-lib分类目录

    At-lib网站分类目录汇集全国所有高质量网站,是中国权威的中文网站分类目录,给站长提供免费网址目录提交收录和推荐最新最全的优秀网站大全是名站导航之家

    www.at-lib.cn
  • 中国链接目录中国链接目录

    中国链接目录简称链接目录,是收录优秀网站和淘宝网店的网站分类目录,为您提供优质的网址导航服务,也是网店进行收录推广,站长免费推广网站、加快百度收录、增加友情链接和网站外链的平台。

    www.cnlink.org
  • 35目录网35目录网

    35目录免费收录各类优秀网站,全力打造互动式网站目录,提供网站分类目录检索,关键字搜索功能。欢迎您向35目录推荐、提交优秀网站。

    www.35mulu.com
  • 就要爱网站目录就要爱网站目录

    就要爱网站目录,按主题和类别列出网站。所有提交的网站都经过人工审查,确保质量和无垃圾邮件的结果。

    www.912219.com
  • 伍佰目录伍佰目录

    伍佰网站目录免费收录各类优秀网站,全力打造互动式网站目录,提供网站分类目录检索,关键字搜索功能。欢迎您向伍佰目录推荐、提交优秀网站。

    www.wbwb.net