这篇文章我将介绍关于写入流量高的处理技巧,并且介绍一款快手开源的很有用的用来做流量聚合的东西BufferTrigger。经过这篇文章的介绍,信任关于大多写流量(后边就称为写qps吧)高的事务场景都能找到处理方案。

读流量(qps)高的场景其实见的更多,比方常用的淘宝和抖音,大部分都是读场景,而写的流量相对读的流量整体要低不少。可是假如遇到做活动,比方电商促销秒杀;或许一些自身的确写流量高的场景,比方直播(点赞,收礼)等有什么好的处理方案呢,下面经过个人遇到的一些事务场景来介绍实际用到的处理方案以及运用的场景和局限性。

音讯行列

运用音讯行列进行流量削峰来处理写qps高的问题应该是最常见的处理方案,将每次的恳求扔到音讯行列,后续由顾客渐渐处理即可,运用音讯行列能够自己控制qps和线程数,所以就能够缓解下流的压力了。

运用场景介绍

这儿就举两个常见的例子

  1. 电商秒杀:每次用户秒杀的结果虽然在页面展现了,实际上一般是把恳求丢到了音讯行列了,后续渐渐消费处理(db中库存的改动等);
  2. 直播间收礼:在一些大V开直播的时分,直播收礼的流量10W/s很正常,服务端需求对礼物进行归类和核算等处理,这明显也能够运用mq去异步处理;

注意事项

音讯行列的处理异步处理的,所以关于需求及时响应处理结果的事务或许就不大合适了。当然,关于处理结果比较固定,比方便是回来成功(商城秒杀),然后交由音讯行列处理并且确保处理成功的领域,不在考虑范围。

设置mq的消费速度要评价好,确保不能打垮下流服务;同时关于不断发生音讯的事务,也不能让消费速度赶不上生产速度,导致音讯不断堆积,引爆行列,必要时要对下流进行恰当的横向扩容。总归,要在功能和消费速度之间进行权衡,找到一个平衡点。

流量聚合-BufferTrigger

BufferTrigger引进

流量聚合简略来说便是把屡次的恳求整合为一个恳求处理,明显只有在事务对单词的恳求不灵敏时并且能承受必定推迟时才干运用,比方直播间点赞,主播对单次点赞底子不灵敏,直播间展现的赞数也不是实时的,彻底能够对屡次点赞进行聚合,最终再进行一些列判断再累计起来存放到db或缓存中,直播间从db或缓存中进行拉取。

那么这个聚合的东西需求什么功用呢?简略来说首要就三点

  1. 供给个能存放数据的容器
  2. 当到达指定数量时输出容器中的数据
  3. 当到达指定时分时输出容器中的数据

刚好快手供给了这么个东西-BufferTrigger,这个东西在快手内部很多运用,特别是主站直播,下面将简略介绍下这个简略易用的东西。

运用场景

对很多的数据进行聚合,然后进行批量操作,适用于数据量大且类似或相同数据多的使命或许能承受必定时刻内的推迟问题

怎么运用

引进依赖

<dependency>
  <groupId>com.github.phantomthief</groupId>
  <artifactId>buffer-trigger</artifactId>
  <version>0.2.9</version>
</dependency>

运用方法1

public class BufferTriggerDemo {
     BufferTrigger<Long> bufferTrigger = BufferTrigger.<Long, Map<Long, AtomicInteger>> simple()
            .maxBufferCount(10)
            .interval(4, TimeUnit.SECONDS)
            .setContainer(ConcurrentHashMap::new, (map, uid) -> {
                map.computeIfAbsent(uid, key -> new AtomicInteger()).addAndGet(1);
                return true;
            })
            .consumer(this::consumer)
            .build();
    public void consumer(Map<Long, AtomicInteger> map) {
        System.out.println(map);
    }
    public void test() throws InterruptedException {
        // 进程退出时手动消费一次
        Runtime.getRuntime().addShutdownHook(new Thread(() -> bufferTrigger.manuallyDoTrigger()));
        // 最大容量是10,这儿尝试增加11个元素0-10
        for (int i = 0; i < 5; i ++) {
            for (long j = 0; j < 11; j ++) {
                bufferTrigger.enqueue(j);
            }
        }
        Thread.sleep(7000);
    }

运用simple办法来进行构建

