聊聊项目中如何实现请求聚合

前言

什么是请求聚合

见名之意就是将多次的请求整合为一个请求处理

如何实现请求聚合

有个快手大佬开源了一个工具类:buffer-trigger,这玩意就可以用来做请求聚合。

buffer-trigger适用场景

  1. 高吞吐量消息处理: 当系统需要处理大量快速产生的数据或消息时,如日志记录、事件追踪、实时交易数据等,单条消息的即时处理可能会导致过多的系统开销(如网络通信、数据库操作等)。通过使用BufferTrigger,可以将这些消息暂时缓存在阻塞队列中,累积到一定数量后一次性进行批量处理。这样既能减少系统调用次数,提升整体处理效率,又能降低对下游系统的瞬时压力。

  2. 延迟敏感但允许适度延后处理: 在某些业务场景中,数据或消息的处理虽有一定的时效性要求,但并不严格到需要立即响应。例如,用户行为分析、运营统计报表生成等任务,可以在容忍的时间窗口内完成。BufferTrigger通过设置批处理阈值和延迟等待时间,允许在满足一定积累量或等待时间后才触发消费,从而实现数据的“准实时”处理,兼顾了处理效率与延迟需求。

  3. 资源优化与成本控制: 对于依赖付费服务(如云存储、API调用)或者计算资源有限的情况,批量处理能够显著减少对外部服务的调用量或内部计算资源的占用。例如,定期向云存储批量上传日志文件、批量发送电子邮件通知、批量查询外部API并聚合结果等。BufferTrigger通过合并多个小任务为一个大任务,有助于降低单位数据处理的成本。

  4. 避免频繁IO操作: 若消息的消费涉及大量的磁盘IO、网络IO或其他昂贵的系统资源操作,如数据库写入、文件写入、跨网络的数据同步等,频繁的单个操作可能导致性能瓶颈。BufferTrigger通过批量处理,能够减少这类操作的次数,从而提高系统整体性能。

  5. 微服务间解耦与流量控制: 在分布式微服务架构中,不同服务之间可能存在强依赖关系。使用BufferTrigger可以在服务间引入一层缓冲,避免下游服务瞬时过载或临时不可用导致整个系统崩溃。同时,批量处理能平滑消费端的请求流量,减轻对上游服务的压力,增强系统的稳定性和容错能力。

如何使用buffer-trigger

1、在项目的pom中引入buffer-trigger GAV

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

2、使用案例一:使用SimpleBufferTrigger

/**
 * {@link BufferTrigger}的通用实现,适合大多数业务场景
 * <p>
 * 消费触发策略会考虑消费回调函数的执行时间,实际执行间隔 = 理论执行间隔 - 消费回调函数执行时间;
 * 如回调函数执行时间已超过理论执行间隔,将立即执行下一次消费任务.
 *
 * @author w.vela
 */

示例:

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);
    }

参数描述

  • maxBuffeCount(long count):
    指定容器最大容量,比如这里指定了10,当在下次聚合前容器元素数量达到10就无法添加了,-1表示无限制;
  • internal(longinterval, TimeUnit unit) :表示多久聚合一次,如果没达到时间那么consumer是不会输出的,聚合后容器就空了。
  • setContainer(Supplier<? extends C> factory, BiPredicate<? super C, ? super E> queueAdder):
    第一个变量为factory,是个Supplier,获取容器用的,要求线程安全;第二个变量是缓存更新的方法BiPredicate<?
    super C, ? super E> queueAdder C为容器类型,E为元素类型
  • consumer(ThrowableConsumer<? super C, Throwable> consumer):
    表示如何消费聚合后的数据,标识我们如何去消费聚合后的数据,我这里就是简单打印。 enqueue(E element): 添加元素;
  • manuallyDoTrigger: 主动触发一次消费,通常在java进程关闭的时候调用

2、使用案例二:使用BatchConsumeBlockingQueueTrigger

