Java ATM机自动取款机毕业设计:从单机模拟到高并发实战架构
传统毕业设计的常见缺陷
很多同学在做ATM机毕业设计时,往往只关注基础功能的实现,比如在控制台输入账号密码,然后进行简单的存取款操作。这种实现方式虽然能完成基本要求,但距离一个真实可用的系统还有很大差距。我总结了一下,主要有以下几个常见问题:
首先是没有并发控制。真实的ATM机是支持多用户同时操作的,如果多个线程同时操作同一个账户,比如同时取款,就可能导致余额计算错误,出现“超取”的情况。很多同学的单线程程序完全没考虑这个问题。
其次是没有事务管理。一次取款操作应该包含多个步骤:检查余额、扣减余额、生成流水记录。如果中间任何一步失败,整个操作都应该回滚,否则就会出现账户余额减少了但流水没记录,或者流水记录了但余额没扣减的混乱状态。
还有就是数据持久化方式过于简单。很多同学用文件存储或者干脆用内存变量,程序一重启数据就全丢了。而且缺乏完整的数据模型设计,账户信息、交易流水混在一起,不利于后续扩展。

技术选型:从纯Java到Spring Boot生态
1. 基础技术栈对比
纯Java实现 vs Spring Boot框架
很多同学刚开始会想用纯Java写,觉得这样更“基础”。但实际开发中,Spring Boot能帮我们节省大量重复工作:
- 依赖管理:纯Java需要手动管理各种jar包,版本冲突让人头疼。Spring Boot通过starter依赖一键配置
- Web服务:纯Java需要自己写Servlet处理HTTP请求,Spring Boot内嵌Tomcat,几行代码就能启动Web服务
- 数据库连接:纯Java用JDBC要写大量模板代码,Spring Boot + JPA/Hibernate让数据库操作变得简单
文件存储 vs 数据库存储
- 文件存储:实现简单,但并发读写容易出问题,数据查询效率低,没有事务保证
- MySQL数据库:支持ACID事务,有成熟的并发控制机制,SQL查询灵活,数据安全有保障
2. 我们的技术选型
基于以上分析,我选择了这样的技术栈:
- 后端框架:Spring Boot 2.7.x(稳定版)
- 数据库:MySQL 8.0 + Redis 7.0(缓存和分布式锁)
- 构建工具:Maven
- API文档:Swagger/OpenAPI 3.0
这个组合既保证了项目的完整性,又不会因为技术太新而增加学习成本。
核心模块设计与实现
1. 数据库设计
先来看数据库表的设计,这是整个系统的基础:
-- 账户表
CREATE TABLE account (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
account_number VARCHAR(20) UNIQUE NOT NULL COMMENT '账号',
account_name VARCHAR(50) NOT NULL COMMENT '账户名',
balance DECIMAL(15,2) NOT NULL DEFAULT 0.00 COMMENT '余额',
password_hash VARCHAR(255) NOT NULL COMMENT '密码哈希',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1-正常,0-冻结',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_account_number (account_number)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='账户表';
-- 交易流水表
CREATE TABLE transaction (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
transaction_id VARCHAR(32) UNIQUE NOT NULL COMMENT '交易流水号',
account_number VARCHAR(20) NOT NULL COMMENT '账号',
transaction_type TINYINT NOT NULL COMMENT '交易类型:1-存款,2-取款,3-转账',
amount DECIMAL(15,2) NOT NULL COMMENT '交易金额',
balance_before DECIMAL(15,2) NOT NULL COMMENT '交易前余额',
balance_after DECIMAL(15,2) NOT NULL COMMENT '交易后余额',
related_account VARCHAR(20) COMMENT '关联账户(转账时使用)',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1-成功,0-失败',
remark VARCHAR(200) COMMENT '备注',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_account_time (account_number, created_time),
INDEX idx_transaction_id (transaction_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='交易流水表';
2. 账户服务实现
账户服务是整个系统的核心,我采用了分层架构:
// 账户实体类
@Entity
@Table(name = "account")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Account {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "account_number", unique = true, nullable = false)
private String accountNumber;
@Column(name = "account_name", nullable = false)
private String accountName;
@Column(nullable = false, precision = 15, scale = 2)
private BigDecimal balance;
@Column(name = "password_hash", nullable = false)
private String passwordHash;
@Column(nullable = false)
private Integer status;
@CreationTimestamp
@Column(name = "created_time", updatable = false)
private LocalDateTime createdTime;
@UpdateTimestamp
@Column(name = "updated_time")
private LocalDateTime updatedTime;
}
// 账户服务接口
public interface AccountService {
/**
* 用户登录验证
*/
Account login(String accountNumber, String password);
/**
* 查询账户余额
*/
BigDecimal queryBalance(String accountNumber);
/**
* 存款操作
*/
TransactionResult deposit(String accountNumber, BigDecimal amount);
/**
* 取款操作(核心业务)
*/
TransactionResult withdraw(String accountNumber, BigDecimal amount);
}
// 账户服务实现
@Service
@Slf4j
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountRepository accountRepository;
@Autowired
private TransactionService transactionService;
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Override
@Transactional(rollbackFor = Exception.class)
public TransactionResult withdraw(String accountNumber, BigDecimal amount) {
// 参数校验
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new BusinessException("取款金额必须大于0");
}
// 获取账户并加锁(防止并发操作)
Account account = accountRepository.findByAccountNumberForUpdate(accountNumber);
if (account == null) {
throw new BusinessException("账户不存在");
}
// 检查账户状态
if (account.getStatus() != 1) {
throw new BusinessException("账户状态异常,无法取款");
}
// 检查余额是否充足
if (account.getBalance().compareTo(amount) < 0) {
throw new BusinessException("余额不足");
}
// 执行扣款
BigDecimal oldBalance = account.getBalance();
BigDecimal newBalance = oldBalance.subtract(amount);
account.setBalance(newBalance);
accountRepository.save(account);
// 记录交易流水
String transactionId = generateTransactionId();
transactionService.recordTransaction(transactionId, accountNumber,
TransactionType.WITHDRAWAL, amount, oldBalance, newBalance);
log.info("取款成功:账户={}, 金额={}, 余额从{}变为{}",
accountNumber, amount, oldBalance, newBalance);
return new TransactionResult(true, transactionId, newBalance, "取款成功");
}
private String generateTransactionId() {
// 生成唯一交易流水号:时间戳 + 随机数
return "TXN" + System.currentTimeMillis() +
String.format("%06d", new Random().nextInt(1000000));
}
}
3. 交易流水与幂等性控制
交易流水记录是金融系统的关键,必须保证幂等性(同一笔交易只能成功一次):
@Service
public class TransactionServiceImpl implements TransactionService {
@Autowired
private TransactionRepository transactionRepository;
@Autowired
private RedisTemplate<String, String> redisTemplate;
// 交易幂等性控制:使用Redis记录已处理的请求ID
private static final String TRANSACTION_IDEMPOTENT_KEY = "atm:txn:idempotent:";
@Override
@Transactional(rollbackFor = Exception.class)
public void recordTransaction(String transactionId, String accountNumber,
TransactionType type, BigDecimal amount,
BigDecimal balanceBefore, BigDecimal balanceAfter) {
// 幂等性检查:如果这个交易ID已经处理过,直接返回
String key = TRANSACTION_IDEMPOTENT_KEY + transactionId;
Boolean isNew = redisTemplate.opsForValue().setIfAbsent(key, "1", 24, TimeUnit.HOURS);
if (Boolean.FALSE.equals(isNew)) {
log.warn("重复的交易请求,transactionId={}", transactionId);
throw new BusinessException("重复的交易请求");
}
// 保存交易记录
Transaction transaction = new Transaction();
transaction.setTransactionId(transactionId);
transaction.setAccountNumber(accountNumber);
transaction.setTransactionType(type.getValue());
transaction.setAmount(amount);
transaction.setBalanceBefore(balanceBefore);
transaction.setBalanceAfter(balanceAfter);
transaction.setStatus(1); // 成功
transactionRepository.save(transaction);
log.info("交易流水记录成功:{}", transactionId);
}
}
并发控制:从synchronized到分布式锁
1. synchronized关键字(单机场景)
在单机部署的情况下,可以使用synchronized保证线程安全:
@Service
public class SynchronizedATMService {
// 使用对象锁保护关键资源
private final Object lock = new Object();
public TransactionResult withdrawWithSync(String accountNumber, BigDecimal amount) {
synchronized (lock) {
// 这里的代码同一时间只能有一个线程执行
return doWithdraw(accountNumber, amount);
}
}
// 或者使用方法级别的同步
public synchronized TransactionResult withdrawWithMethodSync(String accountNumber, BigDecimal amount) {
return doWithdraw(accountNumber, amount);
}
}
适用场景:
- 单机部署的ATM系统
- 用户量不大,并发请求少
- 开发测试环境
局限性:
- 只能保证单个JVM内的线程安全
- 性能较差,锁粒度粗
- 不适合集群部署
2. Redis分布式锁(生产环境)
在生产环境多实例部署时,需要使用分布式锁:
@Component
@Slf4j
public class DistributedLockService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String LOCK_PREFIX = "atm:lock:account:";
private static final long LOCK_EXPIRE_TIME = 30; // 锁过期时间30秒
private static final long WAIT_TIME = 10; // 等待锁的最长时间10秒
/**
* 尝试获取分布式锁
*/
public boolean tryLock(String accountNumber) {
String lockKey = LOCK_PREFIX + accountNumber;
String lockValue = UUID.randomUUID().toString();
long startTime = System.currentTimeMillis();
while (System.currentTimeMillis() - startTime < WAIT_TIME * 1000) {
// 使用SETNX命令尝试获取锁
Boolean success = redisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, LOCK_EXPIRE_TIME, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(success)) {
log.debug("获取锁成功:{}", lockKey);
return true;
}
// 短暂等待后重试
try {
Thread.sleep(100); // 100毫秒后重试
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
log.warn("获取锁超时:{}", lockKey);
return false;
}
/**
* 释放分布式锁
*/
public void unlock(String accountNumber) {
String lockKey = LOCK_PREFIX + accountNumber;
redisTemplate.delete(lockKey);
log.debug("释放锁:{}", lockKey);
}
}
// 在取款服务中使用分布式锁
@Service
public class DistributedATMService {
@Autowired
private DistributedLockService lockService;
@Autowired
private AccountService accountService;
public TransactionResult withdrawWithDistributedLock(String accountNumber, BigDecimal amount) {
// 尝试获取锁
if (!lockService.tryLock(accountNumber)) {
throw new BusinessException("系统繁忙,请稍后重试");
}
try {
// 执行取款操作
return accountService.withdraw(accountNumber, amount);
} finally {
// 无论如何都要释放锁
lockService.unlock(accountNumber);
}
}
}

