去年双十一期间,我们团队负责的淘客APP在流量高峰期出现了严重的系统崩溃。当时每秒订单量突破5万,数据库CPU直接飙到100%,整个交易链路瘫痪了近20分钟。这次事故让我深刻意识到:在电商促销场景下,技术选型直接决定了系统的生死存亡。
淘客类应用的技术架构有三个致命痛点:
经过半年重构,我们最终基于Java生态搭建了新一代系统。本文将重点分享缓存、消息队列和存储三大核心组件的选型对比,包含实际压测数据和踩坑经验。
淘客APP的缓存主要服务于三类场景:
我们最终选择Redis 6.2作为主缓存,关键配置如下:
yaml复制spring:
redis:
lettuce:
pool:
max-active: 200
max-idle: 50
min-idle: 10
timeout: 300ms
cluster:
nodes: 10.0.0.1:6379,10.0.0.2:6379
性能优化点:
踩坑记录:曾经因为没设置合理的连接超时时间,导致线程池被打满。建议timeout不要超过500ms
在高并发场景下,我们通过Caffeine构建了二级缓存:
java复制LoadingCache<String, Product> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.refreshAfterWrite(1, TimeUnit.MINUTES)
.build(key -> productDao.get(key));
选型对比表:
| 维度 | Redis | Caffeine |
|---|---|---|
| 吞吐量 | 8W QPS | 120W QPS |
| 延迟 | 1-5ms | <1ms |
| 数据一致性 | 强一致 | 最终一致 |
| 适用场景 | 分布式共享数据 | 本地热点数据 |
java复制// 生产者配置
DefaultMQProducer producer = new DefaultMQProducer("PID_ORDER");
producer.setNamesrvAddr("mq1:9876;mq2:9876");
producer.setRetryTimesWhenSendFailed(3);
producer.start();
// 消费者配置
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("CID_ORDER");
consumer.setConsumeThreadMin(20);
consumer.setConsumeThreadMax(64);
关键特性:
针对日志类消息,我们使用Kafka并做了以下优化:
batch.size=16384和linger.ms=20提升吞吐snappy压缩节省带宽压测数据对比:
| 指标 | RocketMQ | Kafka |
|---|---|---|
| 单机TPS | 6W | 15W |
| 延迟 | 50ms | 5ms |
| 消息可靠性 | 99.9999% | 99.99% |
| 事务支持 | 完整支持 | 有限支持 |
MySQL采用ShardingSphere实现水平分片:
sql复制# 订单表按用户ID分片
CREATE TABLE `t_order_0` (
`id` bigint NOT NULL COMMENT '包含用户ID哈希值',
`user_id` bigint NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
分片策略:
对于需要跨分片查询的业务,我们引入TiDB 5.0:
sql复制-- 创建分区表
CREATE TABLE `t_settlement` (
`id` BIGINT AUTO_RANDOM,
`amount` DECIMAL(18,2),
PRIMARY KEY (`id`)
) PARTITION BY RANGE (id) (
PARTITION p0 VALUES LESS THAN (1000000),
PARTITION p1 VALUES LESS THAN (2000000)
);
性能对比:
| 场景 | MySQL(分片) | TiDB |
|---|---|---|
| 单点查询 | 3ms | 5ms |
| 跨分片Join | 不支持 | 200ms |
| 写入TPS | 8000 | 12000 |
| 扩容复杂度 | 需要停机迁移 | 在线自动平衡 |
采用双重检查锁+异步加载机制:
java复制public Product getProduct(String id) {
// 第一层检查
Product product = redis.get(id);
if (product == null) {
synchronized (this) {
// 第二层检查
product = redis.get(id);
if (product == null) {
product = loadFromDB(id);
// 异步更新缓存
executor.submit(() -> redis.setex(id, 3600, product));
}
}
}
return product;
}
当RocketMQ出现堆积时,我们采用三级处理策略:
综合性能考虑,我们最终选择改良版Snowflake:
code复制0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000
调整了workerId的分配策略,通过ZK动态分配避免冲突。
在8核32G的物理机上进行的对比测试:
缓存层测试:
消息队列测试:
数据库测试:
所有测试数据均通过Grafana监控采集,JMeter施加压力。