/**
 * {@link BufferTrigger}基于阻塞队列的批量消费触发器实现.
 * <p>
 * 该触发器适合生产者-消费者场景,缓存容器基于{@link LinkedBlockingQueue}队列实现.
 * <p>
 * 触发策略类似Kafka linger,批处理阈值与延迟等待时间满足其一即触发消费回调.
 * @author w.vela
 */

示例:

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);
    }

  • batchBlocking():提供自带背压(back-pressure)的简单批量归并消费能力;
  • bufferSize(intbufferSize): 缓存队列的最大容量; batchSize(int size): 批处理元素的数量阈值,达到这个数量后也会进行消费
  • linger(Duration duration): 多久消费一次
  • setConsumerEx(ThrowableConsumer<?
    super List, Exception> consumer)
    : 消费函数,注入的对象为缓存队列中尚存的所有元素,非逐个元素消费;

3、两种实现方式在使用上的区别

BatchConsumeBlockingQueueTrigger每次将元素原封不动保存下来,然后一次性消费一整个列表元素。而SimpleBufferTrigger,每次添加元素都会进行计算。

以上示例摘抄该博文https://juejin.cn/post/7160569936576774181
这篇文章比较详细对请求聚合以及buffer-trigger进行了介绍

更多buffer-trigger内容可以看官方源码注释以及相应的单元测试案例
https://github.com/PhantomThief/buffer-trigger

以上就是buffer-trigger的使用教程,不过如果只是写到这边,就没啥意思了,下面就以一个实战的例子,来演示下如何实现请求聚合

案例

注: 以一个批量注册用户为例子,来演示请求聚合。案例将buffer-trigger与springboot做了一个整合。案例只列出核心代码,完整示例查看文末demo链接

1、项目中引入buffer-trigger GAV

 <dependency>
            <groupId>com.github.phantomthief</groupId>
            <artifactId>buffer-trigger</artifactId>
            <version>${buffer.trigger.version}</version>
        </dependency>

2、封装请求参数类

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class DataExchange<T,R> {

    private String bizNo;
    private T request;
    private CompletableFuture<Result<R>> response;
}

3、封装buffer-trigger处理类

@RequiredArgsConstructor
public class DelegateBatchConsumerTriggerHandler<T, R> implements BatchConsumerTriggerHandler<T, R>{


    private final BufferTrigger<DataExchange<T, R>> bufferTrigger;



    @SneakyThrows
    @Override
    public Result<R> handle(T request, String bizNo) {
        DataExchange dataExchange = new DataExchange<>();
        dataExchange.setBizNo(bizNo);
        dataExchange.setRequest(request);
        CompletableFuture<Result> response = new CompletableFuture<>();
        dataExchange.setResponse(response);
        bufferTrigger.enqueue(dataExchange);
        return response.get();
    }


    @Override
    public void closeBufferTrigger() {
        // 触发该事件,关闭BufferTrigger,并将未消费的数据消费
       if(bufferTrigger != null){
           bufferTrigger.close();
       }
    }
}

4、封装buffer-trigger创建工厂

public interface BatchConsumerTriggerFactory {

    default <T,R> BatchConsumerTriggerBuilder<DataExchange<T,R>> builder(){
        return null;
    }

    default <T,R> BufferTrigger<DataExchange<T,R>> getTrigger(ThrowableConsumer<List<DataExchange<T,R>>, Exception> consumer, String bufferTriggerBizType){
        if(!support(bufferTriggerBizType)){
           return null;
        }
       return builder().setConsumerEx(consumer).build();
    }

    boolean support(String bufferTriggerBizType);

    default <T,R> BatchConsumerTriggerHandler<T,R> getTriggerHandler(ThrowableConsumer<List<DataExchange<T,R>>, Exception> consumer, String bufferTriggerBizType){
        BufferTrigger<DataExchange<T, R>> trigger = getTrigger(consumer, bufferTriggerBizType);
        return new DelegateBatchConsumerTriggerHandler<>(trigger);
    }


}

5、模拟用户注册dao

