秒杀活动,提高性能,防止超卖,订单超时

目录

初步思考

秒杀活动

订单防止超卖

订单超时如何处理


初步思考

原文地址

前端:页面尽可能静态化,css/js合并,减少请求数

扩容:增加机器,提高处理请求能力

限流:应用限流(nginx,tomcat设置线程池,最大请求数),服务限流(限流算法,令牌桶/漏桶),MQ堆积消息,用户请求限制(单位时间内访问接口次数)【主要保证每台机器能够处理自己能力之内的任务】

使用多线程开发,提高机器处理能力

动静分离

1、数据拆分

动静分离的首要目的是将动态页面改造成适合缓存的静态页面;第一步就是分离出动态数据,主要从以下两方面进行:

  • 用户,用户身份信息包括登录状态及登录画像等,通过动态请求获取;
  • 时间,秒杀时间是由服务端统一管控的,可以通过动态请求获取;

2、静态缓存

剩下的数据是静态数据,例如商品信息,商品图片,页面js/css等等,进行合理的缓存(生成静态页面)

静态数据缓存到哪里?浏览器,CDN,服务端

用户从CDN获取到静态数据

秒杀活动

秒杀一般在12306抢票或者电商举行的一些活动时遇到。对于一些稀缺或特价商品,电商网站一般会在约定时间点,在秒杀页面对其进行限量销售。

秒杀相关特点:

  • 秒杀时大量用过户会在同一时间同时进行抢购,网站瞬时访问流量激增。(高并发)
  • 秒杀一般是访问请求数量远远大于库存数量,只有少部分用户能够秒杀成功。(防止超卖现象)
  • 秒杀业务流程,下订单,减库存,支付(秒杀肯定是为了商品,一般不会有订单过期,取消订单,增加库存)

从架构视角来看,秒杀系统本质是一个高性能、高一致、高可用的三高系统。

设计流程

1、限流
2、将库存放到redis中、接收用户请求的时候。从redis取库存,判断库存量是否大于本次订单购买量
   库存大于本次购买量:扣减redis中的库存、并且将订单信息推送到MQ;
   库存小于本次购买量:直接返回、数量不足。
3、MQ消费者获取消息:
   1):更新数据库库存(乐观锁)
   2):生成订单信息,扣除用户账户的订单金额(余额不足的话、将本次购买量加回到库存里)
   3):异步通知用户购买结果。

原文地址

令牌机制实现秒杀业务(推荐)

利用定时任务将某些商品在规定时间之后要开启秒杀,根据库存量同步到Redis中。根据每一个商品产生对应的token数量,采用Redis中的List数据类型存储每个商品的令牌。(采用List数据类型存储的原因主要是每一个线程从List中pop时是单线程处理的,所以每一个线程拿到的令牌就不可能是同一个了)
在秒杀期间,每一个用户都去获取对应商品中的令牌(判断令牌是否有效-非空判断,如果是已经被支付过的token就不能再次被抢购)
超时未支付,归还令牌 支付成功,产生订单,标记当前令牌已经被支付,同时删除当前令牌。

原文链接

订单防止超卖

现象

  • 不同用户在读请求的时候,发现商品库存足够,然后同时发起请求,进行秒杀操作,减库存,导致库存减为负数;
  • 同一个用户在有库存的时候,连续发出多个请求,于是生成多个相同订单;

原因

数据库底层的写操作和读操作可以同时进行;写操作默认带有隐式锁,但是读操作默认是不带锁的;所以当用户1去修改库存的时候,用户2依然可以都到库存为1,所以出现了超卖现象。

方案分析

(1)Sql限制

在更新数据库减少库存时,进行库存限制条件(排他锁,也就是写锁起到了作用)

并采用数据库乐观锁(version版本号,CAS原理),如果限定条件不满足或版本号不一致就无法成功;

update table set count=count-1,version=versiono+1 where version=version and (count-1)>0;

(2)使用Redis对列实现

将要促销的商品以对列的方式存入Redis中;每当用户抢到一件商品则从队列中删除一个数据,以确保商品不会超卖。(将多线程变为单线程读写)

(3)使用Redis原子性操做命令(decr,incr),移除后同步更新数据库记录;

(1)数据库加唯一索引限制

对于生成多个相同订单,在UserId和OrderId上加唯一索引。

订单超时如何处理

原文地址

