掌握物联网平台 Thingsboard http高性能接口的核心思想

前言

我接触物联网项目已经有5年时间,对物联网平台Thingsboard进行过一些改造经验总结。这里聊聊springboot高性能的 HTTP接口实现的思路。

首先给大家展示下高并发http接口的代码

整个代码示例,就是springboot http接口请求,然后查询数据库,返回查询的结果给客户端。

controller 代码示例:

@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
    @RequestMapping(value = "/{entityType}/{entityId}/values/timeseries", method = RequestMethod.GET, params = {"keys", "startTs", "endTs"})
    @ResponseBody
public DeferredResult<ResponseEntity> getTimeseries(
            @ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, defaultValue = "DEVICE") @PathVariable("entityType") String entityType,
            @ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
            @ApiParam(value = TELEMETRY_KEYS_BASE_DESCRIPTION, required = true) @RequestParam(name = "keys") String keys,
            @ApiParam(value = "A long value representing the start timestamp of the time range in milliseconds, UTC.")
            @RequestParam(name = "startTs") Long startTs,
            @ApiParam(value = "A long value representing the end timestamp of the time range in milliseconds, UTC.")
            @RequestParam(name = "endTs") Long endTs,
            @ApiParam(value = "A long value representing the aggregation interval range in milliseconds.")
            @RequestParam(name = "interval", defaultValue = "0") Long interval,
            @ApiParam(value = "An integer value that represents a max number of timeseries data points to fetch." +
                    " This parameter is used only in the case if 'agg' parameter is set to 'NONE'.", defaultValue = "100")
            @RequestParam(name = "limit", defaultValue = "100") Integer limit,
            @ApiParam(value = "A string value representing the aggregation function. " +
                    "If the interval is not specified, 'agg' parameter will use 'NONE' value.",
                    allowableValues = "MIN, MAX, AVG, SUM, COUNT, NONE")
            @RequestParam(name = "agg", defaultValue = "NONE") String aggStr,
            @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
            @RequestParam(name = "orderBy", defaultValue = "DESC") String orderBy,
            @ApiParam(value = STRICT_DATA_TYPES_DESCRIPTION)
            @RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes) throws ThingsboardException {
        return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_TELEMETRY, entityType, entityIdStr,
                (result, tenantId, entityId) -> {
                    // If interval is 0, convert this to a NONE aggregation, which is probably what the user really wanted
                    Aggregation agg = interval == 0L ? Aggregation.valueOf(Aggregation.NONE.name()) : Aggregation.valueOf(aggStr);
                    List<ReadTsKvQuery> queries = toKeysList(keys).stream().map(key -> new BaseReadTsKvQuery(key, startTs, endTs, interval, limit, agg, orderBy))
                            .collect(Collectors.toList());

                    Futures.addCallback(tsService.findAll(tenantId, entityId, queries), getTsKvListCallback(result, useStrictDataTypes), MoreExecutors.directExecutor());
                });
}

代码说明accessValidator.validateEntityAndCallback 这个验证 + 回调,然后在回调里面调用tsService.findAll,并且又采用 了 Futures.addCallback(tsService.findAll())再次异步调用service的代码逻辑。

service 的findAll()方法

@Override
public ListenableFuture<List<TsKvEntry>> findAll(TenantId tenantId, EntityId entityId, List<ReadTsKvQuery> queries) {
        return Futures.transform(findAllByQueries(tenantId, entityId, queries),
                result -> {
                    if (result != null && !result.isEmpty()) {
                        return result.stream().map(ReadTsKvQueryResult::getData).flatMap(Collection::stream).collect(Collectors.toList());
                    }
                    return Collections.emptyList();
                }, MoreExecutors.directExecutor());
}

代码说明Futures.transform(findAllByQueries()) 这里再是异步调用 findAllByQueries()方法

findAllByQueries() 方法

@Override
 public ListenableFuture<List<ReadTsKvQueryResult>> findAllByQueries(TenantId tenantId, EntityId entityId, List<ReadTsKvQuery> queries) {
        validate(entityId);
        queries.forEach(this::validate);
        if (entityId.getEntityType().equals(EntityType.ENTITY_VIEW)) {
            EntityView entityView = entityViewService.findEntityViewById(tenantId, (EntityViewId) entityId);
            List<String> keys = entityView.getKeys() != null && entityView.getKeys().getTimeseries() != null ?
                    entityView.getKeys().getTimeseries() : Collections.emptyList();
            List<ReadTsKvQuery> filteredQueries =
                    queries.stream()
                            .filter(query -> keys.isEmpty() || keys.contains(query.getKey()))
                            .collect(Collectors.toList());
            return timeseriesDao.findAllAsync(tenantId, entityView.getEntityId(), updateQueriesForEntityView(entityView, filteredQueries));
        }
        return timeseriesDao.findAllAsync(tenantId, entityId, queries);
}