@Repository
public class UserDao {
    private final Map<Long, User> userMap = new ConcurrentHashMap<>();
    private final ThreadLocalRandom random = ThreadLocalRandom.current();
    private final LongAdder idAdder = new LongAdder();

    public User register(UserDTO userDTO){
        mockExecuteCostTime();
        return getUser(userDTO);
    }


    public List<User> batchRegister(List<UserDTO> userDTOs){
        mockExecuteCostTime();
        List<User> users = new ArrayList<>();
        userDTOs.forEach(userDTO -> users.add(getUser(userDTO)));
        return users;
    }

6、模拟用户注册service

a、 常规方式

@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {

    private final UserDao userDao;
    private final LongAdder count = new LongAdder();
    @Override
    public Result<User> register(UserDTO user) {
        count.increment();
        System.out.println("执行次数:" + count.sum());

        return Result.success(userDao.register(user));
    }
    }

b、 请求聚合方式

前置条件: 需在yml指定相关队列、定时器配置以及业务类别

lybgeek:
  buffer:
    trigger:
      consume-queue-trigger-properties:
        - bufferTriggerBizType: userReisgeter
          config:
            batchSize: 100
            bufferSize: 1000
            batchConsumeIntervalMills: 1000

@Service
@RequiredArgsConstructor
public class UserServiceBufferTriggerImpl implements UserService, InitializingBean, DisposableBean {
    public static final String BUFFER_TRIGGER_BIZ_TYPE = "userReisgeter";

    private final UserDao userDao;

    private final BatchConsumerTriggerFactory batchConsumerTriggerFactory;

    private BatchConsumerTriggerHandler<UserDTO,User> batchConsumerTriggerHandler;


    private final LongAdder count = new LongAdder();

    @SneakyThrows
    @Override
    public Result<User> register(UserDTO user) {
       return batchConsumerTriggerHandler.handle(user,BUFFER_TRIGGER_BIZ_TYPE + "-" + UUID.randomUUID());
    }

 
    @Override
    public void afterPropertiesSet() throws Exception {
        // key为业务属性唯一键,如果不存在业务属性唯一键,则可以取bizNo作为key,示例以username作为唯一键
        Map<String, CompletableFuture<Result<User>>> completableFutureMap = new HashMap<>();
        batchConsumerTriggerHandler = batchConsumerTriggerFactory.getTriggerHandler((ThrowableConsumer<List<DataExchange<UserDTO, User>>, Exception>) dataExchanges -> {
            List<UserDTO> userDTOs = new ArrayList<>();
            for (DataExchange<UserDTO, User> dataExchange : dataExchanges) {
                UserDTO userDTO = dataExchange.getRequest();
                completableFutureMap.put(userDTO.getUsername(),dataExchange.getResponse());
                userDTOs.add(userDTO);
            }
            count.increment();
            System.out.println("执行次数:" + count.sum());
            List<User> users = userDao.batchRegister(userDTOs);
            if(CollectionUtil.isNotEmpty(users)){
                for (User user : users) {
                    CompletableFuture<Result<User>> completableFuture = completableFutureMap.remove(user.getUsername());
                    if(completableFuture != null){
                        completableFuture.complete(Result.success(user));
                    }
                }
            }

        },BUFFER_TRIGGER_BIZ_TYPE);


    }

    @Override
    public void destroy() throws Exception {
        // 触发该事件,关闭BufferTrigger,并将未消费的数据消费
        batchConsumerTriggerHandler.closeBufferTrigger();
    }
}

7、分别开启20个线程,对常规方式以及聚合方式的service进行测试

a、 常规方式

