09银行系统项目复盘:从学生作业到生产级别的三次架构演变
银行系统项目复盘:从学生作业到生产级别的三次架构演变
大一那年,我用300行Java代码写了个“银行系统”,得意地交作业。直到工作后,我亲眼看到因为并发问题,同一个账户被取走了两笔钱——那一刻我才明白,课堂项目和生产系统之间,隔着一个太平洋。
开篇:那个让我差点挂科的“完美作业”
2024年,我的Java课程作业:
java
// 我的“杰作” - 账户类
public class Account {
public String id; // 卡号 - public,方便!
public String name; // 用户名 - public,方便!
public double money; // 余额 - public,方便!
public void getMoney(double m) {
money = money - m; // 直接操作!
}
}
// 测试代码
public static void main(String[] args) {
Account a = new Account();
a.id = "123456";
a.name = "张三";
a.money = 1000;
a.getMoney(500);
System.out.println("余额:" + a.money); // 输出500
}
教授给的评语是: “封装性零分,线程安全性零分,安全性零分。建议重修。”
当时的我不服气:“能跑起来不就行了吗?”

第一次重构:从“能跑就行”到“面向对象”
1.1 封装性觉醒:我不再相信任何人
工作第一年,导师看了我的代码后说:“在银行系统里,public 修饰的余额字段,相当于把金库密码贴在门口。”
重构前(学生的思维):
java
// 问题1:余额可以被直接修改
account.money = 1000000; // 黑客狂喜!
// 问题2:没有校验
account.getMoney(-1000); // 取负1000?居然成功了!
// 问题3:并发灾难
// 线程A:检查余额(1000) > 取款金额(800) → 通过
// 线程B:检查余额(1000) > 取款金额(800) → 通过
// 线程A:执行取款,余额变为200
// 线程B:执行取款,余额变为-600(灾难!)
重构后(程序员的思维):
java
public class BankAccount {
// 所有字段私有化 - 金库上锁
private final String accountId; // final:卡号不可变
private final String holderName; // final:用户名不可变
private volatile double balance; // volatile:保证可见性
private final double dailyLimit; // 日限额
// 构造器私有,通过工厂方法创建 - 控制创建过程
private BankAccount(String accountId, String holderName, double initialBalance, double dailyLimit) {
validateId(accountId);
validateName(holderName);
validateAmount(initialBalance);
this.accountId = accountId;
this.holderName = holderName;
this.balance = initialBalance;
this.dailyLimit = dailyLimit;
}
// 工厂方法 - 统一入口
public static BankAccount create(String holderName, double initialBalance) {
String accountId = generateAccountId();
double limit = calculateLimit(initialBalance);
return new BankAccount(accountId, holderName, initialBalance, limit);
}
// 取款方法 - 加锁保证线程安全
public synchronized WithdrawalResult withdraw(double amount) {
// 校验1:金额必须为正
if (amount <= 0) {
return WithdrawalResult.failure("取款金额必须大于0");
}
// 校验2:不能超过余额
if (amount > balance) {
return WithdrawalResult.failure("余额不足");
}
// 校验3:不能超过日限额
if (amount > dailyLimit) {
return WithdrawalResult.failure("超过单日取款限额");
}
// 校验4:最小取款单位(ATM机限制)
if (amount % 100 != 0) {
return WithdrawalResult.failure("取款金额必须为100的整数倍");
}
// 执行取款
balance -= amount;
// 记录交易
TransactionRecorder.recordWithdrawal(accountId, amount, balance);
return WithdrawalResult.success(amount, balance);
}
// 存款方法
public synchronized DepositResult deposit(double amount) {
// 校验逻辑类似...
}
// 唯一公开的getter
public double getBalance() {
return balance;
}
// 账户信息DTO - 不暴露完整对象
public AccountInfo getAccountInfo() {
return new AccountInfo(
accountId,
holderName,
balance,
dailyLimit
);
}
}
这次重构的教训:
- 封装是信任的边界:不相信调用者,不相信同事,甚至不相信明天的自己
- 校验是安全的第一道防线:每个输入都可能是恶意的
- 并发不是可选项:银行系统天生就是并发的
1.2 业务逻辑的陷阱:转账不只是“A减B加”