代码说明:这里调用了 timeseriesDao.findAllAsync()方法,从方法名称可以看出,这也是异步的调用。

findAllAsync() 方法

@Override
 public ListenableFuture<List<ReadTsKvQueryResult>> findAllAsync(TenantId tenantId, EntityId entityId, List<ReadTsKvQuery> queries) {
        List<ListenableFuture<ReadTsKvQueryResult>> futures = queries.stream()
                .map(query -> findAllAsync(tenantId, entityId, query)).collect(Collectors.toList());
        return Futures.allAsList(futures);
}

@Override
public ListenableFuture<ReadTsKvQueryResult> findAllAsync(TenantId tenantId, EntityId entityId, ReadTsKvQuery query) {
        if (query.getAggregation() == Aggregation.NONE) {
            return findAllAsyncWithLimit(tenantId, entityId, query);
        } else {
            long startPeriod = query.getStartTs();
            long endPeriod = Math.max(query.getStartTs() + 1, query.getEndTs());
            long step = Math.max(query.getInterval(), MIN_AGGREGATION_STEP_MS);
            List<ListenableFuture<Optional<TsKvEntryAggWrapper>>> futures = new ArrayList<>();
            while (startPeriod < endPeriod) {
                long startTs = startPeriod;
                long endTs = Math.min(startPeriod + step, endPeriod);
                long ts = endTs - startTs;
                ReadTsKvQuery subQuery = new BaseReadTsKvQuery(query.getKey(), startTs, endTs, ts, 1, query.getAggregation(), query.getOrder());
                futures.add(findAndAggregateAsync(tenantId, entityId, subQuery, toPartitionTs(startTs), toPartitionTs(endTs)));
                startPeriod = endTs;
            }
            ListenableFuture<List<Optional<TsKvEntryAggWrapper>>> future = Futures.allAsList(futures);
            return Futures.transform(future, new Function<>() {
                @Nullable
                @Override
                public ReadTsKvQueryResult apply(@Nullable List<Optional<TsKvEntryAggWrapper>> input) {
                    if (input == null) {
                        return new ReadTsKvQueryResult(query.getId(), Collections.emptyList(), query.getStartTs());
                    } else {
                        long maxTs = query.getStartTs();
                        List<TsKvEntry> data = new ArrayList<>();
                        for (var opt : input) {
                            if (opt.isPresent()) {
                                TsKvEntryAggWrapper tsKvEntryAggWrapper = opt.get();
                                maxTs = Math.max(maxTs, tsKvEntryAggWrapper.getLastEntryTs());
                                data.add(tsKvEntryAggWrapper.getEntry());
                            }
                        }
                        return new ReadTsKvQueryResult(query.getId(), data, maxTs);
                    }

                }
            }, readResultsProcessingExecutor);
        }
}

代码说明:这里是直接查询数据库的逻辑,所有方法都采用的异步调用,交给线程次去执行,返回ListenableFuture

高并发接口设计的核心总结

思考

小伙伴肯定第一反应是,不就是查询下数据库吗,搞这么复杂有必要吗?为什么就一个简单的查询数据库,需要增加这么多的异步执行 controller 调用service,service调用dao 都是异步执行,然后依赖回调,所以代码看着很复杂,为什么要这样实现呢?为什么不直接一步到位,直接调用到dao执行查询,直接返回执行结果。

总结

单线程方式

通常我们的http请求,发起一次调用,如果按照直接调用到数据库返回,不采取任何的异步的情况下,假设这个过程需要执行 10秒。所以一个请求就需要占用服务器线程10秒,假设我们的服务器同时支持 5000个线程,那么 5000个请求,要等待 10秒过后才能接收新的请求。所以这个10s内我们的服务器的并发就是 5000,每秒的并发就是5000/10 = 500个请求/s

全部异步方式

全部异步方式,假设我们整个过程还是10s,拆分成了三步,controller 执行需要3s,service 需要3s,dao需要 4s,假设我们线程池也是5000,这个时候,并发数最直接的就是controller能接收的最大并发数量。

  1. 优势一

也就是3s内 5000个并发,由于时间缩短了controller的线程释放更快,所以他并发接收请求的能力就提升了。

  1. 优势二

因为一次请求被拆分成了三个任务,任务变得小了,线程等待的时间会大大降低,所以前面我们的理论上10s完成一次完整的业务请求响应,通常时间会缩短到 5-6秒完成。

  1. 优势三

因为我们优势一的吞吐量 起来了,优势二的效率也提高了,我们的并发量也就提高了,粗略估算下 5000/3 = 1600个请求/s。

项目源码参考

这个例子,核心在于分享下高并发编程的一些思路,我这里附上我曾经写的GRPC server 源码,里面就采用了大量的这种异步回调思路,增加了我grpc服务器的吞吐量和效率,以及服务的可靠性。

grpc server 源码高并发核心实现

  • 26
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

gzcsschen

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值