01 苏三商城项目介绍:从秒杀到分库分表,从可靠消息到可观测性的一体化工程实践
01 苏三商城项目介绍
如果你见过足够多的“商城项目”,你会发现真正拉开差距的从来不是功能清单,而是工程约束下的系统性能力:高峰流量如何“稳态吸收”,数据一致性如何“可证明”,关键链路如何“可观测与可恢复”,以及系统如何“可演进而非一次性”。
苏三商城的价值在于:它把这些能力拆成了可阅读、可复用、可演示的实现路径——你不仅能跑起来,更能把设计动机、取舍与边界条件讲清楚。
这篇文章不讲空泛概念,只讲你拿去就能讲清楚、写得出来、扛得住的工程细节。
目录
- 一图看懂:架构、技术栈、亮点
- 架构师视角:目标、约束与设计原则
- 亮点 1:秒杀链路不是“喊口号”,而是能跑的工程闭环
- 亮点 2:订单分库分表(ShardingSphere)真正落地,路由规则可解释、可扩展
- 亮点 3:消息与异步:不是发个 MQ 就完事,而是“可靠性 + 业务闭环”
- 亮点 4:搜索与缓存:ES 检索 + 全量同步兜底,工程上可持续
- 亮点 5:ID 生成更贴近线上:WorkerId 动态分配 + 心跳续期 + 冲突自愈
- 亮点 6:设计模式不炫技,专门用来降复杂度(策略模式:优惠券结算)
- 亮点 7:稳定性治理:幂等防重复提交 + 统一异常收敛
- 亮点 8:安全体系:JWT 认证链路可落到 SecurityContext
- 亮点 9:接口防刷与防篡改:IP 白名单 + 请求签名验签
- 亮点 10:数据合规:MyBatis 查询结果自动脱敏
- 亮点 11:存储抽象:一套 API 统一 MinIO / RustFS / 七牛
- 亮点 12:线程池隔离与异步编排:详情页埋点不影响主链路
- 亮点 13:MyBatis 拦截器数据填充:审计字段与主键生成下沉到持久层
- 亮点 14:百万级 Excel 导出:任务化、异步化、可通知,不阻塞主请求
- 亮点 15:数据源治理:动态数据源与 ShardingSphere 共存,不让分片改造撕裂全局
- 真实用户评价
- 加入星球
一图看懂:架构、技术栈、亮点
苏三商城系统架构图:

苏三商城系统使用技术:

苏三商城项目亮点:

部分页面截图