我犯过的经典错误:
java
// 错误示范:天真的转账
public void transfer(String fromId, String toId, double amount) {
Account from = findAccount(fromId);
Account to = findAccount(toId);
from.withdraw(amount); // 第一步:扣款
to.deposit(amount); // 第二步:存款
}
问题: 如果在第一步和第二步之间程序崩溃了,钱就消失了!
正确做法:事务性操作
java
public TransferResult transfer(String fromId, String toId, double amount) {
// 校验:不能给自己转账
if (fromId.equals(toId)) {
return TransferResult.failure("不能给自己转账");
}
// 获取两个账户的锁(按固定顺序,避免死锁)
Account from = lockAccount(fromId);
Account to = lockAccount(toId);
try {
// 在一个事务中执行
return executeInTransaction(() -> {
// 扣款
WithdrawalResult withdrawal = from.withdraw(amount);
if (!withdrawal.isSuccess()) {
return TransferResult.failure("扣款失败: " + withdrawal.getMessage());
}
// 存款
DepositResult deposit = to.deposit(amount);
if (!deposit.isSuccess()) {
// 回滚:把钱加回去
from.rollbackWithdrawal(amount);
return TransferResult.failure("存款失败,已回滚");
}
// 记录转账
TransactionRecorder.recordTransfer(fromId, toId, amount);
return TransferResult.success(amount, from.getBalance());
});
} finally {
// 释放锁
unlockAccount(from);
unlockAccount(to);
}
}
第二次重构:从“单机玩具”到“分布式微服务”
2.1 单一职责原则:银行不是一个大类

我见过最可怕的银行系统代码:
java
// God Class - 上帝类
public class BankSystem {
// 管理账户
private List<Account> accounts;
// 管理用户
private List<User> users;
// 管理交易
private List<Transaction> transactions;
// 管理日志
private List<Log> logs;
// 连接数据库
private Connection conn;
// 发送邮件
private EmailSender emailSender;
// 生成报表
private ReportGenerator reportGenerator;
// 200多个方法...
public void createAccount() { /* ... */ }
public void deleteAccount() { /* ... */ }
public void transferMoney() { /* ... */ }
public void generateStatement() { /* ... */ }
public void sendNotification() { /* ... */ }
public void backupDatabase() { /* ... */ }
// ... 还有195个方法
}
重构:按领域拆分
java
// 账户服务 - 只负责账户相关
public interface AccountService {
Account createAccount(OpenAccountRequest request);
Account getAccount(String accountId);
void closeAccount(String accountId);
void freezeAccount(String accountId);
void unfreezeAccount(String accountId);
}
// 交易服务 - 只负责交易
public interface TransactionService {
TransactionResult deposit(DepositRequest request);
TransactionResult withdraw(WithdrawalRequest request);
TransactionResult transfer(TransferRequest request);
List<Transaction> getStatement(String accountId, DateRange range);
}
// 风控服务 - 只负责风险控制
public interface RiskControlService {
RiskAssessment assessWithdrawal(WithdrawalRequest request);
RiskAssessment assessTransfer(TransferRequest request);
void flagSuspiciousActivity(String accountId, String reason);
}
// 通知服务 - 只负责通知
public interface NotificationService {
void sendBalanceAlert(String accountId, BalanceAlert alert);
void sendTransactionAlert(String accountId, TransactionAlert alert);
void sendSecurityAlert(String accountId, SecurityAlert alert);
}
2.2 依赖注入:从“new”到“注入”
坏味道代码:
java
public class Bank {
private AccountRepository repo = new AccountRepository();
private EmailService email = new EmailService();
private LogService log = new LogService();
private AuditService audit = new AuditService();
// ... 直接new了10个依赖
public void transferMoney() {
// 业务逻辑和具体实现紧耦合
repo.updateBalance();
email.sendNotification();
log.record();
audit.track();
}
}
依赖注入重构:
java
public class BankTransferService {
// 通过接口依赖,而不是具体实现
private final AccountRepository accountRepo;
private final NotificationService notificationService;
private final TransactionLogger transactionLogger;
private final AuditTrail auditTrail;
private final RiskControlService riskControl;
// 依赖通过构造器注入
@Inject
public BankTransferService(
AccountRepository accountRepo,
NotificationService notificationService,
TransactionLogger transactionLogger,
AuditTrail auditTrail,
RiskControlService riskControl) {
this.accountRepo = accountRepo;
this.notificationService = notificationService;
this.transactionLogger = transactionLogger;
this.auditTrail = auditTrail;
this.riskControl = riskControl;
}
public TransferResult transfer(TransferCommand command) {
// 1. 风险控制检查
RiskAssessment risk = riskControl.assessTransfer(command);
if (risk.isHighRisk()) {
return TransferResult.rejected("风控拒绝");
}
// 2. 执行转账(事务性)
TransferResult result = accountRepo.transferInTransaction(command);
// 3. 记录日志
transactionLogger.logTransfer(command, result);
// 4. 审计追踪
auditTrail.recordTransfer(command);
// 5. 发送通知
if (result.isSuccess()) {
notificationService.sendTransferNotification(command);
}
return result;
}
}
依赖注入的好处:
- 可测试性:可以轻松Mock依赖
- 可替换性:更换实现只需改配置
- 单一职责:每个类只关注自己的核心逻辑
第三次重构:从“过程式思维”到“领域驱动设计”
3.1 贫血模型 vs 充血模型

