肖哥弹架构 跟大家“弹弹” Spring MVC设计与实战应用,需要代码关注
欢迎 点赞,关注,转发。
关注公号Solomon肖哥弹架构获取更多精彩内容
历史热点文章
- MyCat应用实战:分布式数据库中间件的实践与优化(篇幅一)
- 图解深度剖析:MyCat 架构设计与组件协同 (篇幅二)
- 一个项目代码讲清楚DO/PO/BO/AO/E/DTO/DAO/ POJO/VO
- 写代码总被Dis:5个项目案例带你掌握SOLID技巧,代码有架构风格
- 里氏替换原则在金融交易系统中的实践,再不懂你咬我
“当你的支付系统出现金额错乱,当你的秒杀商品遭遇超卖,90%的问题都源于事务隔离级别的错误选择!”
本文将为你彻底解密Spring MVC中的事务隔离机制,通过电商、金融等真实案例。
🔥 精准选择隔离级别:
- 4大隔离级别深度对比(附原理时序图)
- 20+生产环境代码示例
- 高并发场景下的性能与一致性平衡术
💎 核心内容:
✅ 从READ UNCOMMITTED到SERIALIZABLE的完整应用场景
✅ 电商系统全链路隔离级别实战(订单/库存/支付)
✅ 隔离级别导致的6大经典生产事故复盘
✅ Spring事务注解的高级配置技巧
🚀 性能优化亮点:
- 监控大屏:如何用READ UNCOMMITTED实现毫秒级响应
- 秒杀系统:READ COMMITTED+乐观锁的完美搭配
- 财务对账:REPEATABLE READ的分批处理技巧
- 银行转账:SERIALIZABLE的行级锁优化方案
一、事务隔离架构时序图
1. 关键组件说明
-
客户端应用
- 通过JDBC/ORM框架(如MyBatis)操作数据库
- 设置事务边界和隔离级别
// Spring示例 @Transactional(isolation = Isolation.READ_COMMITTED) public void businessMethod() { // 业务代码 }
-
连接池
-
管理物理连接(如HikariCP)
-
关键配置参数:
spring: datasource: hikari: isolation-level: READ_COMMITTED # 默认隔离级别 max-lifetime: 180000 # 连接存活时间(ms)
-
-
MySQL服务层
-
事务协调器(Transaction Coordinator)
-
隔离级别实现逻辑:
-- 查看当前隔离级别 SELECT @@transaction_isolation; -- 设置全局/会话级别 SET GLOBAL transaction_isolation = 'REPEATABLE-READ'; SET SESSION transaction_isolation = 'READ-COMMITTED';
-
二、 事务如何选择?
// 根据业务动态选择隔离级别
@Transactional(isolation =
systemConfig.isHighConsistency() ?
Isolation.SERIALIZABLE :
Isolation.READ_COMMITTED
)
public void businessOperation() {
// ...
}
关键原则:
-
默认使用数据库的默认级别(MySQL默认为
REPEATABLE_READ
) -
在以下情况升级隔离级别:
- 涉及金钱操作 →
SERIALIZABLE
- 报表生成 →
REPEATABLE_READ
- 涉及金钱操作 →
-
在以下情况降级隔离级别:
- 允许短暂不一致的统计 →
READ_UNCOMMITTED
- 高并发写入场景 →
READ_COMMITTED
- 允许短暂不一致的统计 →
三、掌握事务5种隔离策略
1. READ_UNCOMMITTED
(读未提交)
原理图(实时监控场景)
适用场景特点:对数据准确性要求低,但对实时性要求极高的非关键业务
业务场景
// 实时监控系统(允许脏读)
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public BigDecimal getRealtimeSales() {
// 实时统计销售额(允许读到未提交的临时数据)
return salesDao.sumUncommittedOrders();
}
必须使用场景:
-
实时监控大屏
- 如物流仓库的温度监控大屏,需要每秒刷新数据,短暂显示异常值可接受
- 必要性:牺牲准确性换取毫秒级延迟,若用更高级别会导致数据延迟明显
-
网站实时在线人数
- 显示"当前正在浏览人数"的近似值
- 必要性:用户理解这是估算值,系统可承受±5%误差
-
游戏实时排行榜
- 玩家战力榜每分钟自动刷新
- 必要性:允许排名短暂跳动,避免锁竞争导致卡顿
-
IoT设备状态看板
- 展示工厂设备实时运行状态(如转速、电压)
- 必要性:设备状态变化极快,需优先保证刷新速度
-
社交媒体在线状态
- 显示好友"2分钟前在线"的提示
- 必要性:状态延迟比短暂错误更影响体验
错误用法后果
// 错误:在支付系统中使用READ_UNCOMMITTED
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void checkBalance(Long userId) {
BigDecimal balance = accountDao.getBalance(userId); // 可能读到未提交的脏数据
if (balance.compareTo(price) < 0) {
throw new RuntimeException("余额不足"); // 可能误判
}
}
后果:用户看到错误的余额导致支付失败投诉。
2. READ_COMMITTED
(读已提交)
原理图(库存扣减场景)
适用场景特点:需要看到最新已确认数据,但允许结果变化的常规业务
业务场景
// 电商订单处理(避免脏读)
@Transactional(isolation = Isolation.READ_COMMITTED)
public void processOrder(Long orderId) {
Order order = orderDao.findById(orderId); // 读到已提交数据
if (order.getStatus().equals("PAID")) {
inventoryDao.deduct(order.getProductId()); // 扣减库存
}
}
必须使用场景:
-
电商订单支付
- 支付成功后更新订单状态
- 必要性:必须确保状态是已确认的,但支付记录可分多次查询
-
银行ATM余额查询
- 查询卡内可用余额
- 必要性:需显示最新确认金额,但允许查询后立即发生转账
-
机票预订系统
- 用户查询航班余票
- 必要性:需反映实时库存,但两次搜索间可能减少
-
新闻评论审核
- 展示已通过审核的评论
- 必要性:避免显示未审核内容,但新评论可实时增加
-
库存预占系统
- 下单前检查商品库存
- 必要性:读取已确认库存,但允许后续并发扣减
错误用法后果
// 错误:在对账系统中使用READ_COMMITTED
@Transactional(isolation = Isolation.READ_COMMITTED)
public void reconcile() {
BigDecimal start = salesDao.getDailyTotal(); // 第一次查询
// ...其他操作
BigDecimal end = salesDao.getDailyTotal(); // 第二次查询结果可能不同
generateReport(start, end); // 报表数据不一致
}
后果:财务对账不平,需人工干预核查。
3. REPEATABLE_READ
(可重复读)
原理图(财务对账场景)
适用场景特点:需要事务内多次读取结果一致的长期操作
业务场景
// 生成对账单(保证多次读取一致)
@Transactional(isolation = Isolation.REPEATABLE_READ)
public Report generateDailyReport() {
BigDecimal morningSales = salesDao.getTotalSales(); // 时间点快照
// ...其他操作
BigDecimal eveningSales = salesDao.getTotalSales(); // 与morningSales一致
return new Report(morningSales, eveningSales);
}
必须使用场景:
-
财务月末对账
- 生成银行账户日终对账单
- 必要性:对账期间账户余额必须冻结
-
数据迁移工具
- 将旧系统数据迁移到新库
- 必要性:迁移过程需要数据快照一致性
-
销售月报生成
- 统计月度销售数据生成PDF
- 必要性:报表需反映统计开始时的完整状态
-
购物车价格计算
- 结算时计算商品总价
- 必要性:防止价格在计算中途被修改
-
数据库备份
- 生产环境在线热备份
- 必要性:备份文件需要保持数据关联一致性
错误用法后果
// 错误:在实时库存检查中使用REPEATABLE_READ
@Transactional(isolation = Isolation.REPEATABLE_READ)
public boolean checkStock(Long productId) {
Integer stock = inventoryDao.getStock(productId); // 快照读
// 其他服务此时可能已修改库存
return stock > 0; // 返回过期数据
}
后果:超卖问题,实际库存不足但系统允许下单。
4. SERIALIZABLE
(串行化)
原理图(银行转账场景)
适用场景特点:不允许任何并发冲突的关键业务
业务场景
// 银行转账(绝对隔离)
@Transactional(isolation = Isolation.SERIALIZABLE)
public void transfer(Long fromId, Long toId, BigDecimal amount) {
Account from = accountDao.findById(fromId);
Account to = accountDao.findById(toId);
if (from.getBalance().compareTo(amount) >= 0) {
accountDao.deduct(fromId, amount);
accountDao.add(toId, amount); // 其他事务无法并发修改这两个账户
}
}
必须使用场景:
-
银行转账系统
- 从账户A向账户B转账
- 必要性:必须绝对防止并发操作导致金额计算错误
-
机票选座系统
- 用户锁定航班座位
- 必要性:避免两个用户同时选择同一座位
-
医院处方系统
- 医生开具药品并扣库存
- 必要性:确保麻醉类药品不会超发
-
拍卖系统结标
- 确定最终中标者
- 必要性:防止多个出价同时被确认为获胜者
-
区块链交易打包
- 验证并打包新区块交易
- 必要性:交易顺序必须严格串行化记录
错误用法后果
// 错误:在商品列表查询中使用SERIALIZABLE
@Transactional(isolation = Isolation.SERIALIZABLE)
public List<Product> listProducts() {
return productDao.findAll(); // 全表加锁
}
后果:
- 系统并发性能骤降
- 用户浏览商品时卡顿超时
四、 事务隔离对比总结表
隔离级别 | 原理简述 | 必须使用的业务场景 | 代码注解提示 |
---|---|---|---|
READ_UNCOMMITTED | 允许读取未提交数据 | 实时监控/大数据看板 | // 允许脏读,性能优先 |
READ_COMMITTED | 只读已提交数据 | 订单状态更新/余额查询 | // 避免脏读,允许不可重复读 |
REPEATABLE_READ | 快照读(MVCC实现) | 财务对账/数据导出 | // 保证同一事务内读取一致性 |
SERIALIZABLE | 全表锁(完全串行) | 银行转账/库存核销 | // 绝对隔离,性能代价高 |
五、事务隔离优化方案
1. READ UNCOMMITTED(读未提交)优化
问题:数据可能脏读,但要求极快响应
优化口诀: “非关键数据,缓存兜底”
具体方法:
- 给监控大屏加Redis缓存层,设置1秒自动过期:
---------------------物流实时位置大屏
@Service
public class LogisticsService {
// 使用Redis缓存降低数据库压力
@Cacheable(value = "vehicle_positions", key = "#vehicleId", ttl = 1) // 1秒过期
public Position getCurrentPosition(String vehicleId) {
// 注意:这里故意不使用事务,允许读取未提交数据
return jdbcTemplate.queryForObject(
"SELECT latitude, longitude FROM vehicle_positions WHERE vehicle_id=?",
(rs, rowNum) -> new Position(rs.getDouble(1), rs.getDouble(2)),
vehicleId
);
}
// 异步修正脏数据
@Scheduled(fixedRate = 5000) // 每5秒执行一次
public void fixDirtyData() {
List<Position> dirtyPositions = getDirtyReads();
dirtyPositions.forEach(pos -> {
Position committedPos = getCommittedPosition(pos.getVehicleId());
if (!pos.equals(committedPos)) {
redisTemplate.opsForValue().set(
"vehicle_positions::" + pos.getVehicleId(),
committedPos
);
}
});
}
}
2. READ COMMITTED(读已提交)优化
问题:可能不可重复读,但并发要高
优化口诀: “短平快,乐观锁”
具体方法:
- 把下单事务拆成多个小事务:
- 用版本号避免锁等待:
-- 商品表添加version字段
UPDATE products
SET stock=stock-1, version=version+1
WHERE id=100 AND version=5;
----------------------电商订单支付
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderDao orderDao;
private final InventoryDao inventoryDao;
// 短事务+乐观锁控制
@Transactional(isolation = Isolation.READ_COMMITTED)
public void payOrder(Long orderId) {
// 阶段1:快速校验
Order order = orderDao.findByIdForUpdate(orderId);
if (order.getStatus() != OrderStatus.UNPAID) {
throw new IllegalStateException("订单状态异常");
}
// 阶段2:库存扣减(乐观锁)
boolean stockDeducted = inventoryDao.deductWithVersion(
order.getProductId(),
order.getQuantity(),
order.getInventoryVersion()
);
if (!stockDeducted) {
throw new ConcurrentModificationException("库存并发修改");
}
// 阶段3:支付核心(新事务)
paymentTransaction(order);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
void paymentTransaction(Order order) {
paymentGateway.charge(order);
orderDao.updateStatus(order.getId(), OrderStatus.PAID);
}
}
3. REPEATABLE READ(可重复读)优化
问题:长事务导致性能差
优化口诀: “快照读,分批搞”
具体方法:
- 数据迁移时分批提交:
@Transactional
public void migrateData() {
List<Data> batch = queryBatch(1000); // 每次查1000条
batch.forEach(this::processSingleRecord);
}
@Transactional(propagation = REQUIRES_NEW) // 每批新事务
void processSingleRecord(Data data) {
targetDao.insert(data);
}
- 对账时手动加锁:
-- 对账前先锁定
SELECT * FROM accounts WHERE id=123 FOR UPDATE;
- 财务月末对账
@Service
public class ReconciliationService {
// 分批处理+显式加锁
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void monthlyReconciliation(LocalDate month) {
// 1. 锁定账户(避免对账期间数据变动)
List<Account> accounts = accountDao.lockAccountsForReconciliation();
// 2. 分批处理交易记录
int batchSize = 1000;
List<Transaction> batch;
int offset = 0;
do {
batch = transactionDao.getBatch(month, offset, batchSize);
reconcileBatch(accounts, batch);
offset += batchSize;
EntityManagerHelper.clear(); // 防止JPA缓存膨胀
} while (!batch.isEmpty());
}
// 每批新事务提交
@Transactional(propagation = Propagation.REQUIRES_NEW)
void reconcileBatch(List<Account> accounts, List<Transaction> batch) {
batch.forEach(tx -> {
Account acc = accounts.stream()
.filter(a -> a.getId().equals(tx.getAccountId()))
.findFirst()
.orElseThrow();
acc.reconcile(tx);
});
accountDao.batchUpdate(accounts);
}
}
4. SERIALIZABLE(串行化)优化
问题:性能差但必须强一致
优化口诀: “精细化锁,异步排队”
具体方法:
- 转账时精准锁定账户:
@Transactional
public void transfer(Long from, Long to) {
// 只锁两个账户行
accountDao.lockAccounts(List.of(from, to));
// 执行转账...
}
- 用Redis队列串行化请求:
# 伪代码:转账请求排队
def transfer_task(amount):
with redis.lock("account_lock"):
process_transfer(amount)
# 所有请求入队
queue.enqueue(transfer_task, amount=100)
- 银行跨账户转账
@Service
public class BankTransferService {
private final AccountDao accountDao;
private final TransferQueue transferQueue;
// 最终入口(异步化)
public void transferAsync(TransferRequest request) {
transferQueue.enqueue(request); // 进入RabbitMQ/Kafka队列
}
// 实际处理(串行消费)
@Transactional(isolation = Isolation.SERIALIZABLE)
public void processTransfer(TransferRequest request) {
// 1. 精准锁定两个账户(避免全表锁)
List<Account> accounts = accountDao.lockAccountsByIds(
List.of(request.fromAccountId(), request.toAccountId())
);
Account from = accounts.get(0);
Account to = accounts.get(1);
// 2. 业务校验
if (from.getBalance().compareTo(request.amount()) < 0) {
throw new InsufficientBalanceException();
}
// 3. 执行转账
from.debit(request.amount());
to.credit(request.amount());
// 4. 记录流水(新事务)
recordTransaction(request);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
void recordTransaction(TransferRequest request) {
transactionDao.insert(new Transaction(request));
}
}
5. 通用优化技巧
- 查看当前锁情况(MySQL):
SHOW ENGINE INNODB STATUS; -- 看锁等待
SELECT * FROM performance_schema.events_waits_current; -- 当前阻塞
-- 查看长事务
SELECT * FROM information_schema.innodb_trx;
- 连接池配置(HikariCP示例):
spring:
datasource:
hikari:
maximum-pool-size: 20 # 建议值:CPU核心数 * 2
connection-timeout: 3000 # 3秒拿不到连接报错
leak-detection-threshold: 5000 # 5秒未关闭连接报警
- 避免全表锁
-- 好的写法(使用索引)
SELECT * FROM orders WHERE user_id=100 FOR UPDATE;
-- 坏的写法(全表锁)
SELECT * FROM orders WHERE status='PAID' FOR UPDATE;
- 动态降级策略(高并发时):
// 根据系统负载自动降级
@Transactional(isolation = getCurrentIsolationLevel())
public void businessMethod() {
// ...
}
Isolation getCurrentIsolationLevel() {
return systemLoad > 80% ? Isolation.READ_COMMITTED
: Isolation.REPEATABLE_READ;
}
6. 通用优化工具类
public class TransactionHelper {
// 动态隔离级别切换
public static <T> T executeWithIsolation(
TransactionTemplate txTemplate,
IsolationLevel level,
Callable<T> task
) {
txTemplate.setIsolationLevel(level.getValue());
return txTemplate.execute(status -> {
try {
return task.call();
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
// 监控事务耗时
@Around("@annotation(org.springframework.transaction.annotation.Transactional)")
public Object monitorTransaction(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
try {
return pjp.proceed();
} finally {
long cost = System.currentTimeMillis() - start;
Metrics.record("transaction_time", cost);
if (cost > 500) {
log.warn("Long transaction detected: {}ms", cost);
}
}
}
}
7. 各场景配置建议
- Redis缓存配置(Spring Boot)
spring:
cache:
type: redis
redis:
time-to-live: 1s # 1秒过期
datasource:
hikari:
isolation-level: READ_COMMITTED # 默认级别
- 批量处理参数(JPA)
# application.properties
spring.jpa.properties.hibernate.jdbc.batch_size=1000
spring.jpa.properties.hibernate.order_inserts=true
- 锁超时设置(MySQL)
SET GLOBAL innodb_lock_wait_timeout=3; -- 锁等待超时3秒
六、电商交易系统 - 隔离实战
四种隔离级别在实际业务中的应用。系统包含订单处理、库存管理、支付服务和数据报表等模块。
1. 项目结构
src/
├── main/
│ ├── java/
│ │ └── com/
│ │ └── ecommerce/
│ │ ├── config/ # 配置类
│ │ ├── controller/ # 控制器
│ │ ├── model/ # 数据模型
│ │ ├── repository/ # 数据访问
│ │ ├── service/ # 业务服务
│ │ └── EcommerceApplication.java
│ └── resources/
│ ├── application.yml # 应用配置
│ └── schema.sql # 数据库脚本
└── test/ # 测试类
2. READ UNCOMMITTED 应用 - 实时监控服务
@Service
@RequiredArgsConstructor
public class MonitorService {
private final JdbcTemplate jdbcTemplate;
private final RedisTemplate<String, Object> redisTemplate;
/**
* 获取实时订单统计(允许脏读)
* 适用场景:管理员仪表盘显示近似实时数据
* 优化:使用Redis缓存降低数据库压力
*/
@Cacheable(value = "realtime_stats", key = "'order_count'", ttl = 1) // 1秒缓存
public OrderStats getRealtimeOrderStats() {
// 使用READ UNCOMMITTED查询未提交数据
return jdbcTemplate.queryForObject(
"SELECT " +
"COUNT(*) as total, " +
"SUM(CASE WHEN status='PAID' THEN 1 ELSE 0 END) as paid " +
"FROM orders /* 不加锁 */",
(rs, rowNum) -> new OrderStats(
rs.getInt("total"),
rs.getInt("paid")
)
);
}
/**
* 数据补偿任务:每小时修正一次脏数据
*/
@Scheduled(cron = "0 0 * * * *")
public void fixDirtyData() {
OrderStats realStats = jdbcTemplate.queryForObject(
"SELECT " +
"COUNT(*) as total, " +
"SUM(CASE WHEN status='PAID' THEN 1 ELSE 0 END) as paid " +
"FROM orders WITH (NOLOCK)",
(rs, rowNum) -> new OrderStats(
rs.getInt("total"),
rs.getInt("paid")
)
);
OrderStats cachedStats = (OrderStats) redisTemplate.opsForValue()
.get("realtime_stats::order_count");
if (!realStats.equals(cachedStats)) {
redisTemplate.opsForValue().set(
"realtime_stats::order_count",
realStats
);
}
}
}
3. READ COMMITTED 应用 - 订单服务
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final InventoryService inventoryService;
private final PaymentService paymentService;
/**
* 创建订单(READ COMMITTED隔离级别)
* 适用场景:电商下单流程,需要看到已提交的库存数据
* 优化:短事务 + 乐观锁控制
*/
@Transactional(isolation = Isolation.READ_COMMITTED)
public Order createOrder(OrderRequest request) {
// 阶段1:快速校验(非DB操作)
validateRequest(request);
// 阶段2:库存预占(新事务)
InventoryLockResult lockResult = inventoryService.tryLockInventory(
request.getProductId(),
request.getQuantity()
);
if (!lockResult.isSuccess()) {
throw new InventoryException("库存不足");
}
// 阶段3:创建订单
Order order = new Order();
order.setUserId(request.getUserId());
order.setProductId(request.getProductId());
order.setQuantity(request.getQuantity());
order.setStatus(OrderStatus.CREATED);
Order savedOrder = orderRepository.save(order);
// 阶段4:异步支付(不影响主事务)
paymentService.processPaymentAsync(savedOrder.getId());
return savedOrder;
}
// 验证逻辑(非事务)
private void validateRequest(OrderRequest request) {
if (request.getQuantity() <= 0) {
throw new IllegalArgumentException("数量必须大于0");
}
}
}
4. REPEATABLE READ 应用 - 财务对账服务
@Service
@RequiredArgsConstructor
public class ReconciliationService {
private final AccountRepository accountRepository;
private final TransactionRepository transactionRepository;
/**
* 每日对账(REPEATABLE READ隔离级别)
* 适用场景:需要事务内多次读取一致性的财务对账
* 优化:分批处理 + 显式加锁
*/
@Transactional(isolation = Isolation.REPEATABLE_READ)
public ReconciliationResult dailyReconciliation(LocalDate date) {
// 1. 锁定需要对的账户
List<Account> accounts = accountRepository
.findAllByIdForUpdate(getRelevantAccountIds(date));
ReconciliationResult result = new ReconciliationResult();
int batchSize = 500;
int offset = 0;
List<Transaction> batch;
// 2. 分批处理交易记录
do {
batch = transactionRepository.findByDate(date, offset, batchSize);
batch.forEach(tx -> {
Account acc = accounts.stream()
.filter(a -> a.getId().equals(tx.getAccountId()))
.findFirst()
.orElseThrow();
if (acc.reconcile(tx)) {
result.addMatched(tx);
} else {
result.addMismatched(tx);
}
});
offset += batchSize;
} while (!batch.isEmpty());
// 3. 更新账户状态
accountRepository.saveAll(accounts);
return result;
}
}
5. SERIALIZABLE 应用 - 支付服务
@Service
@RequiredArgsConstructor
public class PaymentService {
private final AccountRepository accountRepository;
private final PaymentTransactionRepository transactionRepository;
private final KafkaTemplate<String, Object> kafkaTemplate;
/**
* 转账支付(SERIALIZABLE隔离级别)
* 适用场景:需要绝对一致性的金融交易
* 优化:异步队列 + 行级锁
*/
@Transactional(isolation = Isolation.SERIALIZABLE)
public void transfer(TransferRequest request) {
// 1. 精确锁定两个账户(避免全表锁)
List<Account> accounts = accountRepository
.findAllByIdForUpdate(Arrays.asList(
request.getFromAccountId(),
request.getToAccountId()
));
Account fromAccount = accounts.get(0);
Account toAccount = accounts.get(1);
// 2. 业务验证
if (fromAccount.getBalance().compareTo(request.getAmount()) < 0) {
throw new InsufficientBalanceException();
}
// 3. 执行转账
fromAccount.debit(request.getAmount());
toAccount.credit(request.getAmount());
// 4. 记录交易(新事务)
recordTransaction(request, fromAccount, toAccount);
// 5. 发送通知(异步)
kafkaTemplate.send("payment-events",
new PaymentEvent(request, PaymentStatus.COMPLETED));
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
private void recordTransaction(TransferRequest request,
Account from, Account to) {
PaymentTransaction transaction = new PaymentTransaction();
transaction.setFromAccount(from);
transaction.setToAccount(to);
transaction.setAmount(request.getAmount());
transaction.setStatus(TransactionStatus.COMPLETED);
transactionRepository.save(transaction);
}
}
6. 数据库配置
-- schema.sql
-- 账户表:存储用户资金账户信息
CREATE TABLE accounts (
id BIGINT PRIMARY KEY, -- 账户ID(主键)
user_id BIGINT NOT NULL, -- 关联的用户ID
balance DECIMAL(19,4) NOT NULL, -- 当前余额(精确到4位小数)
version INT NOT NULL DEFAULT 0, -- 乐观锁版本号(用于并发控制)
-- 约束条件
CONSTRAINT chk_balance_non_negative CHECK (balance >= 0) -- 余额不能为负数
) COMMENT '用户资金账户表';
-- 订单表:记录用户购买订单
CREATE TABLE orders (
id BIGINT PRIMARY KEY, -- 订单ID(主键)
user_id BIGINT NOT NULL, -- 下单用户ID
product_id BIGINT NOT NULL, -- 商品ID
quantity INT NOT NULL, -- 购买数量
status VARCHAR(20) NOT NULL, -- 订单状态(CREATED/PAID/SHIPPED等)
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间
-- 索引
INDEX idx_user_status (user_id, status), -- 加速用户订单查询
INDEX idx_created (created_at), -- 按时间排序
-- 约束条件
CONSTRAINT chk_quantity_positive CHECK (quantity > 0) -- 数量必须大于0
) COMMENT '用户订单表';
-- 库存表:商品库存管理
CREATE TABLE inventory (
product_id BIGINT PRIMARY KEY, -- 商品ID(主键)
stock INT NOT NULL, -- 可用库存
locked_stock INT NOT NULL DEFAULT 0, -- 预占库存(下单未支付)
version INT NOT NULL DEFAULT 0, -- 乐观锁版本号
-- 约束条件
CONSTRAINT chk_stock_non_negative CHECK (stock >= 0), -- 库存不能为负
CONSTRAINT chk_locked_stock_non_negative CHECK (locked_stock >= 0), -- 预占库存不能为负
CONSTRAINT chk_total_stock CHECK (stock + locked_stock >= 0) -- 总库存校验
) COMMENT '商品库存表';
-- 交易记录表:资金流水
CREATE TABLE transactions (
id BIGINT PRIMARY KEY, -- 交易ID(主键)
from_account_id BIGINT NOT NULL, -- 转出账户
to_account_id BIGINT NOT NULL, -- 转入账户
amount DECIMAL(19,4) NOT NULL, -- 交易金额
status VARCHAR(20) NOT NULL, -- 交易状态(PROCESSING/SUCCESS/FAILED)
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间
-- 索引
INDEX idx_from_account (from_account_id), -- 加速转出账户查询
INDEX idx_to_account (to_account_id), -- 加速转入账户查询
INDEX idx_created_status (created_at, status), -- 按时间和状态查询
-- 约束条件
CONSTRAINT chk_amount_positive CHECK (amount > 0), -- 金额必须大于0
CONSTRAINT fk_from_account FOREIGN KEY (from_account_id) REFERENCES accounts(id),
CONSTRAINT fk_to_account FOREIGN KEY (to_account_id) REFERENCES accounts(id)
) COMMENT '资金交易流水表';
-- 初始化数据示例
INSERT INTO accounts (id, user_id, balance) VALUES
(1001, 1, 5000.0000), -- 用户1初始余额5000
(1002, 2, 3000.0000); -- 用户2初始余额3000
INSERT INTO inventory (product_id, stock) VALUES
(101, 1000), -- 商品ID 101 库存1000
(102, 500); -- 商品ID 102 库存500
7. Spring Boot 配置
# application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/ecommerce
username: root
password: password
hikari:
maximum-pool-size: 20
connection-timeout: 3000
jpa:
show-sql: true
properties:
hibernate:
jdbc:
batch_size: 1000
order_inserts: true
cache:
type: redis
redis:
time-to-live: 1s
kafka:
bootstrap-servers: localhost:9092
8. 测试用例
@SpringBootTest
class EcommerceApplicationTests {
@Autowired
private OrderService orderService;
@Autowired
private PaymentService paymentService;
@Autowired
private MonitorService monitorService;
@Test
void testOrderCreate() {
OrderRequest request = new OrderRequest(1L, 101L, 2);
Order order = orderService.createOrder(request);
assertNotNull(order.getId());
}
@Test
void testHighConcurrencyPayment() {
IntStream.range(0, 100).parallel().forEach(i -> {
TransferRequest request = new TransferRequest(1000L, 1001L,
new BigDecimal("100.00"));
paymentService.transfer(request);
});
Account account = accountRepository.findById(1000L).orElseThrow();
assertTrue(account.getBalance().compareTo(BigDecimal.ZERO) >= 0);
}
@Test
void testRealtimeStats() {
OrderStats stats = monitorService.getRealtimeOrderStats();
assertTrue(stats.getTotalOrders() >= 0);
}
}