安全性设计与实现
1. 防止余额透支
余额检查必须在事务内完成,并且要加锁:
@Service
public class SafeWithdrawService {
@Autowired
private AccountRepository accountRepository;
@Transactional(rollbackFor = Exception.class)
public void safeWithdraw(String accountNumber, BigDecimal amount) {
// 使用SELECT ... FOR UPDATE获取行锁
Account account = accountRepository.findByAccountNumberForUpdate(accountNumber);
// 双重检查:在事务内再次验证余额
if (account.getBalance().compareTo(amount) < 0) {
throw new BusinessException("余额不足,取款失败");
}
// 执行扣款
account.setBalance(account.getBalance().subtract(amount));
accountRepository.save(account);
}
}
2. 操作日志追踪
完整的操作日志对于问题排查和审计至关重要:
@Aspect
@Component
@Slf4j
public class OperationLogAspect {
@Around("@annotation(operationLog)")
public Object logOperation(ProceedingJoinPoint joinPoint, OperationLog operationLog) throws Throwable {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
// 记录操作开始
String requestId = UUID.randomUUID().toString();
log.info("操作开始[{}]:方法={}, 参数={}", requestId, methodName, Arrays.toString(args));
long startTime = System.currentTimeMillis();
try {
// 执行原方法
Object result = joinPoint.proceed();
long costTime = System.currentTimeMillis() - startTime;
log.info("操作成功[{}]:耗时={}ms, 结果={}", requestId, costTime, result);
return result;
} catch (Exception e) {
long costTime = System.currentTimeMillis() - startTime;
log.error("操作失败[{}]:耗时={}ms, 异常={}", requestId, costTime, e.getMessage(), e);
throw e;
}
}
}
// 使用注解标记需要记录日志的方法
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OperationLog {
String value() default "";
}
// 在服务方法上使用
@Service
public class LoggedATMService {
@OperationLog("用户取款操作")
public TransactionResult withdrawWithLog(String accountNumber, BigDecimal amount) {
// 业务逻辑
return withdraw(accountNumber, amount);
}
}
生产环境避坑指南
1. 数据库连接泄漏问题
数据库连接是宝贵资源,必须正确管理:
// 错误示例:手动管理连接容易忘记关闭
public void wrongQuery() {
Connection conn = null;
try {
conn = dataSource.getConnection();
// 执行查询
// ...
// 如果这里抛异常,conn就不会被关闭!
} catch (SQLException e) {
e.printStackTrace();
}
// 忘记关闭连接
}
// 正确做法:使用try-with-resources或Spring的事务管理
@Repository
public class SafeAccountRepository {
@Autowired
private JdbcTemplate jdbcTemplate; // Spring会自动管理连接
public Account findByAccountNumber(String accountNumber) {
String sql = "SELECT * FROM account WHERE account_number = ?";
return jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Account.class), accountNumber);
}
}
// 或者在配置中设置连接池参数
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/atm");
config.setUsername("root");
config.setPassword("password");
config.setMaximumPoolSize(20); // 最大连接数
config.setMinimumIdle(5); // 最小空闲连接
config.setConnectionTimeout(30000); // 连接超时时间
config.setIdleTimeout(600000); // 空闲连接超时
config.setMaxLifetime(1800000); // 连接最大生命周期
return new HikariDataSource(config);
}
}
2. 冷启动延迟优化
应用启动时,可以预热缓存和连接池:
@Component
public class SystemWarmUp implements ApplicationRunner {
@Autowired
private AccountRepository accountRepository;
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Override
public void run(ApplicationArguments args) throws Exception {
log.info("开始系统预热...");
// 预热数据库连接
warmUpDatabase();
// 预热Redis连接
warmUpRedis();
// 加载常用数据到缓存
loadHotDataToCache();
log.info("系统预热完成");
}
private void warmUpDatabase() {
// 执行一些简单的查询,建立连接池
accountRepository.count();
}
private void warmUpRedis() {
redisTemplate.opsForValue().set("warmup", "1", 10, TimeUnit.SECONDS);
}
private void loadHotDataToCache() {
// 加载常用账户信息到缓存
List<Account> hotAccounts = accountRepository.findTop100ByOrderByUpdatedTimeDesc();
// ... 缓存逻辑
}
}
3. 测试数据隔离
测试环境必须使用独立的数据,避免影响生产:
// 使用Testcontainers进行集成测试
@SpringBootTest
@Testcontainers
class ATMServiceIntegrationTest {
@Container
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
.withDatabaseName("test_atm")
.withUsername("test")
.withPassword("test");
@Container
static GenericContainer<?> redis = new GenericContainer<>("redis:7.0")
.withExposedPorts(6379);
@DynamicPropertySource
static void properties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", mysql::getJdbcUrl);
registry.add("spring.datasource.username", mysql::getUsername);
registry.add("spring.datasource.password", mysql::getPassword);
registry.add("spring.redis.host", redis::getHost);
registry.add("spring.redis.port", redis::getFirstMappedPort);
}
@Test
void testWithdraw() {
// 测试代码使用独立的测试数据库
// 不会影响生产数据
}
}
// 或者在测试类上使用内存数据库
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY)
class AccountRepositoryTest {
@Autowired
private AccountRepository accountRepository;
@Test
void testSaveAccount() {
// 使用H2等内存数据库进行测试
}
}
4. 监控与告警配置
生产环境必须有完善的监控:
# application.yml 监控配置
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
metrics:
export:
prometheus:
enabled: true
health:
db:
enabled: true
redis:
enabled: true
# 自定义健康检查
@Component
public class ATMHealthIndicator implements HealthIndicator {
@Autowired
private AccountRepository accountRepository;
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Override
public Health health() {
// 检查数据库连接
try {
accountRepository.count();
} catch (Exception e) {
return Health.down()
.withDetail("database", "连接失败: " + e.getMessage())
.build();
}
// 检查Redis连接
try {
redisTemplate.opsForValue().get("health_check");
} catch (Exception e) {
return Health.down()
.withDetail("redis", "连接失败: " + e.getMessage())
.build();
}
return Health.up()
.withDetail("database", "连接正常")
.withDetail("redis", "连接正常")
.build();
}
}
总结与思考
通过这个项目,我们实现了一个从简单控制台程序到生产级ATM系统的完整演进。关键点总结如下:
- 架构设计:采用分层架构,分离关注点,提高代码可维护性
- 并发控制:根据部署场景选择合适的锁机制(synchronized或分布式锁)
- 数据一致性:通过数据库事务和行锁保证余额计算的准确性
- 安全性:实现幂等性控制、操作审计、防重放攻击
- 可观测性:完善的日志记录和监控告警
这个项目不仅满足了毕业设计的基本要求,更重要的是展示了如何将一个学术项目升级为生产可用的系统。在答辩时,你可以详细讲解这些设计决策和技术选型,这会让你的项目脱颖而出。
思考题:如何扩展这个系统支持跨行转账?
跨行转账涉及更多复杂问题:
- 如何保证分布式事务的一致性?
- 如何处理不同银行之间的通信协议差异?
- 如何设计对账和差错处理机制?
- 如何保证跨行转账的安全性?
你可以尝试基于现有的ATM系统,设计一个跨行转账模块,考虑使用消息队列保证最终一致性,或者研究一下分布式事务解决方案如Seata。这会是项目一个很好的延伸方向。
希望这篇笔记能对你的毕业设计有所帮助。在实际开发中,记得多写测试、多考虑异常情况,这样才能构建出健壮的系统。祝你毕业设计顺利!
更多推荐

所有评论(0)