架构师视角:目标、约束与设计原则
工程上最难的不是“实现功能”,而是把系统放进真实约束里仍然成立。站在架构视角,商城域的关键挑战通常集中在四条主轴:
- 峰值弹性:活动/大促/秒杀带来强脉冲流量,系统需要有清晰的“削峰填谷”与“稳态退化”策略
- 一致性可证:订单、库存、支付、优惠券天然是跨资源写入,必须明确一致性模型(强一致/最终一致)与补偿策略
- 可观测可恢复:任何一个链路点出问题,都要能快速定位(观测),并能以最小爆炸半径恢复(隔离与补偿)
- 可演进:从单库单表走到分库分表、从同步调用走到消息化、从单体走到模块化,演进路径要有“台阶”而不是“断崖”
苏三商城围绕上述主轴做了工程落地:你不仅能看到方案,更能在代码里看到“为什么这么做”。
能力矩阵(架构 → 实现 → 可定位的代码入口):
| 能力域 | 典型问题 | 工程落地 | 关键入口 |
|---|---|---|---|
| 流量治理 | 高峰流量如何快速失败而不拖垮后端 | Redis Lua 限流(原子计数+过期) | LimitConfig.java |
| 并发一致性 | 多商品扣库存如何避免部分成功与死锁 | Redisson MultiLock 联锁 | RedissonUtil.java |
| 事务粒度 | 锁与事务如何组合,降低锁竞争放大 | 锁外校验 + TransactionTemplate 小事务扣减 | TradeService.java |
| 数据扩展 | 订单如何承载规模增长 | ShardingSphere 分库分表(按 code 路由) | application.yml:L80-L118,OrderDataBasePreciseShardingAlgorithm.java,OrderTablePreciseShardingAlgorithm.java |
| 数据源治理 | 如何让动态数据源与分片数据源共存 | 动态数据源 Provider 注入 ShardingSphereDataSource | ShardingConfig.java |
| 异步可靠性 | 延迟/异步链路如何显式化成功与失败 | RocketMQ 异步发送 + 回调 | MqHelper.java |
| 搜索兜底 | ES 如何长期不漂移 | 全量同步对齐(分页循环) | SyncProductService.java |
| ID 稳定性 | WorkerId 冲突如何检测与自愈 | Redis 心跳续期 + 冲突自愈 | WorkIdAllocator.java |
| 幂等防线 | 如何防止重复提交造成脏写 | AOP + Redis Key 去重 | RepeatSubmitAspect.java |
| 异常收敛 | 如何统一返回与错误分层 | 全局异常处理器 | GlobalExceptionHandler.java |
| 复杂度治理 | 优惠券/营销如何避免 if-else 失控 | 策略模式 + 启动期装配 | CouponContext.java,CustomizeApplicationListener.java |
| 安全认证 | 无状态登录如何落到权限上下文 | JWT Filter 写入 SecurityContext | JwtTokenFilter.java |
| 防篡改 | 内部接口如何抵御重放与伪造 | IP 白名单 + 时间戳签名验签 | VerifySignAspect.java,VerifySign.java |
| 数据合规 | 查询结果如何自动脱敏 | MyBatis Interceptor 脱敏注解驱动 | SensitiveInterceptor.java |
| 存储抽象 | 多种对象存储如何低成本切换 | 工厂 + 策略(优先级与降级) | FileStorageFactory.java,FileStorageService.java |
| 线程池隔离 | 非核心任务如何不影响主链路 | 自定义线程池 + CompletableFuture | ThreadPoolConfig.java,ProductService.java |
| 数据填充 | 创建人/修改人、主键如何自动补齐且不侵入业务层 | MyBatis Interceptor 拦截 Executor.update,向 DynamicContext 绑定用户与 ID | UserInterceptor.java |
| 异步导出 | 百万级 Excel 导出如何不拖慢请求且可回查结果 | 任务表 + MQ + 导出通知 + EasyExcel | ExcelExportTask.java,ExcelUtil.java |
读者画像:谁会从这个项目里赚到“技术红利”
- 想把“商城系统”从 CRUD 提升到“高并发与稳定性工程”的同学
- 想补齐分库分表、MQ、ES、缓存一致性、限流与幂等等能力拼图的后端
- 面试被问到“你们怎么防超卖/怎么做订单分片/消息丢了怎么办”的同学
- 技术负责人:需要一套能给团队做 code review、做训练、做演进路线的工程样板
用户真实使用场景(你会非常有代入感)
“我们把项目里的秒杀链路按模块拆开走了一遍:限流 → 分布式锁 → 事务扣减 → MQ 异步,最后把库存一致性和失败补偿讲清楚了,面试官追问都能接得住。”
“以前说分库分表只会背概念,这次直接拿订单
code的分片算法讲路由,扩容怎么做、迁移怎么做、查询怎么改,一套说下来很稳。”
“我最喜欢的是项目的‘工程化味道’:线程池隔离、统一异常、策略模式把 if-else 打散,代码读起来很像线上的东西。”
亮点 1:秒杀链路不是“喊口号”,而是能跑的工程闭环
秒杀最怕三件事:扛不住、超卖、不可恢复。项目里把这三件事拆解成可验证的设计:
- 分布式限流(Redis + Lua):把“计数、判断、过期”放在 Redis 端原子执行,避免并发竞态
参考实现:LimitConfig.java - 分布式锁(Redisson MultiLock):订单包含多个商品时,用联锁一次锁住所有库存资源,避免部分扣减与死锁风险
参考实现:RedissonUtil.java - 事务扣减(TransactionTemplate):锁外开事务、事务内扣减,控制事务粒度,降低锁占用与故障放大
参考调用点:TradeService.java
秒杀链路(图解):

