ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

高并发下的同城配送系统:视频回传、午高峰削峰与动态定价

2026/9/2 3:17:30 拓冰建站 浏览量
高并发下的同城配送系统:视频回传、午高峰削峰与动态定价 午高峰两个小时订单量最集中本来是系统最紧张的时候但偏偏天气一变很多业务方会跑来问为什么恶劣天气下用户看到的配送单价反而不高同一时间产品又提了一个“新需求”配送车辆或骑手设备要支持超长视频回传全程录像一旦点开可能连续几小时不断流。这两个问题放在一起乍一看一个是价格策略一个是文件传输没什么关系。但真正做过同城配送或带LBS业务的后端系统会发现它们其实是同一套链路里的两个环节前端感知能力和后端定价调度能力共用同一套高并发基础设施。本文不讨论任何平台的具体定价是否合理也不评价极端天气下是否应该配送更不涉及劳动权益争议。这些属于运营策略和制度层面代码系统只是把规则执行出来。我们要做的是从后端架构视角拆解三件事超长视频回传为什么难午高峰两小时到底在考验什么恶劣天气下的动态定价系统是怎么算出一个“看起来比较低”的单价。读完你会得到一个通用方案视频分片上传、消息队列削峰、多因子定价引擎以及这三者如何串联成全链路闭环。1. 先给问题定个调三个关键词是同一个系统问题的三个侧面如果把“超长视频来袭午高峰两小时恶劣天气单价这么低”当成三个独立需求去接那大概率会做出三个互相割裂的模块上线后各跑各的遇到极端情况就会出问题。正确的做法是先把它还原成一个系统问题配送业务的实时感知与动态调度系统。超长视频代表的是“感知侧数据的海量化”。车辆记录仪或骑手手机连续录制一小时可能产生几GB数据这些数据要回传、存储、抽帧分析还要能在事后快速检索。恶劣天气下视频码率可能降低但录制时长不变网络反而更不稳定数据量和技术难度同时上升。午高峰两小时代表的是“并发洪峰”。订单集中在11点到13点之间涌入同时视频回传流量也达到峰值GPS轨迹上报频率翻倍天气数据源访问量激增。这时候系统面临的不只是某一个接口压力大而是所有链路同时被推高。恶劣天气单价代表的是“策略引擎的实时计算”。动态定价不是简单把价格乘一个固定系数而是要综合天气、时段、供需、距离、配送难度等多个因子在毫秒级时间内计算出来并推送到用户端和配送端。单价看起来低往往不是因为一个因子低而是多个因子叠加后的结果与用户预期不一致。这三件事共享的基础设施是稳定的传输通道、高吞吐的消息队列、低延迟的策略计算引擎和可回放的数据存储。所以本文的路线图是先讲超长视频回传的工程方案再讲午高峰如何削峰然后讲动态定价引擎的设计最后把它们串成全链路并给出可运行的最小示例。2. 超长视频回传为什么“视频长”在配送场景里是个硬伤2.1 配送场景的视频回传难点很多人以为上传大文件就是把文件丢给对象存储再拿一个URL回来。但在配送场景下“超长视频”四个字意味着完全不同的工程挑战。第一文件体积大且持续时间长。一个小时的1080p视频按常规码率算大约在1.5GB到3GB之间。配送高峰期如果要求全程录制一个配送员半天就能产生七八GB数据。到了服务端这就是持续不断写入的分钟级任务而不是偶尔一次的文件上传。第二网络环境极不稳定。配送员或车辆处于移动状态会频繁穿过地下车库、隧道、高楼密集区手机网络会在4G、5G之间切换甚至长时间回到弱网状态。普通的分片上传策略在弱网下基本不可用因为每次切换网络都会导致TCP连接重建若没有断点续传机制上传任务就会不断从头开始最终既消耗流量又无法完成。第三视频内容的合规性和安全性。超长视频包含路段、行人、车牌、人脸等大量敏感信息不可能直接原样上传到任意存储。在传输和存储链路里一般需要考虑对关键帧做脱敏处理至少要在存储层做权限隔离在展示层做局部模糊。这个需求会在后续章节的“最佳实践”里再展开。2.2 分片上传与断点续传的基本思路解决超长视频回传的通用方案是客户端将大文件按固定大小切成多个分片逐片上传服务端记录每个分片的上传状态全部上传完成后服务端触发合并任务。这个方案同时解决了三个问题弱网容错某个分片失败只需重传该分片不需要重传整个文件。并行提速多个分片可以并发上传充分利用带宽。服务端可校验分片合并前可以校验每个分片的MD5保证数据完整性。要支持断点续传客户端在上传前先调用接口查询已完成分片列表服务端从Redis或数据库中取出已上传分片编号客户端只上传缺失分片。2.3 一个最小可运行的分片上传控制器下面是一个基于Spring Boot风格的分片上传服务端示例核心是三个接口初始化上传、上传分片、合并分片。// 文件路径src/main/java/com/example/video/VideoUploadController.java RestController RequestMapping(/api/video) public class VideoUploadController { private static final String UPLOAD_TASK_KEY video:upload:; private static final String UPLOAD_CHUNK_KEY video:chunk:; Resource private StringRedisTemplate stringRedisTemplate; Resource private OSSClient ossClient; PostMapping(/init) public ResultString init(RequestBody InitUploadRequest request) { String uploadId UUID.randomUUID().toString(); UploadTask task new UploadTask(); task.setUploadId(uploadId); task.setFileName(request.getFileName()); task.setTotalChunks(request.getTotalChunks()); task.setChunkSize(request.getChunkSize()); stringRedisTemplate.opsForValue().set( UPLOAD_TASK_KEY uploadId, JSON.toJSONString(task), 24, TimeUnit.HOURS ); return Result.ok(uploadId); } PostMapping(/chunk) public ResultBoolean uploadChunk(RequestParam String uploadId, RequestParam Integer chunkIndex, RequestParam String md5, MultipartFile chunk) { String chunkKey UPLOAD_CHUNK_KEY uploadId : chunkIndex; // 如果该分片已经上传过且MD5一致直接返回成功支持断点续传 String exists stringRedisTemplate.opsForValue().get(chunkKey); if (exists ! null exists.equals(md5)) { return Result.ok(true); } String objectKey chunks/ uploadId / chunkIndex; ossClient.putObject(video-bucket, objectKey, chunk.getInputStream()); stringRedisTemplate.opsForValue().set(chunkKey, md5, 24, TimeUnit.HOURS); return Result.ok(true); } PostMapping(/merge) public ResultString merge(RequestParam String uploadId) { String taskJson stringRedisTemplate.opsForValue().get(UPLOAD_TASK_KEY uploadId); if (taskJson null) { return Result.fail(上传任务已过期请重新初始化); } UploadTask task JSON.parseObject(taskJson, UploadTask.class); // 校验分片完整性 for (int i 0; i task.getTotalChunks(); i) { String chunkKey UPLOAD_CHUNK_KEY uploadId : i; if (!stringRedisTemplate.hasKey(chunkKey)) { return Result.fail(分片缺失: i); } } // 按顺序合并分片这里以对象存储的multipart方式为例 ListPartETag partETags new ArrayList(); for (int i 0; i task.getTotalChunks(); i) { String objectKey chunks/ uploadId / i; partETags.add(ossClient.uploadPart(video-bucket, objectKey, i 1)); } String targetKey merged/ uploadId _ task.getFileName(); ossClient.completeMultipartUpload(video-bucket, targetKey, partETags); // 清理临时分片 stringRedisTemplate.delete(UPLOAD_TASK_KEY uploadId); for (int i 0; i task.getTotalChunks(); i) { stringRedisTemplate.delete(UPLOAD_CHUNK_KEY uploadId : i); } return Result.ok(targetKey); } }这段示例展示了三个关键逻辑一是用uploadId关联整个上传任务二是通过Redis保存分片MD5来实现断点续传三是合并前必须校验分片完整性。实际生产环境中分片上传往往要配合客户端做并发控制和重试退避服务端还要对同一uploadId的上传请求做限流防止恶意刷接口。3. 午高峰两小时订单洪峰与视频流量叠加时的削峰方案3.1 洪峰的真实形态午高峰两小时系统同时承受的压力来自多个方向新订单创建、支付回调、订单状态变更、实时定位上报、视频分片上传、天气因子查询。这些请求不是均匀分布的而是集中在11:30到12:30之间形成明显的陡峭峰值峰值QPS可能是平时均值的五到十倍。如果在架构上没有做削峰数据库连接池会先被打满接着Redis连接数超限最后依赖数据库和Redis的下游服务全部超时形成雪崩。很多团队第一次遇到午高峰问题时第一反应是扩容数据库但实际上扩容只能缓解存储层压力解决不了“大量写入同一个热点商家”或“同一区域订单集中”带来的热点问题。3.2 削峰填谷的通用手段削峰的核心原则是“把同步调用变成异步消息把瞬时高峰摊薄到更长的时间窗口”。具体来说有三个手段。第一是使用消息队列承接瞬时写入。订单创建成功后不立即去更新计数、计算积分、派发配送而是把后续动作封装成消息发到Kafka或RocketMQ由消费者异步处理。这样数据库承受的写入TPS可以稳定在一个可控水平。第二是分级限流和降级。对于非核心链路例如视频缩略图生成、订单同步到搜索系统、推送营销消息可以设置单独的限流阈值峰值时直接丢弃或延迟处理。对于核心链路例如支付回调、订单状态流转要保证较高的配额但也要设置熔断机制。第三是弹性扩容。Kubernetes环境下可以配置HPA根据消息队列堆积量或QPS指标自动扩容消费者实例。但扩容不是万能的如果下游数据库能力不足扩容消费者反而会增加数据库压力所以扩容必须配合限流和异步化一起做。3.3 用Kafka消费超长视频和订单事件下面的示例演示如何用Kafka消费者分别处理视频回传完成事件和订单事件将价格计算、视频抽帧等耗时操作异步化。// 文件路径src/main/java/com/example/consumer/OrderEventConsumer.java Component public class OrderEventConsumer { Resource private PriceService priceService; KafkaListener(topics order-created, groupId order-pricing) public void onOrderCreated(OrderCreatedMessage message) { // 异步计算价格避免在订单创建接口同步阻塞 priceService.calculateAndPersist(message.getOrderId()); } }// 文件路径src/main/java/com/example/consumer/VideoEventConsumer.java Component public class VideoEventConsumer { Resource private VideoAnalyzeService analyzeService; KafkaListener(topics video-merged, groupId video-analyze) public void onVideoMerged(VideoMergedMessage message) { // 视频合并完成后异步抽帧、做敏感信息脱敏、生成索引 analyzeService.analyze(message.getObjectKey()); } }消费者本身要做幂等设计因为Kafka在极端情况下可能重复投递消息。一个简单做法是在处理前检查Redis中是否存在已处理标记如果已处理直接返回。同时消费者要记录处理时间和失败原因方便通过监控大盘定位是消费能力不足还是下游依赖故障。4. 恶劣天气动态定价单价为什么看起来“低”4.1 动态定价不是“涨一倍”那么简单很多业务方理解动态定价以为就是天气越差价格越高简单做一个系数乘法就够了。但真实的定价引擎要综合非常多的因子而且每个因子都有自己的权重和计算方式。典型的多因子定价公式可以简化为最终价格 基础里程价 × 时段因子 × 天气因子 × 供需因子 × 配送难度因子基础里程价与距离强相关通常按直线距离或实际导航距离计算时段因子在午高峰和晚高峰会增加目的是一方面鼓励用户错峰下单另一方面提高峰值运力供给天气因子则根据实时天气数据和天气预报动态调整供需因子反映当前开放运力和待配送订单量的比值如果运力紧张因子上升否则回落配送难度因子考虑楼宇、电梯、停车因素有时也把恶劣天气导致的绕路距离计算进去。4.2 为什么“单价这么低”单价看起来低通常不是因为系统把价格算错了而是用户和系统对“价格”的理解存在偏差。第一用户感知的单价往往是“最终支付金额”扣除各类优惠券后的结果。平台为了在午高峰冲单量会发放大量满减券用户侧看到的价格被优惠稀释但配送侧的履约费用并不等同于用户支付金额。第二天气恶劣时配送时长显著拉长。虽然系统可能会上调天气因子但配送时长从20分钟变成40分钟单位时间收入实际是下降的。用户只看到一单的绝对价格没有看到系统基于“单位时间收益”模型计算出的调整结果。第三天气变化和定价引擎刷新之间存在时间窗口。天气数据源一般是每10到30分钟更新一次如果突然暴雨系统可能还在使用20分钟前的“小雨”因子。这个滞后会让用户觉得恶劣天气和价格变化对不上。第四订单取消率和改派率高。恶劣天气下用户取消订单或配送员无法完成配送的情况明显增加系统为了控制完单率会把部分预计超时区域的定价上调但如果该区域运力严重不足即使价格上调仍可能出现无人接单最终系统再次派单时又被加了“改派费用”这些费用在用户侧并不可见。4.3 一个可扩展的定价策略实现下面用策略模式实现一个简单的多因子定价引擎重点是展示“因子可编排、规则可配置”的思想。// 文件路径src/main/java/com/example/pricing/WeatherFactorStrategy.java Component public class WeatherFactorStrategy implements FactorStrategy { // 生产环境应从配置中心读取而不是硬编码 private static final MapString, BigDecimal WEATHER_FACTOR_MAP Map.of( sunny, new BigDecimal(1.00), cloudy, new BigDecimal(1.00), light_rain, new BigDecimal(1.15), heavy_rain, new BigDecimal(1.30), storm, new BigDecimal(1.50), snow, new BigDecimal(1.60) ); Override public BigDecimal calculate(OrderPriceContext context) { String weather context.getWeatherCode(); return WEATHER_FACTOR_MAP.getOrDefault(weather, BigDecimal.ONE); } Override public String factorName() { return weather; } }// 文件路径src/main/java/com/example/pricing/OrderPriceEngine.java Service public class OrderPriceEngine { private final ListFactorStrategy factorStrategies; public OrderPriceEngine(ListFactorStrategy factorStrategies) { this.factorStrategies factorStrategies; } public BigDecimal calculate(OrderPriceContext context) { BigDecimal price context.getBasePrice(); for (FactorStrategy strategy : factorStrategies) { BigDecimal factor strategy.calculate(context); price price.multiply(factor).setScale(2, RoundingMode.HALF_UP); } return price; } }这个实现的优点是新增一个定价因子时不需要改动主流程只需要新增一个实现FactorStrategy接口的类并注入Spring容器。对运营团队来说还可以把因子权重和天气映射表放到配置中心例如Apollo或Nacos实现不需要发版就能调整定价策略。生产环境中定价策略的每次调整都应该走灰度发布并通过监控数据观察完单率、取消率和用户投诉率的变化。5. 全链路闭环天气感知、超长视频与定价引擎如何联动5.1 一条完整的数据链路如果只把超长视频、午高峰、动态定价当作三个独立模块实际上并没有解决问题。到了真实业务里它们是互相依赖的。天气数据源将实时天气信息推送到消息队列或同步接口定价引擎订阅天气变化更新天气因子同时订单系统在创建订单时带上天气编码和实时路况信息定价引擎计算出最终价格配送员端开始履约后车载或手机端会录制并回传超长视频视频分片上传服务将视频保存到对象存储视频抽帧分析服务提取关键帧识别路面情况、拥堵程度、异常事件再将这些信息反馈给调度系统用于修正预计送达时间和后续订单的配送难度因子。这条链路的完整闭环可以简化成天气API、路况API、视频抽帧结果 ↓ 实时计算引擎Storm/Flink/Spark Streaming ↓ 定价因子更新 订单分配策略更新 ↓ 新订单接入 - 动态定价 - 调度履约 - 视频回传 - 事件反馈 - 因子再修正5.2 为什么实时计算是核心上述链路里实时计算引擎承担了“决策大脑”的角色。天气因子、供需因子、配送难度因子都必须在一定时效内更新如果延迟超过五分钟系统就会基于过期数据做决策。实现层面可以选择Flink或Spark Structured Streaming。天气数据流和订单数据流分别接入通过窗口聚合计算出当前区域的供需比再结合天气编码生成定价因子。实时计算结果写入Redis或内存网格供订单API查询。对中小团队来说不一定需要一开始就引入Flink。可以先做一个简单的轮询任务每10秒扫描一次天气数据源和运力数据更新到Redis定价接口直接读Redis。只有当数据量和实时性要求进一步上升时再迁移到流式计算框架。6. 环境准备与前置条件6.1 软件环境本文示例代码以Java和Spring Boot为主同时会用到Redis、Kafka、对象存储。以下是演示环境的最低要求版本请以实际项目为准本文不绑定某个具体版本。JDK 8或更高版本Maven 3.6或更高版本Redis 5.0及以上Kafka 2.x或3.xMySQL 5.7及以上如果涉及订单表存储对象存储MinIO或云厂商OSS如果你只想本地跑通分片上传示例暂不启动Kafka也是可以的因为分片上传逻辑只依赖Redis和对象存储。但完整演示动态定价和削峰就需要把Kafka启动起来。6.2 基础配置示例# 文件路径src/main/resources/application.yml server: port: 8080 spring: application: name: delivery-system redis: host: 127.0.0.1 port: 6379 kafka: bootstrap-servers: 127.0.0.1:9092 consumer: group-id: delivery-group auto-offset-reset: earliest enable-auto-commit: false oss: endpoint: http://127.0.0.1:9000 access-key: minioadmin secret-key: minioadmin bucket: video-bucket需要特别注意Kafka消费者要设置enable-auto-commit为false手动提交offset并且处理完业务后再提交。这样如果消费者在业务处理中崩溃消息不会被确认重新消费时还能重新处理避免丢消息。7. 完整示例代码实现7.1 项目依赖以下pom.xml片段包含主要依赖。!-- 文件路径pom.xml -- dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.springframework.kafka/groupId artifactIdspring-kafka/artifactId /dependency dependency groupIdio.minio/groupId artifactIdminio/artifactId version8.5.7/version /dependency dependency groupIdcom.alibaba/groupId artifactIdfastjson/artifactId version2.0.32/version /dependency /dependencies这里选择的版本号只是演示用实际引入时建议通过Spring Boot BOM管理版本避免不同依赖之间的兼容性问题。7.2 分片上传三个接口前面的章节已经给出了UploadController核心代码。在此基础上还需要定义UploadTask和InitUploadRequest两个类。// 文件路径src/main/java/com/example/video/UploadTask.java public class UploadTask { private String uploadId; private String fileName; private Integer totalChunks; private Long chunkSize; // getter/setter 省略 }// 文件路径src/main/java/com/example/video/InitUploadRequest.java public class InitUploadRequest { private String fileName; private Integer totalChunks; private Long chunkSize; // getter/setter 省略 }为了演示方便这里使用了HashMap存储分片信息实际生产环境建议直接使用对象存储的分片上传功能比如MinIO的ComposeObject或云厂商的multipart接口避免把分片存储到业务数据库。7.3 动态定价引擎在4.3节已经给出WeatherFactorStrategy和OrderPriceEngine。为了让定价引擎更完整再补充一个供需因子策略。// 文件路径src/main/java/com/example/pricing/SupplyDemandFactorStrategy.java Component public class SupplyDemandFactorStrategy implements FactorStrategy { Resource private RedisTemplateString, String redisTemplate; private static final String SUPPLY_KEY supply:ratio:; Override public BigDecimal calculate(OrderPriceContext context) { String key SUPPLY_KEY context.getRegionId(); String ratio redisTemplate.opsForValue().get(key); if (ratio null) { return BigDecimal.ONE; } // ratio表示当前区域订单量与可用运力的比值越高说明运力越紧张 return new BigDecimal(ratio).min(new BigDecimal(1.8)); } Override public String factorName() { return supply_demand; } }这里把供需比限制在1.8以内是为了防止某区域突然出现极端比值后价格暴涨导致用户投诉。生产环境中这一个阈值应该放到配置中心同时设置报警。7.4 Kafka削峰消费前面已展示两个消费者。为了减少重复代码可以把公共的幂等逻辑抽取到一个抽象类中。// 文件路径src/main/java/com/example/consumer/AbstractIdempotentConsumer.java public abstract class AbstractIdempotentConsumerT { Resource private RedisTemplateString, String redisTemplate; private static final String PROCESSED_KEY processed:msg:; public void handle(T message, String messageId, ConsumerHandlerT handler) { Boolean success redisTemplate.opsForValue() .setIfAbsent(PROCESSED_KEY messageId, 1, 24, TimeUnit.HOURS); if (Boolean.FALSE.equals(success)) { log.info(duplicated message, id{}, messageId); return; } try { handler.handle(message); } catch (Exception e) { redisTemplate.delete(PROCESSED_KEY messageId); throw e; } } FunctionalInterface public interface ConsumerHandlerT { void handle(T message); } }使用幂等消费者后即使Kafka因为网络抖动重复投递消息也不会重复处理。这个设计在视频回调、订单回调、支付回调等场景里是标配不需要等出现问题后再补。8. 运行结果与效果验证8.1 启动服务假设你已经启动Redis、Kafka和MinIO先启动Spring Boot应用。mvn spring-boot:run启动成功后日志里会显示Tomcat started on port 8080同时Kafka消费者启动的日志也会出现。8.2 测试分片上传接口第一步初始化上传任务。curl -X POST http://localhost:8080/api/video/init \ -H Content-Type: application/json \ -d {fileName:rider_20250112.mp4,totalChunks:4,chunkSize:5242880}预期返回{ code: 0, data: a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d }第二步上传分片。curl -X POST http://localhost:8080/api/video/chunk \ -F uploadIda1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d \ -F chunkIndex0 \ -F md5xxxx \ -F chunk/tmp/part0重复上传同一分片如果MD5一致接口会直接返回成功这就是断点续传的核心表现。第三步合并分片。curl -X POST http://localhost:8080/api/video/merge?uploadIda1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d预期返回合并后的对象存储key。如果某个分片没有上传接口会返回“分片缺失”的提示。8.3 验证动态定价测试定价引擎时由于没有真实天气数据源可以直接在入口写一个测试接口传入不同天气编码观察价格变化。假设基础价格为10元晴天价格为10元暴雨天气价格应为13元暴雪天气价格应为16元。如果实际输出与预期不符优先检查Redis中是否有供应比因子缓存。某些极端情况下供应比因子会放大到1.8导致暴雨天气价格不是预期的13元而是更高。这时不要立即认为是代码bug先确认当前区域的供需数据是否正常。9. 常见问题与排查方法问题现象可能原因排查方式解决方案分片合并后视频无法播放分片上传顺序错乱或分片缺失检查Redis中的分片MD5记录对比合并日志合并前增加分片数量校验合并时使用排序后的分片列表弱网下上传反复失败客户端超时时间太短重试策略太激进查看服务端请求日志观察失败分片编号设置指数退避重试超时时间建议从10秒起步天气定价没有生效定价引擎读不到最新天气数据或天气编码不匹配检查天气数据源消费日志确认Redis中天气因子是否更新增加天气数据源监控天气因子统一从配置中心获取午高峰Kafka消息积压消费者消费能力不足或下游数据库写入慢查看消费者组消费延迟和数据库慢SQL日志扩容消费者实例异步化非核心逻辑优化慢SQL定价结果与运营配置不一致策略引擎缓存了旧规则或因子执行顺序不对检查配置中心版本和策略执行日志发布新策略时清理规则缓存增加版本号视频存储成本增长快超长视频全部持久化没有冷热分层查看对象存储桶大小和文件生命周期配置配置生命周期规则超过30天的视频转为冷存储或删除消息重复消费导致重复入库消费者没有做幂等处理查看消费者日志观察相同消息ID是否处理多次集成幂等消费者以业务唯一ID为准做去重这些问题是真实项目中比较高频的故障点。在设计和编码阶段就考虑幂等、超时、重试和监控可以明显减少上线后的线上事故。10. 最佳实践与工程建议10.1 分片上传的设计要点视频分片大小建议在2MB到8MB之间太小的分片会产生大量HTTP请求消耗服务端连接资源太大的分片在弱网下失败率会明显增加。一般做法是客户端根据当前网络测速动态调整分片大小弱网时适当降低分片尺寸。服务端一定要对同一uploadId的并发上传做防护。恶意客户端可能在短时间内调用上千次chunk接口把Redis资源耗尽。可以在初始化上传时生成一个短期token后续分片请求必须携带token同时限制同一uploadId的并发上传数。10.2 动态定价的灰度与监控定价策略影响面非常大一旦规则配置错误会导致价格异常、用户投诉和配送员拒单。最佳实践是每次修改定价因子时都走灰度发布例如只对某个城市或某个商圈的1%流量生效观察完单率、取消率、投诉率后再逐步放量。监控指标建议包括平均每单价格、单位时间收益、订单取消率、超时未接单率、用户投诉率。这些指标要按城市、时段、天气三个维度拆分方便快速定位问题。10.3 削峰场景下的限流降级午高峰场景下即使已经用消息队列削峰也要对入口接口做限流。推荐使用Guava RateLimiter或分布式限流组件Sentinel以订单创建接口为例可以设置单机QPS上限为200超过限流阈值的请求返回“系统繁忙”由客户端做重试或降级提示。降级策略要明确哪些链路可以降级。视频回传是非实时性任务可以降级GPS轨迹上报可以延迟处理订单状态流转是核心链路不能降级。降级不是每家公司都能做到但至少要在代码中提前预留开关否则线上发生故障时只能靠重启或临时改代码来恢复这是非常危险的。10.4 合规与安全超长视频涉及个人隐私一定要在存储和访问环节做权限控制。建议视频默认不公开访问只能通过后端鉴权后生成临时URL。关键帧和视频缩略图在存储时要做脱敏处理对路人脸部和车牌区域进行局部模糊。另外天气数据和路况数据属于第三方数据源调用前要确认授权范围不能把天气API的返回结果直接缓存到公共桶中。生产环境中的密钥、accessKey和数据库账号必须放到配置中心或密钥管理服务不能出现在代码仓库中。11. 总结与下一步实践回到开头的三个问题超长视频、午高峰两小时、恶劣天气单价低其实都是同一条配送调度链路上的技术挑战。超长视频回传通过分片上传和断点续传解决了弱网传输问题午高峰通过消息队列削峰和幂等消费解决了瞬时爆发问题恶劣天气单价则是多因子定价引擎的输出结果单价看起来低本质上是因为定价因子叠加、配送时长拉长、用户侧优惠和天气数据延迟共同导致的感知偏差。如果你是后端开发者建议先做一个最小闭环分片上传一个1GB视频观察断点续传和合并效果再写一个定价引擎接入模拟天气因子观察不同天气编码下的价格变化最后用Kafka把订单事件和视频事件串起来验证同一套消息队列是否能承载两类流量。三个小实验做完你对整条链路的理解会比只看架构文章深得多。下一步值得继续深入的方向包括Flink实时计算引擎实现天气因子和供需因子的秒级更新、对象存储的生命周期管理、视频AI抽帧与敏感信息脱敏、以及基于全链路追踪的监控告警系统。记住一点这类系统出问题往往不在单个模块内部而在模块之间的数据衔接和容错设计上所以写代码时要时刻问自己一句话如果依赖服务超时了我的系统会怎样午高峰两个小时订单量最集中系统压力拉满这时候产品那边突然提了个需求配送设备要支持超长视频回传全程录像一旦开启可能连续几小时不断流。紧接着运营也来问恶劣天气下配送单价为什么看起来那么低能不能通过技术手段把价格波动解释清楚如果是一个只负责单一模块的后端很容易把这当成三个独立问题去处理视频回传交给文件上传组定价交给策略组高峰期扩容交给运维组。但实际上它们共享同一套基础设施互相之间还有数据依赖。本文不从运营角度评判价格高低也不讨论极端天气该不该配送而是从后端工程师的视角拆解一套通用架构超长视频如何高效回传、午高峰洪峰如何削峰、动态定价引擎如何计算天气因子以及三者怎么串成完整链路。如果你是做同城配送、即时零售、网约车或任何带LBS和实时视频业务的系统设计者这篇文章可以给你一个可落地的参考。读完后你能理解超长视频回传的分片与断点续传方案、午高峰场景下的异步削峰设计、多因子动态定价引擎的原理并且能照着本文的代码跑通一个最小演示。1. 先把问题拆开三个关键词是同一个系统问题的三个侧面先说一个比较反直觉的判断超长视频、午高峰、恶劣天气单价表面上是三个需求本质上是同一个问题——实时感知与动态调度系统在高并发场景下的稳定性。超长视频对应的是感知侧的数据回传。配送车辆或骑手设备持续录制视频文件体积大、持续时间长、网络环境差这本身就是一类高难度的文件传输问题。恶劣天气下网络信号可能更差视频录制却不能停于是回传压力和数据可靠性压力同时放大。午高峰两小时对应的是流量洪峰。订单集中在11点到13点之间涌入视频回传流量也在这个时段达到顶点GPS轨迹上报频率翻倍天气数据源访问量同步上升。这个时段系统面临的不只是单个接口被打满而是所有链路一起过载。恶劣天气单价对应的是策略引擎的实时计算。动态定价不是简单设一个天气系数而是要综合天气、时段、供需、距离、配送难度等多个因子在毫秒级时间内算出价格。用户感知到的“低单价”往往是多个因子叠加后与预期不一致的结果。这三件事共享的能力是稳定的传输通道、高吞吐的消息队列、低延迟的计算引擎、可回放的数据存储。所以下面会按照“视频回传-洪峰削峰-动态定价-全链路串联”的顺序展开最后给出可运行的代码示例和排错清单。2. 超长视频回传配送场景下的文件传输为什么难2.1 难点不只是“文件大”很多人觉得上传大文件就是把文件丢给对象存储再拿一个URL回来。但配送场景下的超长视频有三个特点决定了它不能按普通文件上传方案做。第一录制时间极长。一个小时的1080p视频按常规码率算大约1到2GB如果要求配送全程录制一个设备半天就能产生五六GB甚至更多数据。服务端要处理的是持续不断的分钟级写入而不是偶尔一次的大文件上传。第二网络环境极不稳定。车辆或骑手处于移动状态会频繁穿过地下车库、隧道、高楼密集区网络在4G、5G之间切换甚至长时间处于弱网状态。普通分片上传如果缺少断点续传网络切换后传输任务会从头开始流量消耗和失败概率都不可接受。第三合规风险。超长视频包含路段、行人、车牌、人脸等大量敏感信息不能原样直接上传到公开存储。至少要在存储层做权限隔离在展示层做关键信息脱敏。这个问题在最后的最佳实践章节会再展开。2.2 分片上传与断点续传的核心思想配送场景通用的视频回传方案是客户端将大文件按固定大小切成多个分片逐片上传服务端记录每个分片的上传状态全部上传完成后服务端触发合并任务。这个方案同时解决三个问题弱网容错某个分片失败只需重传该分片不需要重传整个文件。并发提速多个分片可以并发上传充分利用带宽。服务端可校验合并前校验每个分片的MD5或大小保证数据完整。断点续传的实现方式是客户端在上传前先调用接口查询已完成分片列表服务端从Redis或数据库中取出已上传分片编号客户端只上传缺失分片。这样就算网络断开多次最终也能把文件凑齐。2.3 分片上传服务端示例下面是一个基于Spring Boot风格的分片上传服务端代码核心是三个接口初始化上传、上传分片、合并分片。// 文件路径src/main/java/com/example/video/VideoUploadController.java RestController RequestMapping(/api/video) public class VideoUploadController { private static final String UPLOAD_TASK_KEY video:upload:; private static final String UPLOAD_CHUNK_KEY video:chunk:; Resource private StringRedisTemplate stringRedisTemplate; Resource private OSSClient ossClient; PostMapping(/init) public ResultString init(RequestBody InitUploadRequest request) { String uploadId UUID.randomUUID().toString(); UploadTask task new UploadTask(); task.setUploadId(uploadId); task.setFileName(request.getFileName()); task.setTotalChunks(request.getTotalChunks()); task.setChunkSize(request.getChunkSize()); stringRedisTemplate.opsForValue().set( UPLOAD_TASK_KEY uploadId, JSON.toJSONString(task), 24, TimeUnit.HOURS ); return Result.ok(uploadId); } PostMapping(/chunk) public ResultBoolean uploadChunk(RequestParam String uploadId, RequestParam Integer chunkIndex, RequestParam String md5, MultipartFile chunk) { String chunkKey UPLOAD_CHUNK_KEY uploadId : chunkIndex; // 如果该分片已经上传过且MD5一致直接返回成功实现断点续传 String exists stringRedisTemplate.opsForValue().get(chunkKey); if (exists ! null exists.equals(md5)) { return Result.ok(true); } String objectKey chunks/ uploadId / chunkIndex; ossClient.putObject(video-bucket, objectKey, chunk.getInputStream()); stringRedisTemplate.opsForValue().set(chunkKey, md5, 24, TimeUnit.HOURS); return Result.ok(true); } PostMapping(/merge) public ResultString merge(RequestParam String uploadId) { String taskJson stringRedisTemplate.opsForValue().get(UPLOAD_TASK_KEY uploadId); if (taskJson null) { return Result.fail(上传任务已过期请重新初始化); } UploadTask task JSON.parseObject(taskJson, UploadTask.class); // 合并前必须校验分片完整性 for (int i 0; i task.getTotalChunks(); i) { String chunkKey UPLOAD_CHUNK_KEY uploadId : i; if (!stringRedisTemplate.hasKey(chunkKey)) { return Result.fail(分片缺失: i); } } // 按顺序合并分片这里以对象存储的multipart方式为例 ListPartETag partETags new ArrayList(); for (int i 0; i task.getTotalChunks(); i) { String objectKey chunks/ uploadId / i; partETags.add(ossClient.uploadPart(video-bucket, objectKey, i 1)); } String targetKey merged/ uploadId _ task.getFileName(); ossClient.completeMultipartUpload(video-bucket, targetKey, partETags); // 清理临时分片 stringRedisTemplate.delete(UPLOAD_TASK_KEY uploadId); for (int i 0; i task.getTotalChunks(); i) { stringRedisTemplate.delete(UPLOAD_CHUNK_KEY uploadId : i); } return Result.ok(targetKey); } }这段代码展示了三个关键逻辑用uploadId关联整个上传任务通过Redis保存分片MD5实现断点续传合并前校验分片完整性。实际生产环境中还要配合客户端做并发控制和重试退避并在服务端对同一uploadId的请求做限流防止恶意刷接口。3. 午高峰两小时订单洪峰与视频流量的削峰方案3.1 洪峰不是均匀的流量午高峰两小时系统同时承受多路压力新订单创建、支付回调、订单状态变更、实时定位上报、视频分片上传、天气因子查询。这些请求在11:30到12:30之间形成陡峭峰值峰值QPS可能是平峰的五到十倍。如果架构上没有做削峰最先被打满的是数据库连接池接着是Redis连接数超限最后所有依赖数据库和Redis的下游服务都会超时形成雪崩。很多人第一反应是扩容数据库但扩容只能缓解存储层压力解决不了“大量写入同一个热点商家”或“同一区域订单集中”带来的热点问题。3.2 削峰填谷的通用手段削峰的核心