☰
Java+MySQL银行管理系统:Eclipse项目实战与事务并发处理
2026/10/7 13:46:51 网站建设 项目流程

简介:这份资源是面向高校Java与大数据课程期末实践的银行管理系统完整工程,基于Eclipse开发、MySQL作为后台数据库,适合正在做课程设计或需要练手控制台项目与JDBC操作的学习者。项目围绕管理员与顾客两类角色展开:管理员端包含登录、添加与删除顾客、统计存储金额、富豪排行榜等功能;顾客端则实现登录、存款、取款、转账、修改密码等核心业务,覆盖面向对象设计、数据库连接与增删改查等常见考点。压缩包共14个文件,约2.25MB,以java源码、class字节码、Eclipse工程配置(project、classpath、prefs)以及MySQL驱动jar为主,导入后可直接运行调试。目前已有418人学习下载,可作为期末作业的参考实现,帮助读者快速理清分层结构、数据库表设计与业务逻辑,并在此基础上完成二次开发与功能扩展。

1. 从一份 Eclipse 里的银行管理系统说起:期末作业怎么做出生产味

很多人对「Java 大数据期末:银行管理系统(Eclipse 项目/MySQL 数据库)」的第一反应是:这不就是学生作业吗?但真动手做过的人知道,这套东西麻雀虽小,五脏俱全——它逼着你把 Java 面向对象、JDBC 连接池、MySQL 事务、Eclipse 工程结构一次性串起来。银行场景天然带事务、带并发、带对账,比图书管理、学生管理更能暴露问题。我见过太多同学在 Eclipse 里跑通了增删改查,一加转账就出现余额对不上,一并发就死锁,最后交作业前通宵改 SQL。这篇笔记就按一线做法,把从建库、连 Eclipse、写 DAO 到压测转账的完整路径拆开讲,新手能照着复现,熟手能对照参数和边界。适合正在做课程设计、毕业设计,或者想用一个真实小项目把 Java 和 MySQL 串起来的人。

2. 建库与建表:银行管理系统的数据模型怎么定

2.1 三张核心表撑起整个业务

银行管理系统的数据模型不需要花哨,但字段类型和约束必须一次定对,否则后面改表会连带改一堆 Java 实体类。我一般用三张表:account(账户)、transaction_record(交易流水)、customer(客户)。账户表存余额和状态,流水表存每一笔操作,客户表存身份信息。余额用DECIMAL(15,2)而不是FLOAT,这是血泪经验——浮点数在多次加减后会出现 0.01 级别的误差,对账时能把你逼疯。