  @Test
    public void testRegisterUserByCommon() throws IOException {
      new ConcurrentCall(20).run(()->{
          UserDTO user = UserUtil.generateUser();
          return userServiceImpl.register(user);
      });
    }

控制台输出

执行次数:1
执行次数:2
执行次数:7
执行次数:6
执行次数:10
执行次数:9
执行次数:5
执行次数:4
执行次数:11
执行次数:12
执行次数:3
执行次数:8
执行次数:17
执行次数:16
执行次数:15
执行次数:18
执行次数:14
执行次数:20
执行次数:13
执行次数:19
Result(code=200, msg=success, data=User(id=1, username=yangweize, fullname=杨伟泽, age=12, email=yangweize@qq.com, mobile=64294835455))
Result(code=200, msg=success, data=User(id=3, username=yaojinpeng, fullname=姚晋鹏, age=13, email=yaojinpeng@qq.com, mobile=5381-03836251))
Result(code=200, msg=success, data=User(id=9, username=pengxiaoran, fullname=彭潇然, age=25, email=pengxiaoran@qq.com, mobile=903-85787160))
Result(code=200, msg=success, data=User(id=9, username=guoweize, fullname=郭伟泽, age=9, email=guoweize@qq.com, mobile=57105382845))
Result(code=200, msg=success, data=User(id=8, username=huangjinyu, fullname=黄瑾瑜, age=29, email=huangjinyu@qq.com, mobile=449-27085386))
Result(code=200, msg=success, data=User(id=6, username=renkairui, fullname=任楷瑞, age=3, email=renkairui@qq.com, mobile=2777-67842072))
Result(code=200, msg=success, data=User(id=2, username=fuhaoran, fullname=傅昊然, age=15, email=fuhaoran@qq.com, mobile=332-47390793))
Result(code=200, msg=success, data=User(id=5, username=linmingxuan, fullname=林明轩, age=27, email=linmingxuan@qq.com, mobile=116-31209336))
Result(code=200, msg=success, data=User(id=5, username=shensicong, fullname=沈思聪, age=6, email=shensicong@qq.com, mobile=0532-05033168))
Result(code=200, msg=success, data=User(id=11, username=gongtianyu, fullname=龚天宇, age=4, email=gongtianyu@qq.com, mobile=9752-26976731))
Result(code=200, msg=success, data=User(id=13, username=xiongminghui, fullname=熊明辉, age=23, email=xiongminghui@qq.com, mobile=0049-21709250))
Result(code=200, msg=success, data=User(id=17, username=huzhize, fullname=胡志泽, age=0, email=huzhize@qq.com, mobile=760-85426527))
Result(code=200, msg=success, data=User(id=16, username=gaosiyuan, fullname=高思源, age=5, email=gaosiyuan@qq.com, mobile=42452304656))
Result(code=200, msg=success, data=User(id=13, username=mojiaxi, fullname=莫嘉熙, age=2, email=mojiaxi@qq.com, mobile=7264-82263592))
Result(code=200, msg=success, data=User(id=18, username=caizimo, fullname=蔡子默, age=12, email=caizimo@qq.com, mobile=2653-82403850))
Result(code=200, msg=success, data=User(id=10, username=wancongjian, fullname=万聪健, age=10, email=wancongjian@qq.com, mobile=954-37654583))
Result(code=200, msg=success, data=User(id=14, username=gongyuebin, fullname=龚越彬, age=0, email=gongyuebin@qq.com, mobile=77884047173))
Result(code=200, msg=success, data=User(id=15, username=fenghongtao, fullname=冯鸿涛, age=2, email=fenghongtao@qq.com, mobile=8832-09658213))
Result(code=200, msg=success, data=User(id=19, username=jiangyuanbo, fullname=江苑博, age=12, email=jiangyuanbo@qq.com, mobile=2132-90700641))
Result(code=200, msg=success, data=User(id=20, username=xiaoxinlei, fullname=萧鑫磊, age=13, email=xiaoxinlei@qq.com, mobile=02196775183))

b、 聚合请求方式