  1. maxBuffeCount(long count): 指定容器最大容量,比方这儿指定了10,当在下次聚合前容器元素数量到达10就无法增加了,-1表明无限制;
  2. internal(long interval, TimeUnit unit) :表明多久聚合一次,假如没到达时刻那么consumer是不会输出的,聚合后容器就空了。
  3. setContainer(Supplier<? extends C> factory, BiPredicate<? super C, ? super E> queueAdder): 第一个变量为factory,是个Supplier,获取容器用的,要求线程安全;第二个变量是缓存更新的办法BiPredicate<? super C, ? super E> queueAdder C为容器类型,E为元素类型
  4. consumer(ThrowableConsumer<? super C, Throwable> consumer): 表明怎么消费聚合后的数据,标识我们怎么去消费聚合后的数据,我这儿便是简略打印。
  5. enqueue(E element): 增加元素;
  6. manuallyDoTrigger: 自动触发一次消费,通常在java进程封闭的时分调用

执行一遍,输入如下

{0=5, 1=5, 2=5, 3=5, 4=5, 5=5, 6=5, 7=5, 8=5, 9=5}

运用方法2

每次将元素原封不动保存下来,然后一次性消费一整个列表元素。而上面的方法,每次增加元素都会进行核算。

public class BufferTriggerDemo2 {
     BufferTrigger<Long> bufferTrigger = BufferTrigger.<Long>batchBlocking()
             .bufferSize(50)
             .batchSize(10)
             .linger(Duration.ofSeconds(1))
             .setConsumerEx(this::consume)
             .build();
    private void consume(List<Long> nums) {
        System.out.println(nums);
    }
    public void test() throws InterruptedException {
        // 进程退出时手动消费一次
        Runtime.getRuntime().addShutdownHook(new Thread(() -> bufferTrigger.manuallyDoTrigger()));
        for (long j = 0; j < 60; j ++) {
            bufferTrigger.enqueue(j);
        }
        Thread.sleep(7000);
    }
  1. batchBlocking():供给自带背压(back-pressure)的简略批量归并消费能力;
  2. bufferSize(int bufferSize): 缓存行列的最大容量;
  3. batchSize(int size): 批处理元素的数量阈值,到达这个数量后也会进行消费
  4. linger(Duration duration): 多久消费一次
  5. setConsumerEx(ThrowableConsumer<? super List, Exception> consumer): 消费函数,注入的目标为缓存行列中尚存的一切元素,非逐个元素消费;

执行一遍,输入如下

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
[20, 21, 22, 23, 24, 25, 26, 27, 28, 29]
[30, 31, 32, 33, 34, 35, 36, 37, 38, 39]
[40, 41, 42, 43, 44, 45, 46, 47, 48, 49]
[50, 51, 52, 53, 54, 55, 56, 57, 58, 59]

full gc圈套和消费能力提速

需求注意的是BufferTrigger是单线程消费的,这个是一个比较大的圈套,做活动的时分是踩了坑的,特别是在消费操作中涉及到很多io操作的场景,因为在流量很高的时分或许会出现消费速度跟不上生产速度,这很容易导致full gc问题。所以假如有必要的话需求运用线程池来提高消费速度。

音讯行列和BufferTrigger的组合方法

有没有考虑过音讯行列的消费速度过慢,怎么在不影响下流功能的状况下提高消费速?比方直播点赞,在mq每次收到一条点赞音讯的时分是不是就能够运用BufferTrigger来进行聚合?然后每分钟消费一次,在流量剧增的时分是不是能十倍以上的提速?

散库散表

从结构层面我介绍了上面两种处理方案,其实运用这两种方案首要是为了缓解数据库压力,特别是单表的状况。可是高流量一般涉及到的都是用户维度的流量,所以假如必要的话能够根据userId来进行分库分表(我一般习惯就叫散表)并且优化表的规划,需求注意的是,最好不要分了表而不分库,因为多表共用一个资源功能提高还是不大。比方我们内部分表都是1000张,库都是至少10个库(集群)。这样处理一般的恳求上万的tps是彻底没问题的。

下面将从事务规划(技巧)的层面来介绍怎么处理写qps过高的问题。

恳求丢掉

在很多事务场景下,丢掉部分恳求彻底不影响事务,给个合理的提示的话用户底子无法感知,这其间最常见的便是秒杀、抢红包、发弹幕这类事务,恳求量很大,但只有少部分恳求能拿到钱,大部分恳求直接丢掉都是没问题的,到时分告知用户没抢到或许抢没了或许弹幕发生成功即可。

预处理

这类场景在抢红包的时分常常用到,或许很多人都知道很多app抢红包其实并不是在用户每次去抢的时分再去核算金额并落库的,一般是在红包发出去的时分就核算好了,比方1W块钱,100个人,那么就会随机生成100个数额推入到缓存的行列,用户抢的时分从行列直接pop即可。

当然,能够经过音讯行列异步落库,也便是同步写缓存,异步写db

又或许是在用户完结活动的时分得到一个资历,然后在指定的时刻点去瓜分1W块钱,其实也是在这时刻点之前就把能用的金额和获得资历的人拿出来,核算出每个人的金额再存储到缓存和db,最终用户虽然说是去抢,其实便是从缓存中把现已给他存好的钱拿出来了而已。

流量打散

这种方案或许用到的机会不是太多,组里有老人做过所以拿来借鉴。其实这种手法在活动特别新年活动的时分常见,特点是先互动后开奖(即在事务能承受必定的推迟状况)。快手的20年新年活动便是这样,先播一段新年明星拜年的视频,视频播映完之后弹出抢红包按钮。在这一会儿流量肯定是非常高的,假如在这时分再去算每个人的钱对服务功能要求就太高了,这是没必要去做的应战。因为在视频播映的时分我们就能够核算出来金额。

具体的做法是,服务端给客户端回来一个打散最大时刻,比方30S,那么客户端就生成随机生成一个(0,30)的时刻t,并在过now+t的时分去恳求服务端拿到用户的金额,用户点击抢红包的时分就能直接告知他这个金额。