-- 建库,字符集用 utf8mb4,避免中文乱码 CREATE DATABASE bank_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE bank_db; -- 客户表 CREATE TABLE customer ( cust_id BIGINT PRIMARY KEY AUTO_INCREMENT, cust_name VARCHAR(50) NOT NULL, id_card VARCHAR(18) NOT NULL UNIQUE, phone VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB; -- 账户表,余额用 DECIMAL,状态 0 正常 1 冻结 CREATE TABLE account ( acc_id BIGINT PRIMARY KEY AUTO_INCREMENT, acc_no VARCHAR(19) NOT NULL UNIQUE, cust_id BIGINT NOT NULL, balance DECIMAL(15,2) NOT NULL DEFAULT 0.00, status TINYINT NOT NULL DEFAULT 0, version INT NOT NULL DEFAULT 0, CONSTRAINT fk_acc_cust FOREIGN KEY (cust_id) REFERENCES customer(cust_id) ) ENGINE=InnoDB; -- 交易流水表,记录转账双方和金额 CREATE TABLE transaction_record ( tx_id BIGINT PRIMARY KEY AUTO_INCREMENT, from_acc VARCHAR(19), to_acc VARCHAR(19), amount DECIMAL(15,2) NOT NULL, tx_type TINYINT NOT NULL, -- 1 转入 2 转出 3 存款 4 取款 tx_time DATETIME DEFAULT CURRENT_TIMESTAMP, remark VARCHAR(100) ) ENGINE=InnoDB;

逻辑说明:account表加了version字段,这是后面做乐观锁用的,先埋下。transaction_record不设外键到 account,因为流水是历史记录,账户可能被销户,流水不能跟着删。参数上,acc_no用 19 位字符串而不是数字,因为银行卡号有前导零,用 BIGINT 会丢。status用 TINYINT 而不是 BOOLEAN,方便以后扩展成多状态。

2.2 索引和约束:别等数据多了才补

期末作业数据量小,很多人不建索引也能跑。但银行系统里acc_no是高频查询字段,transaction_record的from_acc、to_acc、tx_time都会出现在查询条件里。我一般建三个索引:账户号唯一索引(建表时 UNIQUE 已带)、流水表的转出账户索引、流水表的时间索引。

-- 流水表按转出账户查 CREATE INDEX idx_tx_from ON transaction_record(from_acc); -- 流水表按时间范围查 CREATE INDEX idx_tx_time ON transaction_record(tx_time); -- 账户表按客户查 CREATE INDEX idx_acc_cust ON account(cust_id);

注意:索引不是越多越好,每多一个索引,插入就多一次维护成本。流水表是写多读少的表,索引控制在三个以内。如果你用 MySQL 5.7,utf8mb4下索引长度有限制,VARCHAR(19)没问题,但别把remark也加索引。

3. 在 Eclipse 里把 JDBC 跑通:从驱动到 DAO 的完整链路

3.1 工程结构与驱动引入

Eclipse 里新建Dynamic Web Project或者普通Java Project都行,我一般用普通 Java 项目加lib目录放 jar。MySQL 8.0 的驱动是mysql-connector-java-8.0.x.jar,5.7 用5.1.x,两者驱动类名不同:8.0 是com.mysql.cj.jdbc.Driver,5.7 是com.mysql.jdbc.Driver。这是第一个翻车点,写错就报ClassNotFoundException。

// DBUtil.java 连接工具类 import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { // 8.0 驱动写法,注意时区和 SSL 参数 private static final String URL = "jdbc:mysql://localhost:3306/bank_db?useUnicode=true&characterEncoding=utf8" + "&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true"; private static final String USER = "root"; private static final String PWD = "your_password"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new RuntimeException("MySQL 驱动未加载", e); } } public static Connection getConn() throws SQLException { return DriverManager.getConnection(URL, USER, PWD); } }

逻辑说明:serverTimezone必须设,否则 MySQL 8.0 会报时区错误;useSSL=false在本地开发时关掉,避免mysql ssl连接错误;allowPublicKeyRetrieval=true是 8.0 新认证方式需要的。参数上,characterEncoding=utf8配合库的utf8mb4,中文不会乱码。如果你用连接池,把DriverManager换成HikariDataSource,但期末作业用直连足够。

3.2 DAO 层:转账事务的写法

银行系统最核心的是转账,必须用事务保证「扣款」和「入账」要么都成功,要么都回滚。我一般把转账写在AccountDao.transfer()里,用setAutoCommit(false)手动控制。

// AccountDao.java 转账方法 public boolean transfer(String fromAcc, String toAcc, BigDecimal amount) { String outSql = "UPDATE account SET balance = balance - ? WHERE acc_no = ? AND balance >= ?"; String inSql = "UPDATE account SET balance = balance + ? WHERE acc_no = ?"; String logSql = "INSERT INTO transaction_record(from_acc,to_acc,amount,tx_type) VALUES(?,?,?,?)"; Connection conn = null; try { conn = DBUtil.getConn(); conn.setAutoCommit(false); // 开启事务 try (PreparedStatement ps1 = conn.prepareStatement(outSql); PreparedStatement ps2 = conn.prepareStatement(inSql); PreparedStatement ps3 = conn.prepareStatement(logSql)) { ps1.setBigDecimal(1, amount); ps1.setString(2, fromAcc); ps1.setBigDecimal(3, amount); // 余额不足则影响行数为 0 if (ps1.executeUpdate() == 0) { conn.rollback(); return false; } ps2.setBigDecimal(1, amount); ps2.setString(2, toAcc); ps2.executeUpdate(); ps3.setString(1, fromAcc); ps3.setString(2, toAcc); ps3.setBigDecimal(3, amount); ps3.setInt(4, 2); ps3.executeUpdate(); conn.commit(); return true; } } catch (SQLException e) { if (conn != null) try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } e.printStackTrace(); return false; } finally { if (conn != null) try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }

逻辑说明:扣款 SQL 里带balance >= ?,这是用数据库的原子性防止余额扣成负数,比先查再改更可靠。executeUpdate()返回 0 说明余额不足,直接回滚。参数上,金额用BigDecimal而不是double,和数据库DECIMAL对应。注意finally里恢复autoCommit再关闭,否则连接池回收后会带脏状态。

4. 并发转账与对账:把期末作业做出生产级可靠性

4.1 乐观锁与悲观锁怎么选

上面的事务写法在单线程下没问题,但两个线程同时给同一个账户转账,会出现「超卖」——余额被扣两次但只够一次。常见做法有两种:悲观锁SELECT ... FOR UPDATE,或者乐观锁用version字段。我一般期末作业用乐观锁,因为不阻塞、代码直观。

// 乐观锁版扣款:先查 version,更新时比对 public boolean deductWithVersion(String accNo, BigDecimal amount) { String q = "SELECT balance, version FROM account WHERE acc_no = ?"; String u = "UPDATE account SET balance = balance - ?, version = version + 1 " + "WHERE acc_no = ? AND version = ? AND balance >= ?"; try (Connection conn = DBUtil.getConn(); PreparedStatement ps = conn.prepareStatement(q)) { ps.setString(1, accNo); ResultSet rs = ps.executeQuery(); if (!rs.next()) return false; BigDecimal bal = rs.getBigDecimal("balance"); int ver = rs.getInt("version"); if (bal.compareTo(amount) < 0) return false; try (PreparedStatement up = conn.prepareStatement(u)) { up.setBigDecimal(1, amount); up.setString(2, accNo); up.setInt(3, ver); up.setBigDecimal(4, amount); return up.executeUpdate() == 1; // 返回 0 说明版本被改,重试 } } catch (SQLException e) { e.printStackTrace(); return false; } }

逻辑说明:更新时带version = ?,如果另一个线程已经改过,executeUpdate()返回 0,调用方可以重试。参数上,version每次加 1,不要用时间戳,时间戳在并发下可能重复。乐观锁适合冲突少的场景,如果冲突频繁,重试次数会上升,这时换FOR UPDATE更稳。

4.2 对账 SQL:验证数据一致性

做完转账,怎么知道数据没出错?我一般写一条对账 SQL,检查所有账户的余额是否等于「初始余额 + 转入 - 转出」。这是期末答辩时最能加分的细节。

-- 对账:按账户汇总流水,和账户余额比对 SELECT a.acc_no, a.balance AS real_balance, IFNULL(SUM(CASE WHEN t.to_acc = a.acc_no THEN t.amount ELSE 0 END), 0) - IFNULL(SUM(CASE WHEN t.from_acc = a.acc_no THEN t.amount ELSE 0 END), 0) AS flow_balance FROM account a LEFT JOIN transaction_record t ON t.from_acc = a.acc_no OR t.to_acc = a.acc_no GROUP BY a.acc_no, a.balance HAVING real_balance <> flow_balance;

逻辑说明:这条 SQL 返回的就是「账实不符」的账户。如果结果为空,说明流水和余额一致。参数上,IFNULL处理没有流水的账户。注意LEFT JOIN的条件用OR,在数据量大时性能差,可以拆成两个子查询再合并,但期末数据量下够用。

5. 避坑与排查:Eclipse + MySQL 最常见的五个翻车点

5.1 驱动类找不到或时区报错

现象:启动报ClassNotFoundException: com.mysql.cj.jdbc.Driver,或者The server time zone value '?D1ú±ê×?ê±??' is unrecognized。原因:jar 没加到 Build Path,或者 URL 里没写serverTimezone。解决:右键项目 → Build Path → Configure Build Path → Libraries → Add JARs,把驱动加进去;URL 补上serverTimezone=Asia/Shanghai。如果是 5.7 驱动,类名换成com.mysql.jdbc.Driver。

5.2 中文乱码

现象:插入的中文变成???,或者查询出来是乱码。原因:库、表、连接三处字符集不一致。解决:建库用utf8mb4,连接 URL 加characterEncoding=utf8,Eclipse 里 Java 文件编码设为 UTF-8(Window → Preferences → General → Workspace → Text file encoding)。三处都对了才不会乱。

5.3 事务没生效

现象:转账扣了款但没入账,或者异常后数据没回滚。原因:用了Connection但没关autoCommit,或者用了连接池但每次拿到的不是同一个连接。解决:确保setAutoCommit(false)和commit/rollback在同一个Connection上;用连接池时,事务内的所有操作必须共用同一个连接,不能每次getConn()。

5.4 余额出现小数误差

现象:多次转账后余额是999.9999999而不是1000.00。原因:Java 里用了double或float做金额运算。解决:Java 端统一用BigDecimal,数据库端统一用DECIMAL,BigDecimal的setScale(2, RoundingMode.HALF_UP)控制精度。

5.5 Eclipse 找不到主类或无法加载

现象:运行报找不到或无法加载主类。原因:src目录没被标记为源码目录,或者main方法签名写错。解决:右键src→ Build Path → Use as Source Folder;检查public static void main(String[] args)拼写。如果用了 Tomcat,检查org.apache.catalina.startup.bootstrap相关配置,但纯 Java 项目不涉及。

6. 进阶技巧:用存储过程把转账逻辑下沉到数据库

期末作业写到上面已经能拿高分,但如果你想再进一步,可以把转账逻辑写成 MySQL 存储过程,Java 只负责调用。好处是减少网络往返,事务边界更清晰。我一般用IN参数传账户和金额,OUT参数返回结果码。

DELIMITER $$ CREATE PROCEDURE sp_transfer( IN p_from VARCHAR(19), IN p_to VARCHAR(19), IN p_amt DECIMAL(15,2), OUT p_code INT ) BEGIN DECLARE v_bal DECIMAL(15,2); DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; SET p_code = -1; END; START TRANSACTION; SELECT balance INTO v_bal FROM account WHERE acc_no = p_from FOR UPDATE; IF v_bal IS NULL OR v_bal < p_amt THEN SET p_code = -2; -- 余额不足 ROLLBACK; ELSE UPDATE account SET balance = balance - p_amt WHERE acc_no = p_from; UPDATE account SET balance = balance + p_amt WHERE acc_no = p_to; INSERT INTO transaction_record(from_acc,to_acc,amount,tx_type) VALUES(p_from, p_to, p_amt, 2); COMMIT; SET p_code = 0; -- 成功 END IF; END$$ DELIMITER ;

Java 端用CallableStatement调用:

// 调用存储过程 public int transferByProc(String from, String to, BigDecimal amt) { String sql = "{call sp_transfer(?,?,?,?)}"; try (Connection conn = DBUtil.getConn(); CallableStatement cs = conn.prepareCall(sql)) { cs.setString(1, from); cs.setString(2, to); cs.setBigDecimal(3, amt); cs.registerOutParameter(4, Types.INTEGER); cs.execute(); return cs.getInt(4); // 0 成功,-1 异常,-2 余额不足 } catch (SQLException e) { e.printStackTrace(); return -1; } }

逻辑说明:存储过程里用FOR UPDATE加行锁,保证并发下余额正确。EXIT HANDLER捕获异常后回滚并返回 -1。参数上,p_code用OUT返回,Java 端registerOutParameter注册类型。注意 MySQL 存储过程调试不如 Java 方便,建议先在 SQL 客户端里测通再集成。

验证方法:开两个终端,同时调用transferByProc给同一账户转账,观察最终余额是否等于初始值减去两笔金额。如果不等,检查FOR UPDATE是否生效(需要 InnoDB 引擎)。我自己的习惯是每次改完事务逻辑,先跑一遍对账 SQL,再跑并发测试,最后才提交代码。这套流程帮我省了无数次返工。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询