  @Test
    public void testRegisterUserByBufferTrigger() throws IOException {
        new ConcurrentCall(20).run(()->{
            UserDTO user = UserUtil.generateUser();
            return userServiceBufferTriggerImpl.register(user);
        });
    }

控制台输出

执行次数:1
Result(code=200, msg=success, data=User(id=1, username=heguo, fullname=何果, age=10, email=heguo@qq.com, mobile=5725-06130005))
Result(code=200, msg=success, data=User(id=7, username=houwen, fullname=侯文, age=9, email=houwen@qq.com, mobile=85830365362))
Result(code=200, msg=success, data=User(id=11, username=yangxiaoyu, fullname=杨笑愚, age=5, email=yangxiaoyu@qq.com, mobile=13776594491))
Result(code=200, msg=success, data=User(id=3, username=yusimiao, fullname=余思淼, age=5, email=yusimiao@qq.com, mobile=070-18231344))
Result(code=200, msg=success, data=User(id=12, username=haotianyu, fullname=郝天宇, age=10, email=haotianyu@qq.com, mobile=42693432247))
Result(code=200, msg=success, data=User(id=14, username=wangxinpeng, fullname=汪鑫鹏, age=1, email=wangxinpeng@qq.com, mobile=59660609063))
Result(code=200, msg=success, data=User(id=15, username=tanzhichen, fullname=覃智宸, age=25, email=tanzhichen@qq.com, mobile=075-00624335))
Result(code=200, msg=success, data=User(id=4, username=lu:haoxuan, fullname=吕皓轩, age=14, email=lu:haoxuan@qq.com, mobile=9548-30583153))
Result(code=200, msg=success, data=User(id=2, username=qiuyinxiang, fullname=邱胤祥, age=18, email=qiuyinxiang@qq.com, mobile=04148786960))
Result(code=200, msg=success, data=User(id=5, username=weiweicheng, fullname=魏伟诚, age=25, email=weiweicheng@qq.com, mobile=0960-77489940))
Result(code=200, msg=success, data=User(id=20, username=tanbin, fullname=谭彬, age=27, email=tanbin@qq.com, mobile=297-57401738))
Result(code=200, msg=success, data=User(id=18, username=husiyuan, fullname=胡思远, age=24, email=husiyuan@qq.com, mobile=0809-08658163))
Result(code=200, msg=success, data=User(id=16, username=shishengrui, fullname=石晟睿, age=26, email=shishengrui@qq.com, mobile=8205-70004359))
Result(code=200, msg=success, data=User(id=17, username=lu:zihan, fullname=吕子涵, age=0, email=lu:zihan@qq.com, mobile=162-35081974))
Result(code=200, msg=success, data=User(id=19, username=xionghaoran, fullname=熊昊然, age=19, email=xionghaoran@qq.com, mobile=588-09693393))
Result(code=200, msg=success, data=User(id=13, username=jiangyuebin, fullname=姜越彬, age=19, email=jiangyuebin@qq.com, mobile=472-74492380))
Result(code=200, msg=success, data=User(id=8, username=haoweicheng, fullname=郝伟诚, age=26, email=haoweicheng@qq.com, mobile=73205366322))
Result(code=200, msg=success, data=User(id=10, username=tanhongxuan, fullname=谭鸿煊, age=18, email=tanhongxuan@qq.com, mobile=78536254981))
Result(code=200, msg=success, data=User(id=9, username=xielicheng, fullname=谢立诚, age=18, email=xielicheng@qq.com, mobile=4364-05053591))
Result(code=200, msg=success, data=User(id=6, username=weiluyang, fullname=韦鹭洋, age=28, email=weiluyang@qq.com, mobile=92876761170))

c、 结果分析
常规方式需要调用20次,将结果返回。聚合方式仅需调用一次,就将结果返回

总结

本文主要讲解如何进行请求聚合,请求聚合主要适用于那些需要高效、批量处理数据或消息,并且对处理延迟有一定容忍度的场景。

我们在使用请求聚合时,相关的下游最好能提供批量接口

其次BufferTrigger是单线程消费,在并发很高的场景下可能会出现消费速度跟不上生产速度,这很容易导致full gc问题。所以如果有必要的话需要使用线程池来提升消费速度

demo链接

https://github.com/lyb-geek/springboot-learning/tree/master/springboot-buffer-trigger

posted @ 2024-08-06 09:22  Linyb极客之路  阅读(81)  评论(0编辑  收藏  举报