关键代码片段:Redis Lua 限流脚本(原子计数 + 过期)
return "local key = KEYS[1]\n" +
"local count = tonumber(ARGV[1])\n" +
"local time = tonumber(ARGV[2])\n" +
"local current = redis.call('get', key);\n" +
"if current == nil then\n current = 0\n end\n" +
"if current and tonumber(current) > count then\n" +
" return tonumber(current);\n" +
"end\n" +
"current = redis.call('incr', key)\n" +
"if tonumber(current) == 1 then\n" +
" redis.call('expire', key, time)\n" +
"end\n" +
"return tonumber(current);";关键代码片段:MultiLock 把“多商品下单”变成“要么全锁要么不锁”
public <T> T tryMultiLock(List<String> keys, long waitTime, long leaseTime, Supplier<T> supplier) {
RLock[] rLocks = new RLock[keys.size()];
for (int i = 0; i < keys.size(); i++) {
rLocks[i] = redissonClient.getLock(keys.get(i));
}
RedissonMultiLock redissonMultiLock = new RedissonMultiLock(rLocks);
String collectKey = keys.stream().collect(Collectors.joining());
return doTryLock(redissonMultiLock, collectKey, waitTime, leaseTime, supplier);
}关键代码片段:扣减库存“锁 + 小事务”组合拳(防超卖、控粒度)
private TradeEntity reduceStock(TradeEntity tradeEntity) {
List<String> keys = getLockKey(tradeEntity);
redissonUtil.tryMultiLock(keys, reduceStockLockSeconds, reduceStockLockSeconds, () -> {
checkProductAndStock(tradeEntity);
transactionTemplate.execute(status -> {
for (TradeItemEntity tradeItemEntity : tradeEntity.getTradeItemEntityList()) {
productMapper.reduceStock(tradeItemEntity.getProductId(), tradeItemEntity.getQuantity());
}
return Boolean.TRUE;
});
return tradeEntity;
});
return tradeEntity;
}你拿这个链路去讲“为什么这样设计”“哪个点防超卖”“失败怎么补偿”,会非常有底气。
亮点 2:订单分库分表(ShardingSphere)真正落地,路由规则可解释、可扩展
项目里订单相关逻辑表配置为分库分表:
order_trade、order_trade_item、order_trade_delivery_address- 实际节点:
ds0/ds1两库、每库两表 - 分片键:订单号
code
配置一眼就能看懂:application.yml(shardingsphere tables):L63-L133
更关键的是:你能把路由规则讲清楚——不是“用 ShardingSphere 了”,而是“为什么会落到这张表”:
- 分库算法:
OrderDataBasePreciseShardingAlgorithm.java - 分表算法:
OrderTablePreciseShardingAlgorithm.java
关键代码片段:订单分库分表配置(只展示 tables 片段)
shardingsphere:
sharding:
tables:
order_trade:
actual-data-nodes: ds${0..1}.order_trade_${0..1}
database-strategy:
standard:
sharding-column: code
precise-algorithm-class-name: cn.net.susan.sharding.OrderDataBasePreciseShardingAlgorithm
table-strategy:
standard:
shardingColumn: code
preciseAlgorithmClassName: cn.net.susan.sharding.OrderTablePreciseShardingAlgorithm关键代码片段:分库路由(按订单号 hash 取模)
public String doSharding(Collection<String> collection, PreciseShardingValue<String> preciseShardingValue) {
String orderCode = preciseShardingValue.getValue();
int hashCode = orderCode.hashCode();
if (hashCode < 0) {
hashCode = 0 - hashCode;
}
String dataSourceName = "ds" + (hashCode % 2);
for (String name : collection) {
if (name.endsWith(String.valueOf(hashCode % 2))) {
return name;
}
}
return dataSourceName;
}关键代码片段:分表路由(逻辑表名 + 后缀)
public String doSharding(Collection<String> collection, PreciseShardingValue<String> preciseShardingValue) {
String orderCode = preciseShardingValue.getValue();
int hashCode = orderCode.hashCode();
if (hashCode < 0) {
hashCode = 0 - hashCode;
}
String logicTableName = preciseShardingValue.getLogicTableName();
String physicalTableName = logicTableName + "_" + (hashCode % 2);
return physicalTableName;
}这意味着你不仅能做“新系统分片”,还可以把“单表迁移分片、历史数据迁移、双写/CDC、回滚”讲到工程细节。
面试官常追问的“扩容与演进”也可以从这里自然展开:
- 当
2个分片无法满足规模增长时,如何从N=2平滑扩到N=4(路由策略、数据迁移、双写/CDC、校验与回滚) - 当你既想按用户查询友好,又想按订单号定位快速,如何通过“订单号基因位”做双维度可路由设计
亮点 3:消息与异步:不是发个 MQ 就完事,而是“可靠性 + 业务闭环”
项目同时使用 RabbitMQ / RocketMQ 覆盖不同业务场景,并提供“可观测、可补偿”的发送方式:
- RocketMQ 延迟消息异步发送 + 回调:成功/失败都可捕获并做告警或补偿
参考实现:MqHelper.java - 动态任务调度(Quartz + MQ 驱动):把“任务配置变更”通过消息下发,调度侧实时更新 cron
参考实现:QuartzManage.java
消费入口:DynamicJobConsumer.java
关键代码片段:RocketMQ 延迟消息异步发送(回调明确成功/失败)
MessageHeaders headers = new MessageHeaders(
Collections.singletonMap(MessageConst.PROPERTY_DELAY_TIME_LEVEL, String.valueOf(delayLevel))
);
org.springframework.messaging.Message<Object> message = MessageBuilder.createMessage(data, headers);
rocketMQTemplate.asyncSend(topic, message, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
log.info("延迟消息发送成功, topic:{},message:{}", topic, data);
}
@Override
public void onException(Throwable throwable) {
log.error("延迟消息发送失败, topic:{},throwable:{}", topic, throwable);
}
}, 3000, delayLevel);这类工程化能力非常适合写进你的简历“项目亮点”里:因为它不是概念,是链路。
亮点 4:搜索与缓存:ES 检索 + 全量同步兜底,工程上可持续
项目里商品检索走 ES,并且提供了同步服务做全量对齐:
- ES 客户端封装:
EsTemplate.java - 商品同步到 ES(全量循环分页、逐条补齐统计后写入):
SyncProductService.java
关键代码片段:全量同步的“分页循环 + 逐条写入”,确保可持续对齐
public void syncProductToES() {
handleInsertOrUpdate();
handleDelete();
}
private void handleInsertOrUpdate() {
ProductConditionEntity productConditionEntity = new ProductConditionEntity();
productConditionEntity.setPageSize(NumberConstant.NUMBER_500);
productConditionEntity.setIsDel(0);
ResponsePageEntity<ProductEntity> productEntityResponsePageEntity = productService.searchByPage(productConditionEntity);
while (CollectionUtils.isNotEmpty(productEntityResponsePageEntity.getData())) {
saveData(productEntityResponsePageEntity.getData());
productConditionEntity.setPageNo(productConditionEntity.getPageNo() + 1);
productEntityResponsePageEntity = productService.searchByPage(productConditionEntity);
}
}关键代码片段:写入 ES 前补齐统计字段(销量/好评率),让搜索结果更“业务化”
for (ProductWebEntity productWebEntity : dataList) {
statSaleCount(productWebEntity);
statPositiveRating(productWebEntity);
esTemplate.insertOrUpdate(businessConfig.getProductEsIndexName(), productWebEntity);
}这套方案的价值在于:你能解释“为什么要全量兜底”“如何避免数据漂移”“如何做最终一致性”,而不是停留在“用了 ES”。
亮点 5:ID 生成更贴近线上:WorkerId 动态分配 + 心跳续期 + 冲突自愈
雪花算法的坑,很多人只会背“时钟回拨”。项目里直接把 WorkerId 的工程化分配做出来:
- WorkerId 动态分配、Redis 心跳续期、冲突检测与自愈
参考实现:WorkIdAllocator.java
关键代码片段:心跳续期 + 冲突检测(发现 workerId 被抢占则自愈)
private void doHeartBeat() {
String workerKey = getWorkerKey(snowFlaskWorkerId);
String value = redisTemplate.opsForValue().get(workerKey);
if (!uuid.equals(value)) {
log.error("雪花算法雪花WorkerId失效,workerId={}已失效或发生冲突, 尝试从新获取雪花Id修复问题", snowFlaskWorkerId);
tryResumeWorkerId();
} else {
redisTemplate.expire(workerKey, ttl, TimeUnit.SECONDS);
log.info("心跳成功:雪花算法workerId={}, ttl={}, redisKey={},redisValue={}", snowFlaskWorkerId, ttl, workerKey, uuid);
}
}这部分是典型“讲出来就显得你做过线上”的细节。
亮点 6:设计模式不炫技,专门用来降复杂度(策略模式:优惠券结算)
优惠券是最容易 if-else 爆炸的地方。项目里用策略模式把优惠类型解耦:
- 策略接口:
ICouponStrategy.java - 策略路由上下文:
CouponContext.java - 启动期自动装配策略 Map(无侵入扩展):
CustomizeApplicationListener.java
关键代码片段:启动期扫描所有策略实现,自动构建路由表
Map<String, ICouponStrategy> beansWithMap = applicationContext.getBeansOfType(ICouponStrategy.class);
int initSize = MapUtils.isEmpty(beansWithMap) ? 0 : beansWithMap.size();
Map<CouponTypeEnum, ICouponStrategy> handlerMap = new HashMap<>(initSize);
if (MapUtils.isNotEmpty(beansWithMap)) {
beansWithMap.forEach((beanName, couponStrategy) -> {
handlerMap.put(couponStrategy.getType(), couponStrategy);
});
CouponContext.getInstance().initMap(handlerMap);
}关键代码片段:结算时按类型路由到对应策略(把 if-else 变成 Map 查找)
public BigDecimal calcPayMoney(BigDecimal money, CouponWebEntity couponEntity) {
CouponTypeEnum couponTypeEnum = getCouponTypeEnum(couponEntity);
if (Objects.isNull(couponTypeEnum)) {
return money;
}
ICouponStrategy couponStrategy = couponStrategyMap.get(couponTypeEnum);
AssertUtil.notNull(couponStrategy, "该优惠券类型不存在");
return couponStrategy.calcPayMoney(money, couponEntity);
}你新增一种优惠类型,只要新增一个实现类,不需要改结算主流程。
亮点 7:稳定性治理:幂等防重复提交 + 统一异常收敛
“稳定性”不是一套组件,而是系统对非预期行为的防线:重复提交、参数非法、权限问题、业务异常与系统异常,都需要被显式建模并统一收敛,否则系统会在高峰期以最脆弱的方式失效。
关键代码片段:通过 AOP + Redis 做请求级幂等(防重复提交)
String key = getKey(requestURI, clientIp, params);
String value = redisUtil.get(key);
if (StringUtils.hasLength(value)) {
throw new BusinessException("该用户请求已存在,请勿重复提交");
}
int second = method.getAnnotation(RepeatSubmit.class).second();
redisUtil.set(key, REPEAT_SUBMIT_DEFAULT_KEY, second);参考实现:RepeatSubmitAspect.java
关键代码片段:统一异常收敛(业务异常、权限异常、参数校验与系统异常分层处理)
@ExceptionHandler(Throwable.class)
public ApiResult handleException(Throwable e) {
if (e instanceof BusinessException) {
BusinessException businessException = (BusinessException) e;
return ApiResultUtil.error(businessException.getCode(), businessException.getMessage());
} else if (e instanceof AccessDeniedException) {
return ApiResultUtil.error(HttpStatus.FORBIDDEN.value(), "无权限访问,请联系系统管理员!");
} else if (e instanceof MethodArgumentNotValidException) {
MethodArgumentNotValidException message = (MethodArgumentNotValidException) e;
BindingResult bindingResult = message.getBindingResult();
if (bindingResult.hasErrors()) {
return ApiResultUtil.error(HttpStatus.BAD_REQUEST.value(), bindingResult.getFieldError().getDefaultMessage());
}
}
return ApiResultUtil.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), "服务器内部错误,请联系系统管理员!");
}参考实现:GlobalExceptionHandler.java
亮点 8:安全体系:JWT 认证链路可落到 SecurityContext
很多项目“写了 JWT”,但链路经不起追问:token 从哪里来、如何解析、如何把用户身份注入到权限上下文、异常怎么统一处理。这里的做法比较工程化:过滤器层完成 token 获取与校验,把认证信息写入 SecurityContextHolder,让后续授权链路保持一致。
关键代码片段:从请求获取 token,并将认证写入 SecurityContext
String token = TokenUtil.getTokenForAuthorization(httpServletRequest);
String username = tokenHelper.getUsernameFromToken(token);
if (StringUtils.hasLength(username) && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = tokenHelper.getUserDetailsFromUsername(username);
if (Objects.nonNull(userDetails)) {
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}参考实现:JwtTokenFilter.java
亮点 9:接口防刷与防篡改:IP 白名单 + 请求签名验签
对内接口或关键操作接口,仅靠“登录态”往往不够。工程上常见的做法是:入口收紧(IP 白名单)+ 请求防篡改(时间戳签名),把攻击成本从“随手一调”提升到“必须拿到密钥并通过验签窗口”。
关键代码片段:注解驱动的验签切面(IP 校验 + timestamp + sign)
if (!ipWhiteListHelper.checkIp()) {
throw new BusinessException("非法请求");
}
Object argValue = argValues[0];
if (!(argValue instanceof SignEntity)) {
throw new BusinessException("请求参数错误");
}
SignEntity signEntity = (SignEntity) argValue;
AssertUtil.notNull(signEntity.getTimestamp(), "timestamp不能为空");
AssertUtil.isTrue(StringUtils.hasLength(signEntity.getSign()), "sign不能为空");
SignUtil.checkSign(signEntity, secretKey);参考实现:VerifySignAspect.java,VerifySign.java
亮点 10:数据合规:MyBatis 查询结果自动脱敏
“数据脱敏”最怕两件事:业务开发忘记处理、不同接口用不同规则导致不一致。这里用 MyBatis 拦截器把脱敏能力下沉到基础设施层:只要字段上标注脱敏注解,查询结果会自动转换,降低人为遗漏风险。
关键代码片段:Executor.query 拦截后,对返回对象字段执行脱敏
Object result = invocation.proceed();
if (result instanceof List) {
for (Object obj : (List<?>) result) {
doSensitiveFields(obj);
}
} else if (result instanceof Map) {
for (Object obj : ((Map<?, ?>) result).values()) {
doSensitiveFields(obj);
}
} else {
doSensitiveFields(result);
}
return result;参考实现:SensitiveInterceptor.java
亮点 11:存储抽象:一套 API 统一 MinIO / RustFS / 七牛
线上系统通常要面对“存储切换、成本优化、地域迁移、私有化部署”等现实需求。这里的设计不是把存储写死,而是通过统一接口与工厂选择,做到“实现可替换、切换可回滚、能力可扩展”。
关键代码片段:工厂初始化时按优先级自动选择可用存储(并提供降级路径)
if (isRustFsConfigured()) {
activeStorageService = rustFsFileStorageService;
activeOssType = OssTypeEnum.RUSTFS;
return;
}
if (Objects.equals(OssTypeEnum.MINIO.getValue(), configuredOssType) && isMinioConfigured()) {
activeStorageService = minioFileStorageService;
activeOssType = OssTypeEnum.MINIO;
return;
}
if (Objects.equals(OssTypeEnum.QINIU.getValue(), configuredOssType) && isQiNiuConfigured()) {
activeStorageService = qiNiuFileStorageService;
activeOssType = OssTypeEnum.QINIU;
return;
}参考实现:FileStorageFactory.java,接口:FileStorageService.java
亮点 12:线程池隔离与异步编排:详情页埋点不影响主链路
电商系统里“主链路”与“旁路任务”必须隔离:例如商品详情页的浏览记录属于旁路,如果它阻塞/失败,不应该影响详情页响应。这里用 CompletableFuture + 自定义线程池 做隔离:旁路异步化,线程资源独立,避免默认线程池被挤爆。
关键代码片段:业务侧异步埋点(明确使用自定义线程池执行)
JwtUserEntity userEntity = FillUserUtil.getCurrentUserInfoOrNull();
CompletableFuture.runAsync(() -> {
FillUserUtil.mockUser(() -> {
saveViewRecord(userEntity, productId);
return null;
}, userEntity);
}, productDetailThreadPoolExecutor);关键代码片段:线程池参数可配置(核心/最大线程数、队列、拒绝策略)
ThreadPoolExecutor threadPoolTaskExecutor = new ThreadPoolExecutor(
productDetailThreadPoolPoolConfig.getCorePoolSize(),
productDetailThreadPoolPoolConfig.getMaxPoolSize(),
productDetailThreadPoolPoolConfig.getKeepAliveSeconds(),
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(productDetailThreadPoolPoolConfig.getQueueSize()),
new ThreadPoolExecutor.CallerRunsPolicy()
);参考实现:ProductService.java:L170-L225,ThreadPoolConfig.java
亮点 13:MyBatis 拦截器数据填充:审计字段与主键生成下沉到持久层
很多项目的“创建人 / 修改人 / 主键 ID”最后都变成了业务代码里手工 set,一旦新增 Mapper、批量写入、多人协作,重复代码和漏填问题就会同时出现。这里把数据填充能力直接下沉到 MyBatis 执行层,在 Executor.update 链路统一处理:
- 拦截写操作入口:通过 MyBatis
Interceptor统一拦截insert / update,避免每个 Service、Mapper 手工补字段 - 动态绑定审计字段:在 SQL 执行前向
DynamicContext注入CURRENT_USER_ID、CURRENT_USER_NAME - 插入场景顺带生成主键:
insert时额外绑定GENERATE_ID,把主键生成一起下沉到基础设施层 - 业务代码更干净:业务层只关心领域对象和 SQL 语义,不需要反复写“谁创建、谁修改、ID 怎么来”的样板代码
关键代码片段:拦截 Executor.update,只对当前 MappedStatement 的动态 SQL 做代理增强
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
public class UserInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object target = invocation.getTarget();
if (target instanceof Executor) {
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
Collection<MappedStatement> mappedStatements = mappedStatement.getConfiguration().getMappedStatements();
Iterator<MappedStatement> iterator = mappedStatements.iterator();
while (iterator.hasNext()) {
Object object = iterator.next();
if (object instanceof MappedStatement) {
MappedStatement objectMappedStatement = (MappedStatement) object;
if (!objectMappedStatement.getId().equals(mappedStatement.getId())) {
continue;
}
}
}
}
return invocation.proceed();
}
}关键代码片段:向 DynamicContext 绑定当前用户和生成好的主键
DynamicContext context = (DynamicContext) args[0];
JwtUserEntity currentUserInfo = FillUserUtil.getCurrentUserInfoOrNull();
if (Objects.nonNull(currentUserInfo)) {
context.bind(CURRENT_USER_ID, currentUserInfo.getId());
context.bind(CURRENT_USER_NAME, currentUserInfo.getUsername());
}
if (isInsert) {
IdGenerateHelper idGenerateHelper = SpringUtil.getBean(IdGenerateHelper.class);
context.bind(GENERATE_ID, idGenerateHelper.nextId());
}参考实现:UserInterceptor.java
亮点 14:百万级 Excel 导出:任务化、异步化、可通知,不阻塞主请求
很多后台系统一说“导出 Excel”,最后实现成了同步接口直接查库、组装、写文件,数据一大就卡接口、吃内存、拖垮线程池。这里把导出做成了真正可上线的工程化方案:
- 任务化:先落任务表,导出状态清晰可追踪
- 异步化:通过 MQ 触发导出,避免导出过程阻塞主请求
- 结果可通知:导出成功后自动生成通知消息,把下载结果推给创建任务的用户
- 实现可替换:底层封装成
ExcelUtil,既支持直接流式响应,也支持服务端落盘
关键代码片段:导出任务异步执行,成功后回写文件地址与状态
String serviceName = this.getServiceName(requestEntity);
BaseService baseService = (BaseService) SpringBeanUtil.getBean(serviceName);
String fileName = getFileName(excelBizTypeEnum.getDesc());
String fileUrl = baseService.export(toBean, fileName, this.getEntityName(requestEntity));
commonTaskEntity.setFileUrl(fileUrl);
commonTaskEntity.setStatus(TaskStatusEnum.SUCCESS.getValue());关键代码片段:导出完成后发送通知
CommonNotifyEntity commonNotifyEntity = transactionTemplate.execute((status) -> {
commonTaskMapper.update(commonTaskEntity);
return saveNotifyMessage(commonTaskEntity);
});
mqHelper.send(excelExportTopic, commonNotifyEntity);关键代码片段:EasyExcel 落盘导出
String downloadName = TEMP_FILE_PATH + fileName + ".xlsx";
File file = new File(downloadName);
FileOutputStream fileOutputStream = new FileOutputStream(file);
EasyExcel.write(fileOutputStream, clazz)
.sheet(fileName).doWrite(data);参考实现:ExcelExportTask.java,ExcelUtil.java
亮点 15:数据源治理:动态数据源与 ShardingSphere 共存,不让分片改造撕裂全局
很多项目一旦上了分库分表,就会出现一个很现实的问题:不是所有表、所有模块都需要立刻切到分片数据源。如果分片改造必须“一刀切”,那业务改造成本、回归范围和故障半径都会被放大。这里把动态数据源和 ShardingSphere 做了统一编排,让系统可以渐进演进,而不是整体推倒重来。
- 分片数据源纳入统一入口:把
shardingSphereDataSource注入动态数据源容器,和普通业务数据源一起管理 - 按业务显式切换:需要走分片订单库的方法,直接通过
@DS("sharding")精确声明 - 兼容渐进式改造:未分片模块继续走原有数据源,分片模块逐步迁移,降低一次性重构风险
- 工程可维护:数据源切换规则清晰,后续接主从、脱敏、影子库时也有统一扩展点
关键代码片段:把 ShardingSphere 数据源挂入动态数据源体系
@Bean
public DynamicDataSourceProvider dynamicDataSourceProvider() {
Map<String, DataSourceProperty> datasourceMap = properties.getDatasource();
return new AbstractDataSourceProvider() {
@Override
public Map<String, DataSource> loadDataSources() {
Map<String, DataSource> dataSourceMap = createDataSourceMap(datasourceMap);
dataSourceMap.put("sharding", shardingSphereDataSource);
return dataSourceMap;
}
};
}关键代码片段:订单查询与下单链路按需切到分片数据源
@DS("sharding")
public TradeEntity findById(Long id) {
return tradeMapper.findById(id);
}
@DS("sharding")
public void createOrderTrade(TradeEntity tradeEntity) {
JwtUserEntity currentUserInfo = FillUserUtil.getCurrentUserInfo();
checkOrderTrade(tradeEntity);
tradeEntity.setOrderType(OrderTypeEnum.NORMAL_PRODUCT.getValue());
fillTradeItemEntity(tradeEntity);
tradeSaveService.createTrade(currentUserInfo, tradeEntity);
}参考实现:ShardingConfig.java,TradeService.java
其实项目中有几十个技术亮点,上面只给大家列举了一部分。
完整的苏三商城项目中亮点更多。
真实用户评价
如果你关心的不只是“源码能不能跑”,而是“学完之后能不能真正补齐项目经验、面试表达和简历说服力”,下面这组内容就是商城项目已有的真实用户反馈截图。为了保护隐私,这里直接沿用匿名化截图展示。




最适合用它做的五件事
- 写简历项目亮点:秒杀防超卖、订单分片路由、动态数据源治理、MQ 延迟与可靠性、ES 同步兜底、动态 WorkerId、动态用户填充、百万级 Excel 导出等等。
- 找实习:项目链路完整、技术点够硬,既能补足在校项目经历,也更方便你在面试中讲出有说服力的工程细节。
- 补齐从 0~1 的完整项目经历:不仅有后端,还覆盖管理后台、小程序、部署、联调和上线思路,更接近真实企业项目的完整交付过程。
- 准备大厂面试:每个模块都能追问到实现细节,形成“可辩护的项目经历”。
- 做团队代码训练:把项目当样板做 code review,形成你团队自己的工程规范。
加入星球
如果你想要的不只是“看完文章觉得项目很强”,而是直接拿到完整源码、配套教程、答疑支持和简历包装思路,那么加入 Java突击队 星球会更直接。
苏三商城项目只是其中一个代表性项目,真正的价值是把多个实战项目、技术专题和求职支持打包到同一个学习闭环里。