1. 从“黑马商城”看电商练手项目的核心价值
“黑马商城”这四个字,在开发者圈子里出现的频率相当高。它通常指代一套面向学习者的电商系统实战项目,涵盖商品展示、购物车、订单、支付、后台管理等完整链路。很多人第一次接触它,是在掌握了基础语法之后,想找一个“能串起所有知识点”的综合案例。这个项目恰好满足了这个需求:它不是玩具级的增删改查,而是模拟真实业务场景,把前端交互、后端接口、数据库设计、缓存策略、并发处理这些环节全部揉在一起。
我见过不少朋友,学完基础课程后陷入迷茫,不知道下一步该做什么。他们的困境很典型:单个知识点都懂,但一遇到完整项目就无从下手。黑马商城这类项目的价值就在这里——它提供了一个“脚手架”,让你在模仿中理解一个真实系统是怎么从零搭建起来的。你不需要从零设计架构,但需要理解每一层为什么这么设计。比如为什么商品详情页要单独做缓存?为什么订单号不能直接用自增主键?为什么购物车数据有时候放Redis有时候放数据库?这些问题,只有真正动手做过一遍,才能有体感。
这篇文章适合三类人:第一类是有一定编程基础、想通过完整项目提升实战能力的学习者;第二类是刚入行不久、需要快速了解电商系统核心模块的开发者;第三类是想自己搭建小型电商平台、需要参考实现思路的技术爱好者。我会从整体设计、核心细节、实操过程、问题排查四个维度展开,把我在类似项目中踩过的坑、总结的技巧都摊开来讲。文章不会贴大段代码,但会给出关键配置、参数计算和排查思路,你可以直接对照着在自己的环境里复现。
2. 整体架构设计与技术选型思路
2.1 为什么选择前后端分离加微服务雏形
黑马商城这类项目,早期版本多用单体架构,后来逐渐演变为前后端分离加服务拆分的形态。这个演变不是赶时髦,而是业务复杂度倒逼的结果。单体架构下,商品、订单、用户、支付全在一个工程里,改一处代码要重新部署整个应用,团队协作时冲突不断。前后端分离解决了界面与逻辑耦合的问题,后端只提供JSON接口,前端用Vue或React独立开发,两边可以并行推进。
再往后,当订单模块和商品模块的流量特征差异越来越大——商品查询量大但逻辑简单,订单写入频繁且涉及事务——就自然需要拆分成独立服务。黑马商城通常会把商品服务、订单服务、用户服务、购物车服务、支付服务分开部署,每个服务有自己的数据库。这种拆分带来的好处是:商品服务可以加多级缓存扛住读压力,订单服务可以单独扩容应对写高峰,互不影响。
但拆分也带来了新问题:服务之间怎么调用?数据一致性怎么保证?这就引出了注册中心、配置中心、远程调用、分布式事务这些技术点。对于学习者来说,不需要一上来就搞全套微服务,可以先从单体加前后端分离开始,跑通主流程后再逐步拆分。我的建议是:第一遍做的时候,把商品、订单、用户放在一个工程里,用模块化包结构隔离;第二遍再尝试拆成独立服务,体会其中的差异。
2.2 技术栈选型的背后逻辑
这类项目常见的技术组合是:Spring Boot + MyBatis-Plus + MySQL + Redis + RabbitMQ + Nginx + Vue。每一层选型都有实际考量。
Spring Boot 不用多说,自动配置和起步依赖让搭建成本大幅降低。MyBatis-Plus 相比原生MyBatis,提供了通用的CRUD方法和条件构造器,写单表操作时效率极高,复杂查询再手写SQL,兼顾了开发速度和灵活性。MySQL 作为主数据库,负责持久化商品、订单、用户等核心数据。Redis 承担缓存、会话存储、购物车临时数据、分布式锁等职责,是提升性能的关键组件。RabbitMQ 用于异步解耦,比如下单后异步扣减库存、发送通知、更新积分,避免主流程被非核心逻辑拖慢。Nginx 做反向代理和负载均衡,同时可以托管前端静态资源。Vue 作为前端框架,组件化开发体验好,生态成熟。
这里重点说一下为什么购物车要用Redis。购物车数据的特点是:读写频繁、临时性强、用户不关心持久化。如果每次增删改查都走MySQL,数据库压力会很大。放到Redis里,用Hash结构存储,key是用户ID,field是商品ID,value是数量,读写都在内存中完成,速度极快。而且可以设置过期时间,用户长时间不操作自动清理。但要注意,Redis数据可能丢失,所以关键操作比如下单时,需要把购物车数据落库或做持久化处理。
2.3 数据库设计的关键取舍
电商系统的数据库设计有几个经典难题:商品表怎么设计才能支持多规格?订单表怎么存储商品快照?库存怎么扣减才不出超卖?
商品多规格通常用“商品表 + 规格表 + 规格值表 + SKU表”的结构。SPU代表标准产品单位,比如“某款手机”;SKU代表库存量单位,比如“某款手机 黑色 128G”。用户下单时选择的是具体SKU,所以库存挂在SKU上,价格也挂在SKU上。这种设计虽然表多了几张,但扩展性好,新增颜色或内存规格时不需要改表结构。
订单表存储商品快照是必须的。因为商品信息会变,价格会调,如果订单只存商品ID,以后查历史订单时显示的是最新价格,那就乱套了。所以下单时要把商品名称、图片、单价、规格等关键信息复制一份到订单明细表里。这个操作叫“冗余存储”,虽然浪费一点空间,但保证了历史数据的准确性。
库存扣减是另一个重点。简单做法是下单时直接update stock = stock - 1 where stock > 0,靠数据库行锁保证不超卖。但在高并发下,大量请求排队等锁,响应会变慢。更好的方案是:先在Redis里预扣库存,用Lua脚本保证原子性,下单成功后异步同步到数据库。如果支付超时或取消订单,再把库存回滚到Redis和数据库。这个方案实现复杂一些,但能扛住更高的并发。
3. 核心模块细节与实操要点
3.1 商品详情页的多级缓存策略
商品详情页是电商系统里读请求最集中的地方。一个热门商品可能每秒被查询上万次,如果每次都查数据库,数据库肯定扛不住。所以必须加缓存,而且要多级缓存。
第一级是本地缓存,用Caffeine或Guava Cache,存在应用进程内存里。优点是速度极快,没有网络开销;缺点是容量有限,且多个应用实例之间不一致。适合缓存那些变化极少、访问极频繁的热点数据,比如首页分类、品牌列表。
第二级是Redis缓存,所有应用实例共享。商品详情数据通常以JSON字符串形式存储,key用product:detail:{skuId},过期时间设30分钟到2小时,具体看商品更新频率。更新商品时,先更新数据库,再删除Redis缓存,而不是直接更新缓存。为什么?因为并发更新时,直接更新缓存可能导致脏数据,而删除缓存让下次查询自然回源,更安全。
第三级是数据库,作为最终数据源。查询逻辑是:先查本地缓存,命中则返回;未命中查Redis,命中则回填本地缓存并返回;Redis也未命中,查数据库,回填Redis和本地缓存,再返回。
这里有个坑要注意:缓存穿透。如果查询一个不存在的商品ID,每次都会穿透到数据库。解决办法是缓存空值,比如把product:detail:999999设为空字符串,过期时间短一点,比如5分钟。或者用布隆过滤器,在查询前先判断ID是否存在。布隆过滤器有误判率,但能挡住绝大部分无效请求。
另一个坑是缓存雪崩。如果大量缓存同时过期,请求会全部打到数据库。解决办法是给过期时间加随机值,比如基础30分钟加上0到5分钟的随机数,让过期时间分散开。同时可以加互斥锁,只允许一个线程回源,其他线程等待。
3.2 购物车的数据结构与操作逻辑
购物车模块看似简单,但细节不少。前面说了用Redis Hash存储,具体结构是:key为cart:{userId},field为skuId,value为数量。添加商品时,用HINCRBY命令,如果field不存在则创建,存在则增加数量。这个命令是原子的,不用担心并发问题。
但购物车还需要展示商品的最新信息,比如名称、图片、价格。这些信息不在Redis里,需要根据skuId去商品服务查询。如果购物车里有20个商品,就要调20次商品接口,性能很差。优化方案是:商品服务提供批量查询接口,一次传入多个skuId,返回商品列表。购物车服务拿到列表后,和Redis里的数量合并,再返回给前端。
购物车还需要处理选中状态。用户可能只想结算其中几件商品,所以每个购物车项要有一个selected字段。这个字段可以存在Redis的value里,把value从单纯的数字改成JSON对象,包含数量和选中状态。但这样就不能用HINCRBY了,需要先读出来,修改后再写回去。并发下可能覆盖,所以要么加分布式锁,要么接受偶尔的不一致——毕竟购物车选中状态不是关键数据,用户重新勾选一下就行。
还有一个细节:未登录用户的购物车。用户没登录时,购物车数据存在浏览器本地存储里;登录后,需要把本地购物车合并到Redis里。合并逻辑是:遍历本地购物车,逐个添加到Redis,如果Redis里已有相同skuId,数量相加。这个操作要幂等,避免重复合并导致数量翻倍。
3.3 订单创建与库存扣减的完整流程
下单是整个系统最核心也最复杂的环节。一个完整的下单流程包括:校验购物车选中项、计算总价、扣减库存、创建订单、清空购物车、发送异步消息。每一步都可能出问题,需要仔细设计。
首先是校验。要确认购物车里有选中的商品,商品处于上架状态,库存充足,用户收货地址有效。这些校验可以在前端做一遍提升体验,但后端必须再做一遍,因为前端数据不可信。
计算总价时要注意:价格要用BigDecimal,不能用double,否则会出现精度问题。比如0.1加0.2在double下不等于0.3。BigDecimal的用法是new BigDecimal("0.1").add(new BigDecimal("0.2")),注意要用字符串构造,用double构造仍然有精度损失。
扣减库存是重点。前面提到用Redis预扣,具体做法是:在Redis里为每个SKU维护一个库存字段,下单时用Lua脚本执行“判断库存是否充足,充足则扣减,否则返回失败”。Lua脚本在Redis里是原子执行的,不会被打断。扣减成功后,再异步发送消息到RabbitMQ,由库存服务消费消息去更新数据库。如果数据库更新失败,要有补偿机制,比如记录日志人工处理,或者把Redis库存加回去。
创建订单时,订单号不能直接用自增ID,因为会暴露业务量,而且分库分表后自增ID会冲突。常见方案是:时间戳 + 用户ID后几位 + 随机数,或者用雪花算法生成全局唯一ID。订单状态初始为“待支付”,设置超时时间,比如30分钟。超时未支付,系统自动取消订单并回滚库存。这个超时取消可以用RabbitMQ的延迟队列实现,也可以用定时任务扫描。
清空购物车时,只删除已下单的商品,未选中的保留。这个操作要放在订单创建成功之后,避免下单失败但购物车已清空。
3.4 支付回调与订单状态机
支付模块通常不自己实现,而是对接第三方支付接口。但支付回调的逻辑必须自己处理。第三方支付成功后,会异步通知我们的接口,告诉我们订单支付成功。这个通知可能重复发送,所以接口必须幂等。
幂等的实现方式是:在订单表里记录支付流水号,回调时先查这个流水号是否已处理过。如果已处理,直接返回成功;如果未处理,更新订单状态为“已支付”,记录流水号,然后返回成功。这样即使回调多次,订单状态也不会错乱。
订单状态机是另一个关键点。订单状态包括:待支付、已支付、待发货、已发货、已完成、已取消、已退款。状态之间的流转必须严格控制,不能从“待支付”直接跳到“已完成”。可以在代码里用枚举定义允许的流转,比如OrderStatusEnum.canTransferTo(current, target),每次更新前校验。这样能避免很多逻辑错误。
支付超时取消订单时,要注意并发问题。如果用户在超时前一秒支付,同时定时任务也在取消订单,可能两边同时操作。解决办法是用数据库乐观锁:更新订单状态时带上原状态条件,比如update orders set status = '已取消' where id = ? and status = '待支付'。如果支付回调先执行,状态已变成“已支付”,这条更新影响行数为0,取消操作自动失败。
4. 实操过程与关键环节实现
4.1 环境搭建与基础配置
先说一下我的环境:JDK 17,Maven 3.8,MySQL 8.0,Redis 7.0,RabbitMQ 3.11,Nginx 1.24,Node 18。版本不用完全一致,但JDK建议17以上,MySQL建议8.0以上,因为用到了一些新特性。
数据库初始化时,字符集用utf8mb4,排序规则用utf8mb4_general_ci,这样能支持emoji。连接池用HikariCP,Spring Boot默认就是它。关键配置是maximum-pool-size,建议设为CPU核数的2倍加磁盘数,比如4核机器设10左右。connection-timeout设3000毫秒,idle-timeout设600000毫秒。
Redis配置要注意序列化方式。默认的JDK序列化可读性差,建议用StringRedisSerializer作为key的序列化器,用GenericJackson2JsonRedisSerializer作为value的序列化器。这样存进去的数据是JSON格式,方便排查问题。连接池用Lettuce,配置max-active为8,max-idle为8,min-idle为0。
RabbitMQ需要创建几个关键队列:订单延迟队列(用于超时取消)、库存扣减队列、通知队列。延迟队列用死信队列加TTL实现:创建一个普通队列,设置x-dead-letter-exchange指向死信交换机,消息发送时设置TTL为30分钟,到期后自动转到死信队列,消费者监听死信队列处理取消逻辑。
Nginx配置反向代理,把/api开头的请求转发到后端服务,其他请求指向前端静态文件。负载均衡用默认的轮询策略即可,如果后端实例性能差异大,可以用加权轮询。
4.2 商品模块的接口实现与缓存接入
商品模块的接口主要包括:商品列表、商品详情、商品搜索、分类查询。列表和搜索用分页,详情用缓存。
商品列表接口接收分类ID、页码、每页数量、排序方式等参数。查询时先查缓存,key可以设计为product:list:{categoryId}:{page}:{size}:{sort}。但列表缓存命中率通常不高,因为参数组合太多,所以更常见的做法是只缓存详情,列表直接查数据库,但给数据库加索引。索引建在category_id、status、create_time这些字段上。
商品详情接口是缓存的重点。实现步骤:先查本地缓存Caffeine,key为product:detail:{skuId},如果命中直接返回。未命中查Redis,key相同,如果命中,反序列化成对象,放入本地缓存,返回。Redis也未命中,查数据库,组装商品详情对象(包括SPU信息、SKU列表、规格、图片等),序列化后写入Redis,设置过期时间加随机值,再写入本地缓存,返回。
更新商品时,先更新数据库,再删除Redis缓存和本地缓存。注意删除本地缓存只能删当前实例的,其他实例的本地缓存会过期后自然失效。如果要求强一致,可以发广播消息让所有实例删本地缓存,但一般业务场景接受短暂不一致。
4.3 购物车接口的完整实现
购物车接口包括:添加商品、修改数量、删除商品、查询购物车、选中/取消选中。
添加商品时,先校验商品是否存在且上架,然后执行HINCRBY cart:{userId} {skuId} {quantity}。如果返回的数量超过库存,要回滚,用HINCRBY减回去。同时设置key的过期时间为7天,每次操作都刷新过期时间。
修改数量时,直接HSET覆盖。但要注意数量不能为负,不能超过库存。删除商品用HDEL。查询购物车时,先HGETALL拿到所有skuId和数量,然后调商品服务批量查询商品信息,合并后返回。选中状态可以存在另一个Hash里,key为cart:selected:{userId},field为skuId,value为1或0。
这里有个性能优化点:批量查询商品时,如果skuId很多,可以分批查,比如每批50个,避免接口参数过长。另外,商品服务返回的数据可以再做一层本地缓存,因为购物车里的商品通常变化不频繁。
4.4 下单接口的完整实现与参数计算
下单接口的入参是用户ID和收货地址ID,后端从购物车读取选中商品。流程如下:
第一步,获取购物车选中项。从Redis读取cart:selected:{userId},拿到选中的skuId列表,再读取cart:{userId}拿到数量。
第二步,校验商品。调商品服务批量查询,确认商品存在、上架、库存充足。计算总价,用BigDecimal累加每个商品的单价乘以数量。
第三步,预扣库存。遍历每个skuId,执行Lua脚本扣减Redis库存。Lua脚本内容大致是:if redis.call('get', KEYS[1]) >= ARGV[1] then return redis.call('decrby', KEYS[1], ARGV[1]) else return -1 end。如果任何一个扣减失败,回滚前面已扣减的,返回下单失败。
第四步,创建订单。生成订单号,可以用时间戳 + 用户ID后4位 + 随机4位,比如2024052015300012345678。订单主表记录用户ID、订单号、总价、状态、收货地址快照、创建时间。订单明细表记录每个商品的skuId、名称、图片、单价、数量、规格快照。
第五步,发送异步消息。把订单号、skuId列表、扣减数量发送到RabbitMQ,库存服务消费后更新数据库库存。同时发送延迟消息到延迟队列,30分钟后处理超时取消。
第六步,清空购物车选中项。删除cart:selected:{userId}中已下单的skuId,并从cart:{userId}中删除对应商品。
参数计算方面,总价用BigDecimal,库存扣减用整数,订单号生成要保证全局唯一。如果并发量很大,时间戳加随机数可能重复,可以用Redis的INCR生成序列号,拼接到订单号里。
4.5 支付回调与超时取消的实现
支付回调接口接收第三方支付平台的异步通知,参数包括订单号、支付流水号、支付金额、签名等。处理步骤:验签,确认请求来自支付平台;查订单,确认订单存在且状态为待支付;幂等校验,查支付流水号是否已处理;更新订单状态为已支付,记录支付流水号和支付时间;发送通知消息,触发发货流程。
超时取消用延迟队列实现。下单时发送一条消息到延迟队列,消息体是订单号,TTL为30分钟。30分钟后消息进入死信队列,消费者监听死信队列,收到消息后查订单状态,如果是待支付,则取消订单,回滚Redis库存,更新数据库库存,更新订单状态为已取消。
回滚库存时要注意:先回滚Redis,再发消息回滚数据库。如果数据库回滚失败,要有重试机制。可以记录回滚日志,定时任务扫描失败记录重试。
5. 常见问题与排查技巧实录
5.1 缓存相关问题排查
缓存穿透的表现是:大量请求查询不存在的key,数据库压力骤增。排查方法是看Redis的命中率,如果命中率很低,且数据库QPS很高,可能就是穿透。解决办法是缓存空值或加布隆过滤器。
缓存雪崩的表现是:大量key同时过期,数据库瞬间被打满。排查方法是看Redis的过期时间分布,如果集中在某个时间点,就是雪崩。解决办法是加随机过期时间。
缓存击穿的表现是:某个热点key过期瞬间,大量请求同时回源。排查方法是看单个key的QPS,如果极高且数据库有对应峰值,就是击穿。解决办法是加互斥锁,只允许一个线程回源。
还有一个常见问题是缓存与数据库不一致。比如更新数据库成功但删除缓存失败,导致缓存里是旧数据。解决办法是删除缓存失败时重试,或者用消息队列保证最终一致。更简单的做法是设置较短的过期时间,比如1分钟,让旧数据尽快失效。
5.2 库存超卖与少卖问题
超卖是指库存扣成负数,少卖是指库存没扣够。超卖通常是因为并发扣减时没有加锁或锁失效。用Redis的DECRBY是原子的,不会超卖,但如果先查再扣,中间有时间窗口,就会超卖。所以必须用Lua脚本把判断和扣减放在一起。
少卖通常是因为扣减失败后没有正确回滚,或者消息丢失导致数据库库存没更新。排查方法是比对Redis库存和数据库库存,如果Redis小于数据库,说明有扣减没同步。解决办法是加对账任务,定期同步。
还有一个坑是:Redis库存和数据库库存初始化不一致。比如数据库有100件,Redis只设置了50件。所以系统启动时要做一个初始化任务,把数据库库存同步到Redis。
5.3 订单状态异常与幂等处理
订单状态异常包括:状态跳转错误、重复支付、超时取消后又被支付。排查方法是查订单状态变更日志,看每一步是谁改的、什么时候改的。
重复支付是因为支付回调重复执行。解决办法是幂等校验,用支付流水号做唯一索引,插入失败则说明已处理。
超时取消后又被支付,是因为取消和支付并发。解决办法是更新状态时带原状态条件,用乐观锁。如果取消先执行,状态变成已取消,支付回调更新时影响行数为0,就不会覆盖。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 商品详情页加载慢 | 缓存未命中,直接查数据库 | 看Redis命中率,看数据库慢查询日志 | 加缓存,优化SQL,加索引 |
| 下单时提示库存不足但实际有库存 | Redis库存未初始化或扣减失败未回滚 | 比对Redis和数据库库存 | 初始化库存,加回滚逻辑 |
| 购物车商品数量不对 | 并发添加时覆盖 | 看Redis操作日志 | 用HINCRBY,加分布式锁 |
| 订单状态卡在待支付 | 延迟队列消息丢失 | 查RabbitMQ队列深度和死信队列 | 加消息确认机制,定时任务补偿 |
| 支付回调重复处理 | 未做幂等 | 查支付流水号是否重复 | 加唯一索引,先查后插 |
| 数据库连接池耗尽 | 连接未释放或并发过高 | 看连接池活跃连接数 | 检查代码,加大连接池,加限流 |
5.5 实操心得与避坑建议
第一个心得:不要一开始就追求完美架构。我见过有人一上来就搞微服务、搞分库分表,结果连基本的增删改查都没跑通。正确的做法是先用单体跑通主流程,再逐步优化。先让系统能跑,再让系统跑得快。
第二个心得:日志要打全。关键操作比如下单、支付、库存扣减,都要打日志,记录入参、出参、耗时。出问题时,日志是唯一的线索。日志级别用INFO记录关键节点,用DEBUG记录详细数据,生产环境关掉DEBUG。
第三个心得:多用工具排查。Redis用redis-cli --stat看实时指标,用slowlog看慢查询。MySQL用show processlist看当前连接,用explain看执行计划。RabbitMQ用管理界面看队列深度和消息速率。这些工具能帮你快速定位问题。
第四个心得:压测很重要。用JMeter或wrk对下单接口做压测,看QPS、响应时间、错误率。压测时观察CPU、内存、数据库连接数、Redis命中率。只有压测过,才知道系统能扛多少并发,瓶颈在哪里。
第五个心得:异常处理要细致。不要用catch (Exception e) {}吞掉异常,至少打日志。对于可重试的异常,比如网络超时,要加重试机制。对于不可重试的异常,比如参数错误,要返回明确的错误码和提示。
6. 项目扩展与进阶方向
6.1 从单体到微服务的拆分策略
当单体应用越来越庞大,部署变慢,团队协作冲突增多时,就可以考虑拆分成微服务。拆分的原则是按业务边界拆,而不是按技术分层拆。比如商品、订单、用户、购物车、支付各自独立成一个服务,每个服务有自己的数据库。
拆分的第一步是把共享的实体类和工具类抽成公共模块,用Maven依赖引入。第二步是把跨服务的调用改成远程调用,用OpenFeign或Dubbo。第三步是引入注册中心和配置中心,用Nacos或Consul。第四步是处理分布式事务,可以用Seata的AT模式,或者用消息队列做最终一致。
拆分过程中最大的坑是分布式事务。比如下单要扣库存、创建订单、扣优惠券,这三个操作在不同服务里,怎么保证要么全成功要么全失败?强一致方案用Seata,但性能有损耗;最终一致方案用消息队列,但实现复杂。我的建议是:核心链路用强一致,非核心链路用最终一致。
6.2 高并发场景下的优化方向
高并发优化可以从几个层面入手。数据库层面:加索引、读写分离、分库分表。缓存层面:多级缓存、缓存预热、缓存降级。应用层面:无状态设计、水平扩容、异步化。网络层面:CDN加速、连接池优化、协议优化。
读写分离用ShardingSphere或MyCat,主库写,从库读,通过binlog同步。分库分表按用户ID哈希或按时间范围分,要注意跨库查询和分布式事务问题。
缓存预热是在系统启动或大促前,把热点数据提前加载到缓存。缓存降级是在缓存故障时,直接返回默认值或限流,避免数据库被打垮。
异步化是把非核心逻辑从主流程剥离,用消息队列处理。比如下单后发短信、加积分、更新推荐,都可以异步做。
6.3 安全与风控的考虑
安全方面要注意:SQL注入、XSS攻击、CSRF攻击、越权访问、敏感信息泄露。SQL注入用参数化查询防,XSS用输入过滤和输出转义防,CSRF用Token防,越权用权限校验防,敏感信息用加密存储和脱敏展示防。
风控方面要考虑:恶意刷单、黄牛抢购、异常登录。可以用限流防刷单,用验证码防黄牛,用行为分析防异常登录。限流可以用Guava RateLimiter或Sentinel,验证码可以用图形验证码或短信验证码。
还有一个容易被忽视的点:日志脱敏。用户手机号、身份证号、银行卡号不能明文打到日志里,要脱敏处理,比如只显示前3位和后4位。
6.4 可观测性建设
可观测性包括日志、指标、链路追踪。日志用ELK收集和分析,指标用Prometheus加Grafana展示,链路追踪用SkyWalking或Zipkin。
日志要结构化,用JSON格式,方便检索。关键字段包括:时间戳、服务名、实例IP、日志级别、traceId、spanId、消息内容。traceId用于串联一次请求的所有日志。
指标要覆盖:QPS、响应时间、错误率、CPU、内存、磁盘、网络、数据库连接数、缓存命中率、队列深度。这些指标能帮你快速发现异常。
链路追踪能展示一次请求经过了哪些服务、每个服务耗时多少。对于排查跨服务性能问题非常有用。
7. 个人实操体会与建议
做这类电商项目,最大的收获不是学会了某个具体技术,而是理解了“权衡”二字。缓存用多少级、库存怎么扣、事务要不要强一致,这些都没有标准答案,要根据业务场景来定。比如秒杀场景,库存扣减必须用Redis加Lua脚本,因为并发太高;普通商品下单,直接用数据库行锁也能接受,因为并发没那么大。
另一个体会是:文档和注释很重要。项目做完后,过几个月再看,如果没有注释,很多逻辑要重新捋。所以关键代码要写注释,接口要写文档,数据库表要有说明。可以用Swagger生成接口文档,用PowerDesigner或Navicat生成数据库文档。
最后分享一个小技巧:用Docker Compose管理依赖服务。把MySQL、Redis、RabbitMQ、Nginx的配置写成docker-compose.yml,一键启动所有依赖,省去手动安装配置的麻烦。开发环境用Docker,生产环境用Kubernetes,这是目前比较主流的做法。
这个项目后续还可以扩展很多方向:加商品评价模块、加优惠券模块、加秒杀模块、加推荐模块。每加一个模块,都会遇到新的技术挑战,也会学到新的东西。我的建议是先把主流程做扎实,再逐个扩展,不要贪多嚼不烂。