贫血模型(我早期的写法):
java
// 贫血的Account - 只有getter/setter
public class Account {
private String id;
private String name;
private double balance;
private AccountStatus status;
// 只有数据,没有行为
// getter/setter...
}
// 服务类承担所有业务逻辑
public class AccountService {
public void withdraw(String accountId, double amount) {
Account account = accountRepo.findById(accountId);
// 所有业务逻辑都在服务层
if (account.getBalance() < amount) {
throw new InsufficientBalanceException();
}
if (account.getStatus() != AccountStatus.ACTIVE) {
throw new AccountFrozenException();
}
account.setBalance(account.getBalance() - amount);
accountRepo.save(account);
}
}
问题: 业务逻辑分散在各个Service中,Account只是个数据容器。
充血模型重构(领域驱动设计):
java
// 充血的Account - 数据和行为在一起
public class Account {
private AccountId id;
private AccountHolder holder;
private Money balance;
private AccountStatus status;
private List<Transaction> transactions;
private DailyWithdrawalLimit dailyLimit;
private LocalDate dailyWithdrawalDate;
private Money dailyWithdrawnAmount;
// 业务方法:取款
public WithdrawalResult withdraw(Money amount, WithdrawalStrategy strategy) {
// 校验1:账户状态
if (!status.canWithdraw()) {
return WithdrawalResult.rejected("账户状态不允许取款");
}
// 校验2:余额
if (!balance.canAfford(amount)) {
return WithdrawalResult.rejected("余额不足");
}
// 校验3:日限额
if (!dailyLimit.canWithdraw(amount, dailyWithdrawalDate, dailyWithdrawnAmount)) {
return WithdrawalResult.rejected("超过日取款限额");
}
// 校验4:取款策略
ValidationResult validation = strategy.validate(this, amount);
if (!validation.isValid()) {
return WithdrawalResult.rejected(validation.getMessage());
}
// 执行取款
Money newBalance = balance.subtract(amount);
this.balance = newBalance;
// 更新日取款记录
dailyWithdrawnAmount = dailyWithdrawnAmount.add(amount);
// 记录交易
Transaction transaction = Transaction.withdrawal(this.id, amount, newBalance);
transactions.add(transaction);
// 领域事件
DomainEventPublisher.publish(new MoneyWithdrawnEvent(this.id, amount, newBalance));
return WithdrawalResult.success(amount, newBalance, transaction.getId());
}
// 业务方法:存款
public DepositResult deposit(Money amount) {
// 类似逻辑...
}
// 业务方法:转账
public TransferResult transferTo(Account target, Money amount) {
// 类似逻辑...
}
// 内部类:值对象
public static class Money {
private final BigDecimal amount;
private final Currency currency;
public Money(BigDecimal amount, Currency currency) {
this.amount = amount.setScale(2, RoundingMode.HALF_EVEN);
this.currency = currency;
}
public Money add(Money other) {
validateSameCurrency(other);
return new Money(this.amount.add(other.amount), currency);
}
public Money subtract(Money other) {
validateSameCurrency(other);
return new Money(this.amount.subtract(other.amount), currency);
}
public boolean canAfford(Money other) {
validateSameCurrency(other);
return this.amount.compareTo(other.amount) >= 0;
}
private void validateSameCurrency(Money other) {
if (!this.currency.equals(other.currency)) {
throw new CurrencyMismatchException();
}
}
}
}
3.2 领域事件:系统解耦的关键
传统做法(紧耦合):
java
public class TransferService {
private AccountRepository accountRepo;
private EmailService emailService;
private SmsService smsService;
private AuditService auditService;
private ReportService reportService;
public void transfer(TransferRequest request) {
// 1. 执行转账
accountRepo.transfer(request);
// 2. 发送邮件
emailService.sendTransferEmail(request);
// 3. 发送短信
smsService.sendTransferSms(request);
// 4. 审计
auditService.recordTransfer(request);
// 5. 生成报表
reportService.generateTransferReport(request);
// 问题:每新增一个后续操作,都要修改这个方法
}
}
领域事件重构(事件驱动):
java
// 领域事件
public class MoneyTransferredEvent implements DomainEvent {
private final String transactionId;
private final String fromAccountId;
private final String toAccountId;
private final BigDecimal amount;
private final Instant occurredAt;
// getter...
}
// 转账服务(只负责核心业务)
public class TransferService {
private AccountRepository accountRepo;
private DomainEventPublisher eventPublisher;
public TransferResult transfer(TransferRequest request) {
// 1. 执行转账(事务内)
TransferResult result = accountRepo.transferInTransaction(request);
// 2. 发布领域事件
if (result.isSuccess()) {
MoneyTransferredEvent event = new MoneyTransferredEvent(
result.getTransactionId(),
request.getFromAccountId(),
request.getToAccountId(),
request.getAmount(),
Instant.now()
);
eventPublisher.publish(event);
}
return result;
}
}
// 事件处理器(各自独立)
@Component
class TransferEmailHandler implements DomainEventHandler<MoneyTransferredEvent> {
private final EmailService emailService;
@Override
public void handle(MoneyTransferredEvent event) {
emailService.sendTransferEmail(event);
}
}
@Component
class TransferSmsHandler implements DomainEventHandler<MoneyTransferredEvent> {
private final SmsService smsService;
@Override
public void handle(MoneyTransferredEvent event) {
smsService.sendTransferSms(event);
}
}
@Component
class TransferAuditHandler implements DomainEventHandler<MoneyTransferredEvent> {
private final AuditService auditService;
@Override
public void handle(MoneyTransferredEvent event) {
auditService.recordTransfer(event);
}
}
事件驱动的好处:
- 解耦:核心业务不知道也不关心后续操作
- 可扩展:新增处理器只需新增类,不改原有代码
- 可靠性:事件可以持久化,失败可重试
- 可观察性:所有操作都有事件追溯
实战:生产级银行系统的关键设计