在开发中往往会遇到一些延时任务的需求

  • 生成订单30分钟未支付,则自动取消
  • 生成订单60秒后未支付,给用户发短信

延时任务和定时任务的区别在哪里呢?

  • 定时任务有明确的触发时间,延时任务没有
  • 定时任务有执行周期,而延时任务在某事件触发后一段时间内执行,没有执行周期
  • 延时任务一般是单个任务

方案分析

(1)数据库轮询

通过线程定时的去扫描数据库,通过订单时间来判断是否有超时订单,然后进行操作。

使用场景:小型项目

缺点:存在延迟(定时任务扫描的间隔),严重影响数据库性能(假如数据量过大,每几分钟扫描一次,影响其他业务)

(2)JDK延时对列

JDK自带的DelayQueue,是一个无阻塞队列,该队列只有在延迟期满的时候才能从中获取元素。放入DelayQueue中的对象,是必须要实现Delayed接口的。

方法:

  • poll():获取并移除队列的超时元素,没有则返回空。
  • take():获取并移除队列的超时元素,如果没有则wait当前线程,直到有元素满足超时条件,返回结果。
package com.test.thread;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

public class OrderDelay implements Delayed {

    private String orderID;
    private long timeout;

    public OrderDelay(String orderID, long timeout) {
        this.orderID = orderID;
        this.timeout = timeout;
    }

    /* 返回距离你自定义的超时时间还有多少 */
    @Override
    public long getDelay(TimeUnit unit) {
        return timeout - System.currentTimeMillis();
    }

    @Override
    public int compareTo(Delayed o) {
        if (o == this) {
            return 0;
        }

        OrderDelay t = (OrderDelay) o;
        long d = getDelay(TimeUnit.SECONDS) - t.getDelay(TimeUnit.SECONDS);
        return d > 0 ? 1 : 0;
    }

    public void execute() {
        System.out.println(orderID + "\t订单要被删除啦");
    }

    public static void main(String[] args) {
        LinkedHashMap contains = new LinkedHashMap<>();
        contains.put("order_0", 5);
        contains.put("order_1", 4);
        contains.put("order_2", 10);
        contains.put("order_3", 12);

        DelayQueue queue = new DelayQueue<>();
        System.out.println("任务添加到延时队列中");
        for (Map.Entry entry : contains.entrySet()) {
            queue.put(new OrderDelay(entry.getKey(), (1000 * entry.getValue() + System.currentTimeMillis())));
        }

        long start = System.currentTimeMillis();
        while (true) {
            try {
                queue.take().execute();
                System.out.println("执行时间:\t" + (System.currentTimeMillis() - start));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

优点:效率高,任务触发时间延迟低

缺点:数据存储在内存中,服务器重启,数据丢失;内存限制(订单过于庞大);

(3)时间轮算法

用Netty的HashedWheelTimer实现

package com.test.thread;

import io.netty.util.HashedWheelTimer;
import io.netty.util.Timeout;
import io.netty.util.Timer;
import io.netty.util.TimerTask;

import java.util.concurrent.TimeUnit;

public class HashedWheelTimerTest {

    static class MyTimerTask implements TimerTask {

        boolean flag;

        public MyTimerTask(boolean flag) {
            this.flag = flag;
        }

        @Override
        public void run(Timeout timeout) throws Exception {
            System.out.println("删除订单");
            this.flag = false;
        }
    }

    public static void main(String[] args) {
        MyTimerTask timerTask = new MyTimerTask(true);
        Timer timer = new HashedWheelTimer();
        timer.newTimeout(timerTask, 5, TimeUnit.SECONDS);

        int i = 1;
        while (timerTask.flag) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(i + "秒过去了");
            i++;
        }
    }
}

优点:效率高,代码复杂度比DelayQueue低,任务触发时间延迟比DelayQueue低

缺点:数据存储在内存中,服务器重启/宕机,数据丢失;内存限制(订单/任务数过多);

(4)Redis缓存

Redis的zset是一个有序集合,每一个元素都关联了一个score,通过score排序来获取集合中的值。

实现:将订单超时时间戳与订单号分别设置为score和member,系统扫描第一个元素判断是否超时。

(5)使用消息队列

RocketMQ延时消息(只支持特定级别的延迟消息,1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h)

RabbitMQ有延时对列

你可能感兴趣的:(高并发)