我要开一个超市,超市里面卖的是“数据库和中间件(3)”
第一章:筹备期——基础系统搭建(MySQL核心)
1.1 选址与装修:数据库选型与存储引擎
你准备开一家社区超市,需要一套系统管理商品、库存、销售。你选MySQL作为主数据库,因为要保证数据持久化、支持事务。
为什么选InnoDB?
InnoDB支持事务(ACID)、行级锁、崩溃恢复,适合超市交易场景。MyISAM虽然查询快,但不支持事务,表锁并发差,只适合只读报表。我们决定所有核心表都用InnoDB。
1.2 货架摆放:表结构设计与数据类型优化
商品表(product):
- id BIGINT(主键,自增)
- name VARCHAR(128)(变长,UTF8MB4支持emoji)
- price DECIMAL(10,2)(精确小数,避免浮点误差)
- category_id INT(分类ID,外键)
- stock INT UNSIGNED(库存,非负)
- create_time DATETIME(3)(毫秒精度)
数据类型细节:
- 能用INT不用BIGINT,减少索引空间。
- 时间用DATETIME(3)而非TIMESTAMP,避免2038年问题。
- 商品描述用TEXT,但单独拆出product_detail表,避免主表行溢出(行溢出的原理:每行最多约8KB,TEXT字段前768字节存溢出页指针,其余存在其他页,读取时需额外IO)。
- 状态字段用TINYINT枚举,不用字符串,节省空间。
1.3 商品分类与索引设计
经常按分类查询,为category_id加索引。但查询“分类A且价格降序”时,只建(category_id)索引会导致额外排序。
联合索引最左前缀原则:建(category_id, price)联合索引,B+树先按category_id排序,再按price排序,这样范围查询时能直接利用索引顺序。
索引覆盖:如果查询只需id, name, price,联合索引叶子节点已包含这些字段,无需回表(Extra显示Using index)。
1.4 结账收银:事务与隔离级别
收银台需要保证扣库存和生成订单的原子性。
事务ACID:原子性通过undo log保证;一致性由应用逻辑保证;隔离性通过锁和MVCC;持久性通过redo log。
隔离级别:默认RR(可重复读)。但RR会产生间隙锁,影响并发。我们评估后改为RC(读已提交),因为业务可接受不可重复读,但能减少锁竞争。
MVCC实现:每行记录有DB_TRX_ID(最后修改事务ID)、DB_ROLL_PTR(指向undo log)。RC级别下,每次查询都生成新的ReadView,RR级别下只在事务第一次查询生成ReadView。
行锁优化:扣库存时,用UPDATE product SET stock = stock - 1 WHERE id = ? AND stock > 0,利用行锁,但必须走主键索引。如果库存字段更新频繁,考虑库存单独表或Redis。
1.5 促销活动:SQL优化与执行计划
超市促销,要找出“最近一周销量前10的商品”。SQL:
1 | |
执行计划显示Using temporary; Using filesort,因为GROUP BY和ORDER BY字段不一致。优化方案:
- 在order_detail表建(create_time, product_id)索引,先用时间过滤,再分组。
- 但SUM无法索引覆盖,考虑用汇总表提前统计(每天定时计算销量存入单独表),查询时直接查汇总表。
- 用EXPLAIN分析时关注type(range/ref/eq_ref)、Extra(Using index condition等)。
索引下推(ICP):MySQL5.6后,如果索引包含create_time,存储引擎会先过滤create_time,再回表,减少回表次数。
1.6 库存预警:分区表与分表
库存变动记录表(stock_log)数据量增长快,按月份分区。
分区表:RANGE分区,按月分区,查询时只扫描相关分区。分区对应用透明,但注意分区键必须是主键的一部分。
分表:订单表按user_id分表,用ShardingSphere中间件。分表策略:user_id % 64,路由到对应表。
分库分表挑战:
- 跨节点查询:需聚合多个结果。
- 分页:
ORDER BY create_time LIMIT 10需从每个分表取前10条,然后归并排序,再取前10,性能差。优化:先查出时间范围,再查具体数据。 - 分布式主键:用雪花算法(Snowflake),机房ID+机器ID+时间戳+序列号,解决自增ID冲突。
第二章:火爆开业——缓存与高并发(Redis)
2.1 客流激增:引入Redis缓存
开业促销,客流量巨大,数据库压力飙升。你引入Redis做缓存,商品详情放缓存。
缓存更新策略:更新DB后,删除缓存(Cache Aside Pattern)。为什么不是更新缓存?因为更新可能涉及复杂计算,且多次写时缓存频繁更新浪费。
缓存击穿:热点商品缓存失效瞬间,大量请求穿透。解决方案:互斥锁(SETNX)或热点数据永不过期(但需主动更新)。
锁实现细节:
1 | |
但这样可能锁被其他线程释放(如果业务超时),需用Redisson的RLock,支持看门狗自动续期。
Redisson原理:通过lua脚本加锁,并启动定时线程,每10秒检查锁是否存在并续期30秒。
2.2 恶意请求:缓存穿透与布隆过滤器
有人不断请求不存在的商品ID,导致每次穿透到DB。解决方案:
布隆过滤器:初始化时把所有商品ID放入布隆过滤器,请求时先检查过滤器,不存在直接返回。
布隆过滤器原理:多个哈希函数映射到位数组,判断不存在则绝对不存在,存在可能有误判(可设置误判率)。
Redis实现:Redisson的RBloomFilter,底层用bitmap。误判率越低,数组越大,哈希函数越多。
空值缓存:对于不存在的数据,缓存一个空对象(如”null”),设置较短过期时间,也能防穿透。
2.3 限流进店:Redis计数器与Lua脚本
控制同时进店人数,用Redis的INCR和过期时间,但需要保证原子性:
1 | |
Lua脚本在Redis中原子执行,避免竞态。
2.4 排行榜:Sorted Set底层原理
超市热销榜,用Sorted Set,每次销售后ZINCRBY。
编码优化:当成员数小于128且成员长度小于64时,用ziplist编码,紧凑存储。ziplist是一块连续内存,每个entry包含前一个entry长度、当前数据。查找时需遍历,但数据量小,性能可接受。
跳表:当数据量大时转为skiplist,支持范围查询。跳表是多层链表,插入时随机层数,查找复杂度O(logN)。
排行榜分页:ZREVRANGE支持按排名范围获取,O(logN+M)。
2.5 购物车:Hash结构
用Redis Hash存储用户购物车,key为cart:userId,field为商品ID,value为数量。
Hash底层:field数量少时用ziplist,多时用hashtable。
优缺点:节省内存(相比String),可批量操作(HGETALL、HMSET)。
购物车合并:用户登录时,需合并未登录时的购物车,用Lua脚本保证原子性。
2.6 会员积分:String原子操作
会员积分变更频繁,用Redis String的INCRBY/DECRBY。
原子性:INCR是原子操作,底层是单线程,不会有并发问题。
持久化:积分最终需持久化到DB,通过定时任务同步,或用消息队列异步更新。
2.7 分布式Session
超市会员系统多实例部署,用户会话共享。用Spring Session集成Redis,用Hash存储Session,并设置过期。
序列化:默认JDK序列化体积大,改用Jackson2JsonRedisSerializer。注意对象类型信息,防止反序列化失败。
Session失效:Redis的过期键自动清理,配合惰性删除。
2.8 订单号:分布式ID生成器
订单号需全局唯一、趋势递增。雪花算法:
- 1位符号位
- 41位时间戳(毫秒级,可用69年)
- 10位机器ID(5位机房+5位机器)
- 12位序列号(每毫秒4096个)
时间回拨问题:如果机器时间回拨,可能生成重复ID。解决方案:记录上次生成时间,如果当前时间小于上次,则等待时间追上或抛出异常;或使用百度的UidGenerator,借用数据库递增。
2.9 秒杀抢购:Redis分布式锁+库存预减
秒杀时,先用Redis预扣库存(DECR),成功再生成订单发消息。
Redis事务:MULTI/EXEC可保证一组命令原子执行,但WATCH可用于乐观锁。但更推荐Lua脚本。
库存预减脚本:
1 | |
返回1表示成功,然后异步创建订单。如果后续订单创建失败,需回滚库存(发消息或定时补偿)。
2.10 缓存雪崩:过期时间随机化
大量缓存同时失效,导致DB压力暴增。解决方案:
- 设置随机过期时间(基础时间+随机范围)
- 永不过期,后台线程定时更新
- 多级缓存:本地缓存(Caffeine)+ Redis
2.11 Redis高可用:哨兵与集群
单机Redis故障将导致全站不可用。
哨兵模式:监控主节点,自动故障转移。哨兵通过Raft协议选举leader,判断主观下线(SDOWN)和客观下线(ODOWN)。
集群模式:数据分片,每个主节点有从节点。客户端直连任意节点,返回MOVED或ASK重定向。
集群原理:16384个哈希槽,每个节点负责一部分。扩容时重新分配槽位,数据迁移过程中,客户端可能收到ASK重定向。
脑裂问题:哨兵模式下,网络分区可能导致两个主节点,但哨兵多数派才能选举,可配置min-slaves-to-write等参数防止数据丢失。
2.12 持久化:RDB与AOF
Redis重启后数据恢复,必须持久化。
RDB:fork子进程生成快照,利用写时复制,不影响主进程。但可能丢失最后一次快照后的数据。
AOF:记录每条写命令,每秒刷盘(everysec)最多丢1秒数据。AOF文件重写时也fork子进程,合并命令。
混合持久化(Redis 4.0):AOF重写时先写RDB格式,再追加增量命令,加载更快。
数据恢复:先加载AOF(如果开启),否则加载RDB。
2.13 慢查询与监控
开启Redis慢查询日志,定位大key和复杂命令。SLOWLOG GET 100查看。大key扫描用redis-cli --bigkeys,避免阻塞。
monitor命令慎用,高并发下会大量消耗CPU。
第三章:连锁经营——微服务与中间件
3.1 业务拆分:微服务架构
超市发展成连锁,系统按领域拆分为商品服务、订单服务、库存服务、会员服务、营销服务。
服务发现:用Nacos。每个服务启动时注册,消费者通过服务名调用。
注册中心原理:Nacos支持CP和AP切换。临时实例用Distro协议(AP),持久实例用Raft(CP)。客户端长轮询获取变更。
配置中心:Nacos也管理配置,配置变更实时推送,支持灰度发布、版本回滚。
3.2 服务调用:Dubbo vs OpenFeign
内部RPC选用Dubbo,因为性能高、服务治理强。
Dubbo核心:
- SPI机制:可扩展协议、序列化、负载均衡等。
- 负载均衡:默认随机,可配置一致性哈希(让相同参数落到相同节点,利用缓存)。
- 集群容错:Failover(重试其他节点)、Failfast(快速失败)、Failsafe(忽略异常)等。
- 序列化:Hessian2、Kryo、Protobuf。Kryo性能最好,但需注册类。
- 传输:Netty异步,连接复用。
- 服务路由:tag路由实现灰度发布。
Feign用于HTTP调用,适合对外API,内置Ribbon负载均衡。
3.3 网关:Spring Cloud Gateway
所有外部请求经过网关,做路由、鉴权、限流、日志。
WebFlux:基于Reactor非阻塞,适合IO密集型。
过滤器:GlobalFilter实现鉴权(JWT校验)。
限流:集成Redis RateLimiter,用令牌桶算法。
动态路由:从Nacos拉取路由配置,实时生效。
跨域:CORS配置。
3.4 消息队列:RocketMQ深度
订单创建后,需通知库存、积分、短信等模块,引入RocketMQ。
架构:NameServer(无状态)、Broker(主从)、Producer、Consumer。
消息存储:CommitLog顺序写,ConsumeQueue逻辑队列。零拷贝(mmap)提升性能。
刷盘:同步刷盘(数据可靠,性能低) vs 异步刷盘(性能高,可能丢数据)。我们选异步刷盘+同步复制保证可靠性。
同步复制:Master收到消息,写入PageCache,并等待Slave确认,才返回成功。
消息重试:消费失败后,RocketMQ会重试16次,间隔递增。重试队列是特殊Topic。
死信队列:重试超限后进入死信,需人工干预。
消息幂等:消费者用订单号做唯一键(数据库唯一索引)或Redis setnx,保证同一条消息只处理一次。
事务消息:预扣库存和发送消息需原子。流程:
- 发送半消息。
- 执行本地事务(预扣库存)。
- 提交或回滚消息。
若半消息长时间未确认,MQ回查业务状态。
消息顺序:同一订单的消息发到同一队列,消费者单线程拉取。但故障转移可能导致队列乱序,所以必要时用严格顺序消息(牺牲高可用)。
消息过滤:Tag过滤(Broker端)或SQL92过滤。
消息轨迹:开启轨迹记录,追踪消息生命周期。
延时消息:RocketMQ支持延时等级(1s/5s/…),用于定时任务。
批量消息:提高吞吐量,但每条消息需相同topic和waitStoreMsgOK。
3.5 分布式事务:Seata AT模式
跨服务的事务需保证一致性,用Seata。
AT模式:
- 第一阶段:各分支执行本地SQL,记录undo_log(before image),返回成功。
- 第二阶段:全局提交时异步删除undo_log;全局回滚时根据undo_log生成反向SQL回滚。
全局锁:TC协调,防止脏写。性能较好,但需数据库支持。
TCC模式:业务侵入大,适合高性能场景。Try阶段预留资源,Confirm提交,Cancel释放。
Saga模式:长事务,每个服务有补偿接口,适合业务流程复杂且不需要实时一致。
XA模式:数据库原生支持,但性能差。
3.6 分布式调度:XXL-JOB
每天凌晨统计报表,每月结算积分,需定时任务。
架构:调度中心、执行器。
分片任务:报表统计可拆分为多个分片,每个分片处理部分数据,用Redis分布式锁协调。
失败重试:任务失败自动重试,可设置重试次数。
任务依赖:DAG方式,任务B依赖任务A完成。
动态任务:通过API创建任务,用于临时数据修复。
调度策略:轮询、随机、一致性哈希等。
3.7 限流与熔断:Sentinel
大促时,需对核心接口限流熔断。
滑动窗口:统计QPS、RT、异常比例。将时间划分为多个格子(比如1秒分2个格子),格子独立计数,滑动统计。
流量控制:直接拒绝、Warm Up、排队等待。Warm Up让流量缓慢增加,避免冷启动系统崩溃。
熔断降级:当错误率或慢调用比例超过阈值,熔断器打开,后续请求快速失败。熔断器状态机:关闭->打开->半开。半开时放行少量请求,成功则关闭。
热点参数限流:针对商品ID限流,比如秒杀商品只允许100 QPS。
规则持久化:推送到Nacos,动态生效。
系统自适应限流:根据系统负载(CPU、Load)动态调整。
3.8 链路追踪:SkyWalking
微服务调用链长,需追踪问题。
探针:基于字节码注入,自动拦截RPC、数据库、MQ等调用。
Trace和Span:每个请求生成TraceID,Span记录调用信息(开始时间、耗时、标签)。
采样:默认每秒最多10条,避免存储压力。
存储:使用Elasticsearch存储,可搜索Trace。
拓扑图:自动生成服务依赖关系图。
告警:根据指标触发(如高延迟、异常数)。
3.9 容器化与编排:Kubernetes
为方便部署扩缩,所有服务容器化。
Dockerfile:多阶段构建,减少镜像体积。
K8s概念:
- Pod:最小调度单元,包含一个或多个容器。
- Deployment:管理Pod副本,支持滚动更新、回滚。
- Service:提供稳定的访问入口,ClusterIP、NodePort、LoadBalancer。
- ConfigMap/Secret:挂载配置。
- Ingress:外部访问入口,类似网关,支持SSL、路由。
- HPA:基于CPU或自定义指标自动扩缩容。
- Operator:自定义控制器,如Redis Operator管理Redis集群。
Service Mesh:后期引入Istio,实现流量治理(金丝雀发布、故障注入)、安全(mTLS)、可观测性。
第四章:集团化——数据驱动与高级特性
4.1 数据仓库与离线分析
超市业务数据需分析,用于决策。用Canal监听MySQL binlog,将增量数据同步到Kafka,然后由Flink实时计算,最终落入ClickHouse或Doris。
Canal原理:模拟MySQL slave协议,解析binlog(ROW格式),发送到MQ。
Binlog格式:ROW记录每行变更,占用空间大,但准确。
实时计算:Flink消费Kafka,计算实时指标(每分钟销量)。
OLAP:ClickHouse列式存储,适合聚合查询,用于报表。
4.2 全文搜索:Elasticsearch
商品搜索需要全文检索,用Elasticsearch。
倒排索引:对商品名称、描述分词,建立词到文档ID的映射。
分词器:IK中文分词器,自定义词典。
ES集群:分片、副本、路由。
数据同步:通过Canal或MQ同步MySQL数据到ES,保证最终一致。
4.3 分布式协调:ZooKeeper
有些场景需要分布式协调,如Kafka、Dubbo依赖ZooKeeper。
ZAB协议:ZooKeeper的原子广播,保证数据一致性。
临时节点:用于服务注册,会话断开自动删除。
Watcher机制:监听节点变化,实现配置动态更新。
应用:Dubbo早期用ZK做注册中心,RocketMQ的NameServer也类似(但无状态)。
4.4 监控体系:Prometheus + Grafana
全面监控基础设施、应用、业务指标。
Prometheus:拉取metrics,时序数据库。
Exporter:Node Exporter(主机)、MySQL Exporter、Redis Exporter、JMX Exporter。
告警:Alertmanager,根据规则发送通知。
Grafana:可视化仪表盘。
业务监控:埋点统计订单量、销售额等。
4.5 日志收集:ELK
统一日志收集,方便排查问题。
Filebeat:采集日志文件。
Logstash:过滤、解析日志。
Elasticsearch:存储日志。
Kibana:搜索、可视化。
日志格式:JSON格式,包含TraceID,便于链路关联。
4.6 JVM调优与故障排查
服务运行中需不断优化JVM。
堆内存分配:根据服务特点,新生代占比(-Xmn)、老年代大小。
GC日志:打印GC详情,分析停顿。
G1垃圾回收:适合大堆,设置最大停顿时间。
OOM处理:HeapDumpOnOutOfMemoryError,用MAT分析。
性能工具:jstack、jmap、jstat、Arthas。
4.7 多线程与并发
高并发下需合理使用线程池。
ThreadPoolExecutor:核心线程数、最大线程数、队列、拒绝策略。
拒绝策略:Abort(抛异常)、CallerRuns(调用者执行)、Discard、DiscardOldest。
异步编程:CompletableFuture,结合线程池,实现异步编排。
并发工具:CountDownLatch、CyclicBarrier、Semaphore、ConcurrentHashMap(分段锁/红黑树优化)。
4.8 设计模式在中间件中的应用
- 工厂模式:Spring BeanFactory
- 单例模式:Spring Bean默认单例
- 代理模式:AOP、Dubbo的Proxy
- 观察者模式:Spring事件监听
- 责任链模式:Netty Pipeline、Sentinel的Slot链
- 策略模式:Dubbo负载均衡策略
- 模板方法:JdbcTemplate、RedisTemplate
第五章:复盘与持续优化
5.1 常见坑及解决方案
- MySQL死锁:不同事务更新顺序不同导致,强制按固定顺序更新。
- Redis缓存雪崩:大量key同时过期,加随机过期时间。
- 消息堆积:消费者处理慢,可扩容消费者、增加队列。
- 分布式事务回滚失败:AT模式undo_log丢失,确保本地事务幂等。
- 网关限流误杀:配置动态调参,灰度发布。
- K8s OOMKilled:JVM未感知容器内存,加
-XX:+UseContainerSupport。 - Dubbo连接超时:调整超时时间,排查网络问题。
- ES脑裂:discovery.zen.minimum_master_nodes设置正确。
5.2 性能调优 checklist
- MySQL:慢查询优化、索引设计、连接池大小、缓冲池大小、redo log大小。
- Redis:避免大key、热key、使用pipeline、合理设置maxmemory-policy(如allkeys-lru)。
- JVM:调整堆大小、GC算法、线程栈大小。
- 网络:TCP参数调优、使用连接池。
- 应用:缓存热点数据、异步化、并行处理。
5.3 总结
至此,从一家小超市到全国连锁集团,我们引入了MySQL的所有核心优化(索引、事务、锁、分区、分库分表、主从复制、监控),Redis的所有功能(数据结构、持久化、高可用、集群、Lua、布隆过滤器),以及Java全家桶中间件(Nacos、Dubbo、Gateway、RocketMQ、Seata、XXL-JOB、Sentinel、SkyWalking、K8s、ELK、Prometheus等)。每个技术点都结合了超市的实际问题,从原理到实践,从踩坑到优化。