4.1 防并发问题:乐观锁 vs 悲观锁
场景: 两个ATM同时取款
方案1:悲观锁(数据库行锁)
java
public class PessimisticAccountRepository {
public WithdrawalResult withdrawWithPessimisticLock(String accountId, BigDecimal amount) {
// 开启事务
return transactionTemplate.execute(status -> {
// SELECT ... FOR UPDATE 锁定行
Account account = jdbcTemplate.queryForObject(
"SELECT * FROM accounts WHERE id = ? FOR UPDATE",
new AccountRowMapper(),
accountId
);
// 检查余额
if (account.getBalance().compareTo(amount) < 0) {
status.setRollbackOnly();
return WithdrawalResult.failure("余额不足");
}
// 更新余额
int rows = jdbcTemplate.update(
"UPDATE accounts SET balance = balance - ? WHERE id = ?",
amount, accountId
);
return WithdrawalResult.success(amount, account.getBalance().subtract(amount));
});
}
}
方案2:乐观锁(版本号)
java
public class OptimisticAccountRepository {
public WithdrawalResult withdrawWithOptimisticLock(String accountId, BigDecimal amount) {
int retries = 0;
final int maxRetries = 3;
while (retries < maxRetries) {
// 1. 读取当前版本
Account account = jdbcTemplate.queryForObject(
"SELECT id, balance, version FROM accounts WHERE id = ?",
new AccountRowMapper(),
accountId
);
// 2. 业务校验
if (account.getBalance().compareTo(amount) < 0) {
return WithdrawalResult.failure("余额不足");
}
BigDecimal newBalance = account.getBalance().subtract(amount);
// 3. 尝试更新(带版本检查)
int updated = jdbcTemplate.update(
"UPDATE accounts SET balance = ?, version = version + 1 " +
"WHERE id = ? AND version = ?",
newBalance, accountId, account.getVersion()
);
// 4. 更新成功?
if (updated == 1) {
return WithdrawalResult.success(amount, newBalance);
}
// 5. 版本冲突,重试
retries++;
try {
Thread.sleep(50 * retries); // 指数退避
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
return WithdrawalResult.failure("操作冲突,请重试");
}
}
选择策略:
text
并发冲突频率?
├── 高 → 悲观锁(避免重试开销)
└── 低 → 乐观锁(更好的并发性能)
4.2 防重放攻击:交易幂等性
问题: 同一笔交易被重复提交
解决方案: 幂等性设计
java
public class IdempotentTransferService {
private final IdempotencyStore idempotencyStore;
public TransferResult transfer(TransferRequest request) {
// 1. 生成幂等键(客户端提供或服务端生成)
String idempotencyKey = request.getIdempotencyKey();
if (idempotencyKey == null) {
idempotencyKey = generateIdempotencyKey(request);
}
// 2. 检查是否已处理过
Optional<TransferResult> cached = idempotencyStore.get(idempotencyKey);
if (cached.isPresent()) {
return cached.get(); // 返回缓存结果
}
// 3. 获取幂等锁
IdempotencyLock lock = idempotencyStore.acquireLock(idempotencyKey);
if (!lock.isAcquired()) {
// 其他线程正在处理相同的请求
throw new ConcurrentRequestException("请勿重复提交");
}
try {
// 4. 执行转账
TransferResult result = doTransfer(request);
// 5. 保存结果到幂等存储
idempotencyStore.save(idempotencyKey, result);
return result;
} finally {
// 6. 释放锁
lock.release();
}
}
private String generateIdempotencyKey(TransferRequest request) {
return String.format("%s-%s-%s-%s",
request.getFromAccountId(),
request.getToAccountId(),
request.getAmount().toPlainString(),
request.getTimestamp().toEpochMilli()
);
}
}
4.3 防数据不一致:最终一致性方案
分布式转账场景: 账户服务在A数据库,交易服务在B数据库
java
public class SagaTransferService {
private final CommandBus commandBus;
private final SagaStore sagaStore;
public void transfer(TransferRequest request) {
// 1. 创建Saga(长事务)
String sagaId = sagaStore.createSaga("transfer", request);
try {
// 2. 第一步:扣款(可补偿)
commandBus.send(new DebitCommand(
request.getFromAccountId(),
request.getAmount(),
sagaId
));
// 3. 第二步:存款(可补偿)
commandBus.send(new CreditCommand(
request.getToAccountId(),
request.getAmount(),
sagaId
));
// 4. 第三步:记录交易(不可补偿,但可重试)
commandBus.send(new RecordTransactionCommand(
sagaId,
request.getFromAccountId(),
request.getToAccountId(),
request.getAmount()
));
// 5. 完成Saga
sagaStore.completeSaga(sagaId);
} catch (Exception e) {
// 6. 失败:执行补偿
sagaStore.failSaga(sagaId, e.getMessage());
executeCompensation(sagaId);
throw e;
}
}
private void executeCompensation(String sagaId) {
Saga saga = sagaStore.getSaga(sagaId);
// 逆序执行补偿操作
if (saga.isStepCompleted("recordTransaction")) {
// 删除交易记录(幂等)
commandBus.send(new DeleteTransactionCommand(sagaId));
}
if (saga.isStepCompleted("credit")) {
// 回滚存款(补偿取款)
commandBus.send(new CompensateCreditCommand(
saga.getToAccountId(),
saga.getAmount(),
sagaId
));
}
if (saga.isStepCompleted("debit")) {
// 回滚扣款(补偿存款)
commandBus.send(new CompensateDebitCommand(
saga.getFromAccountId(),
saga.getAmount(),
sagaId
));
}
}
}
从项目到产品:架构演进路线图
阶段1:单体应用(学生项目)
java
// 所有代码在一个项目里
BankSystem/
├── Account.java
├── Bank.java
├── ATM.java
└── Main.java
阶段2:分层架构(初级工程师)
java
// 按技术职责分层
BankSystem/
├── controller/ # 控制层
├── service/ # 服务层
├── repository/ # 数据层
├── model/ # 模型层
└── config/ # 配置层
阶段3:模块化架构(中级工程师)
java
// 按业务功能模块化
BankSystem/
├── account-module/ # 账户模块
├── transaction-module/ # 交易模块
├── customer-module/ # 客户模块
├── report-module/ # 报表模块
└── shared/ # 共享模块
阶段4:微服务架构(高级工程师)
java
// 每个服务独立部署
services/
├── account-service/ # 账户服务
├── transaction-service/ # 交易服务
├── customer-service/ # 客户服务
├── notification-service/# 通知服务
├── audit-service/ # 审计服务
└── api-gateway/ # API网关
阶段5:事件驱动架构(架构师)
java
// 服务通过事件通信
services/
├── command-services/ # 命令服务(写操作)
├── query-services/ # 查询服务(读操作)
├── event-processors/ # 事件处理器
├── message-broker/ # 消息中间件
└── event-store/ # 事件存储
生产级银行系统的检查清单
安全性检查
- 密码加密存储(bcrypt/scrypt)
- HTTPS传输
- SQL注入防护
- XSS防护
- CSRF令牌
- 会话安全管理
- 访问日志记录
- 敏感数据脱敏
可靠性检查
- 事务管理(ACID)
- 幂等性设计
- 重试机制
- 熔断器模式
- 降级策略
- 超时设置
- 资源隔离
性能检查
- 数据库索引优化
- 查询分页
- 缓存策略(Redis)
- 连接池配置
- 异步处理
- 批量操作
- 读写分离
可维护性检查
- 统一日志格式
- 集中配置管理
- API文档(Swagger)
- 健康检查端点
- 指标监控(Prometheus)
- 分布式追踪
- 代码覆盖率
写给Java初学者的真心话
不要被复杂的架构吓倒,记住:
- 所有复杂系统都是从简单开始的:先让程序跑起来,再考虑优化
- 理解原理比记住框架更重要:知道为什么,才知道怎么选
- 代码是写给人看的:可读性 > 聪明性
- 测试是信心的来源:没有测试的代码就像没有刹车的车
- 重构是常态:好代码是改出来的,不是一次写出来的
我的学习路线建议:
text
第一阶段:让程序跑起来
↓
第二阶段:让程序正确运行(加校验、异常处理)
↓
第三阶段:让程序安全运行(加锁、事务)
↓
第四阶段:让程序高效运行(优化、缓存)
↓
第五阶段:让程序可维护(设计模式、架构)
银行系统项目之所以经典,是因为它涵盖了软件开发的核心挑战:
- 数据一致性(账户余额必须准确)
- 安全性(资金不能被盗)
- 可靠性(7×24小时可用)
- 可审计性(每笔交易可追溯)
这些挑战,是所有严肃软件项目都会面对的。通过这个项目学到的,不只是Java语法,更是软件工程的思维方式。
记住:今天你写的银行系统,明天可能真的会被使用。用对待生产系统的心态写每个项目,这是专业程序员和学生最大的区别。
知识点测试
读完文章了?来测试一下你对知识点的掌握程度吧!
评论区
使用 GitHub 账号登录后即可发表评论,支持 Markdown 格式。
如果评论系统无法加载,请确保:
- 您的网络可以访问 GitHub
- giscus GitHub App 已安装到仓库
- 仓库已启用 Discussions 功能