☰
Java Socket长连接+Oracle实现银行排号系统:从协议设计到存储过程原子叫号
2026/10/9 9:04:54 网站建设 项目流程

简介:这份资源面向学习Java网络编程与桌面应用开发的学生及开发者,提供一套基于Socket通信与Java GUI实现的银行排号系统完整项目,可用于课程设计、毕业设计或相关项目参考。压缩包为zip格式,整体约292.61MB,内含项目全套源码与完整文档,源码经过测试校正,可百分百成功运行,文档则用于说明系统设计与实现思路。项目围绕银行排队叫号场景,涉及客户端与服务端通信、界面交互、号码分配与队列管理等模块,能帮助读者理解Socket网络编程与Swing图形界面结合的实际应用方式。目前已有475人学习下载,适合需要参考完整实现方案、梳理项目结构或进行二次开发的学习者,可从中获取可运行的代码框架与配套设计文档,作为项目设计与实现的对照材料。

1. 银行排号系统:从 Socket 长连接到 Oracle 取号的完整落地路径

柜台前排长队、客户反复问“前面还有几个人”、大堂经理拿本子手写登记——这是很多中小网点还在用的排号方式。基于 Java + Socket + Java GUI 的银行排号系统,本质是把“取号、叫号、窗口状态、队列数据”四件事拆成 C/S 两端:客户端负责取号和展示,服务端负责队列调度和持久化,中间用 Socket 长连接做实时推送。它解决的是“多窗口并发叫号时数据不一致”和“客户不知道要等多久”这两个具体问题,适合做课程设计、毕业设计,也适合想练手 Java 网络编程 + Swing 界面 + Oracle 落库的工程师。下面按“协议怎么定 → 服务端怎么扛并发 → 客户端怎么不卡 → 数据怎么落 Oracle → 坑在哪”的顺序讲透。

2. 通信协议与线程模型:Socket 长连接怎么定才不翻车

2.1 为什么不用 HTTP 轮询而选原生 Socket

银行排号场景有一个硬需求:叫号必须实时。客户坐在等候区,窗口按“下一位”后,大屏和客户端要在 1 秒内更新。如果用 HTTP 轮询,2 秒一次,高峰期 50 个客户端就是 25 QPS,看着不高,但每次都要走完整请求-响应,服务端线程频繁创建销毁,Oracle 连接池也容易被拖垮。原生 Socket 长连接的优势在于:连接建立一次,之后只传业务报文,服务端可以主动 push 叫号事件,延迟能压到毫秒级。

常见做法是自定义一个简单的文本协议,用换行符分隔报文,字段用|分隔。比如取号请求TAKE|普通业务|张三,服务端回TAKEN|A023|前面还有5人。这种协议的好处是调试方便,Telnet 就能测;坏处是字段里不能出现|和换行,所以客户姓名要做转义或限制字符集。我一般会在协议头加一个 4 字节的长度字段,改成“长度 + 内容”的二进制帧,避免粘包问题,但课程设计里用BufferedReader.readLine()按行读也能跑通,只要约定每条报文以\n结尾。

2.2 服务端线程模型:一连接一线程够不够用

一个网点同时在线客户端通常不超过 100 个(取号机 + 窗口屏 + 等候区大屏),一连接一线程完全够用。核心结构是:ServerSocket监听端口,accept()到连接后丢给线程池,每个连接对应一个ClientHandler,持有Socket、BufferedReader、PrintWriter。所有ClientHandler共享一个TicketQueue(阻塞队列)和一个WindowManager(窗口状态表)。

// 服务端主循环:线程池 + 共享队列 public class BankServer { private static final int PORT = 9527; // 固定大小线程池,避免高峰期无限创建线程 private static final ExecutorService POOL = Executors.newFixedThreadPool(50); // 全局取号队列,LinkedBlockingQueue 保证并发安全 public static final BlockingQueue<Ticket> QUEUE = new LinkedBlockingQueue<>(); // 窗口状态:窗口号 -> 当前票号 public static final ConcurrentHashMap<Integer, String> WINDOWS = new ConcurrentHashMap<>(); public static void main(String[] args) throws IOException { ServerSocket server = new ServerSocket(PORT); System.out.println("银行排号服务端启动,端口:" + PORT); while (true) { Socket socket = server.accept(); socket.setSoTimeout(0); // 长连接不设读超时 POOL.execute(new ClientHandler(socket)); } } }

逻辑说明:newFixedThreadPool(50)限制最大并发处理线程数,防止连接数暴涨时 JVM 线程栈耗尽内存。LinkedBlockingQueue作为取号队列,take()和put()自带阻塞语义,窗口叫号时直接poll()取下一个,不需要额外加锁。ConcurrentHashMap存窗口状态,多个窗口线程同时更新不会出现脏读。参数上,端口选 9527 只是习惯,实际部署要避开 Oracle 监听常用的 1521 和 8080。setSoTimeout(0)表示读操作永不超时,适合长连接;如果要做心跳检测,可以设成 30000 毫秒,超时后发心跳包。

2.3 报文格式与粘包处理

TCP 是字节流,没有消息边界。如果客户端连续发两条TAKE|...,服务端一次read()可能全读出来。用BufferedReader.readLine()按\n切分是最省事的方案,前提是每条报文末尾必须加\n,且内容里不能有\n。如果客户姓名里真有换行,先做replace("\n", " ")。另一种方案是定长头:前 4 字节是 int 类型的 body 长度,后面跟 body。下面是一个按行读的ClientHandler核心片段:

public class ClientHandler implements Runnable { private final Socket socket; private BufferedReader in; private PrintWriter out; public ClientHandler(Socket socket) { this.socket = socket; } @Override public void run() { try { in = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8")); out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), "UTF-8"), true); String line; while ((line = in.readLine()) != null) { // 按 | 切分,第一个字段是命令字 String[] parts = line.split("\\|"); String cmd = parts[0]; switch (cmd) { case "TAKE": // 取号:业务类型 | 客户姓名 String bizType = parts[1]; String name = parts[2]; Ticket t = TicketService.take(bizType, name); out.println("TAKEN|" + t.getNumber() + "|" + TicketService.aheadCount(t)); break; case "CALL": // 叫号:窗口号 int win = Integer.parseInt(parts[1]); Ticket next = TicketService.callNext(win); if (next != null) { broadcast("CALLED|" + win + "|" + next.getNumber()); } break; case "HEARTBEAT": out.println("PONG"); break; default: out.println("ERROR|未知命令"); } } } catch (IOException e) { System.out.println("客户端断开:" + socket.getRemoteSocketAddress()); } finally { try { socket.close(); } catch (IOException ignored) {} } } // 广播给所有在线客户端,大屏和窗口屏都能收到 private void broadcast(String msg) { for (ClientHandler h : ServerHolder.ALL_CLIENTS) { h.out.println(msg); } } }

逻辑说明:in.readLine()阻塞直到读到\n,天然解决粘包。split("\\|")按竖线切分,注意|在正则里要转义。TAKE命令返回票号和前面等待人数,CALL命令触发叫号并广播给所有客户端。broadcast遍历所有在线ClientHandler,这里用了一个ServerHolder.ALL_CLIENTS静态集合保存所有连接,实际项目里可以用CopyOnWriteArrayList避免并发修改异常。参数上,PrintWriter的第二个参数true表示自动 flush,否则消息会卡在缓冲区里,客户端半天收不到——这是新手最容易踩的坑之一。

3. Java GUI 客户端:Swing 界面怎么做到不卡界面

3.1 取号界面与叫号大屏的布局拆分

客户端分两种角色:取号机(触摸屏,只有取号按钮和票号显示)和窗口屏(显示当前叫号、等待列表)。用 Swing 做,取号机用JFrame+GridLayout放几个大按钮,窗口屏用JFrame+BorderLayout,中间放JLabel显示当前票号,字号调到 72 以上,保证三米外能看清。下面是一个取号机界面的最小实现:

public class TakeNumberUI extends JFrame { private JLabel ticketLabel; // 显示取到的票号 private SocketClient client; // 封装的 Socket 客户端 public TakeNumberUI() { setTitle("银行取号机"); setSize(600, 400); setDefaultCloseOperation(EXIT_ON_CLOSE); setLayout(new BorderLayout()); ticketLabel = new JLabel("请点击下方按钮取号", SwingConstants.CENTER); ticketLabel.setFont(new Font("微软雅黑", Font.BOLD, 36)); add(ticketLabel, BorderLayout.CENTER); JPanel btnPanel = new JPanel(new GridLayout(1, 3, 10, 10)); String[] bizTypes = {"普通业务", "对公业务", "VIP业务"}; for (String biz : bizTypes) { JButton btn = new JButton(biz); btn.setFont(new Font("微软雅黑", Font.PLAIN, 24)); btn.addActionListener(e -> { // 点击后发 TAKE 命令,注意在后台线程发,别阻塞 EDT new Thread(() -> { String resp = client.send("TAKE|" + biz + "|匿名"); SwingUtilities.invokeLater(() -> ticketLabel.setText(resp)); }).start(); }); btnPanel.add(btn); } add(btnPanel, BorderLayout.SOUTH); setVisible(true); } }

逻辑说明:Swing 是单线程模型,所有界面更新必须在事件分发线程(EDT)里做。btn.addActionListener里的代码默认跑在 EDT 上,如果直接调client.send(),网络阻塞会让整个界面卡死,按钮点不动。所以这里用new Thread(...)把网络请求丢到后台,拿到响应后再用SwingUtilities.invokeLater切回 EDT 更新ticketLabel。参数上,GridLayout(1, 3, 10, 10)表示 1 行 3 列,按钮间距 10 像素。字体大小根据屏幕分辨率调,触摸屏一般 24 号以上。

3.2 用 SwingWorker 处理 Socket 接收,避免 EDT 阻塞

窗口屏需要持续接收服务端广播的叫号消息。如果直接在 EDT 里写while(true) { in.readLine(); },界面直接假死。正确做法是用SwingWorker或独立线程接收,收到消息后invokeLater更新 UI。下面是一个窗口屏的接收循环:

public class WindowScreenUI extends JFrame { private JLabel currentLabel; // 当前叫号 private JTextArea waitingArea; // 等待列表 private SocketClient client; public WindowScreenUI(int windowNo) { setTitle("窗口 " + windowNo); setSize(800, 600); setLayout(new BorderLayout()); currentLabel = new JLabel("暂无叫号", SwingConstants.CENTER); currentLabel.setFont(new Font("微软雅黑", Font.BOLD, 96)); currentLabel.setForeground(Color.RED); add(currentLabel, BorderLayout.CENTER); waitingArea = new JTextArea(); waitingArea.setFont(new Font("微软雅黑", Font.PLAIN, 20)); waitingArea.setEditable(false); add(new JScrollPane(waitingArea), BorderLayout.EAST); // 叫号按钮 JButton callBtn = new JButton("下一位"); callBtn.setFont(new Font("微软雅黑", Font.PLAIN, 28)); callBtn.addActionListener(e -> new Thread(() -> client.send("CALL|" + windowNo)).start()); add(callBtn, BorderLayout.SOUTH); // 启动接收线程,持续监听服务端广播 new Thread(() -> { while (true) { String msg = client.receive(); if (msg == null) break; String[] parts = msg.split("\\|"); if ("CALLED".equals(parts[0])) { String win = parts[1]; String ticket = parts[2]; SwingUtilities.invokeLater(() -> { if (String.valueOf(windowNo).equals(win)) { currentLabel.setText(ticket); } waitingArea.append("窗口" + win + " 叫号:" + ticket + "\n"); }); } } }).start(); setVisible(true); } }

逻辑说明:接收线程独立于 EDT,client.receive()阻塞读,收到消息后解析命令字。如果是CALLED且窗口号匹配自己,更新currentLabel;所有窗口屏都往waitingArea追加日志。SwingUtilities.invokeLater保证 UI 更新在 EDT 上执行。参数上,currentLabel字号 96 是为了大屏展示,实际部署时窗口屏可能是一台 55 寸电视,字号还要再调大。waitingArea放在JScrollPane里,消息多了可以滚动查看。

3.3 心跳与断线重连:别让客户端变成“僵尸”

长连接最怕的是网络闪断后客户端不知道,还在傻等。常见做法是客户端每 30 秒发一个HEARTBEAT,服务端回PONG;如果连续 3 次没收到PONG,就认为连接断了,触发重连。重连逻辑要放在独立线程里,重连成功后重新发送登录/注册命令。下面是一个简单的心跳实现:

public class SocketClient { private Socket socket; private BufferedReader in; private PrintWriter out; private volatile boolean connected = false; public void connect(String host, int port) throws IOException { socket = new Socket(host, port); socket.setSoTimeout(35000); // 读超时 35 秒,比心跳间隔略长 in = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8")); out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), "UTF-8"), true); connected = true; startHeartbeat(); } private void startHeartbeat() { new Thread(() -> { while (connected) { try { Thread.sleep(30000); out.println("HEARTBEAT"); } catch (InterruptedException e) { break; } } }).start(); } public String send(String msg) { out.println(msg); try { return in.readLine(); } catch (IOException e) { connected = false; return "ERROR|连接断开"; } } public String receive() { try { return in.readLine(); } catch (IOException e) { connected = false; return null; } } }

逻辑说明:setSoTimeout(35000)表示读操作最多阻塞 35 秒,超过就抛SocketTimeoutException,触发重连逻辑。心跳线程每 30 秒发一次HEARTBEAT,服务端回PONG,客户端在receive()里读到PONG就忽略。如果 35 秒内没读到任何数据,readLine()抛异常,connected置 false,上层可以捕获后重新connect()。参数上,心跳间隔和读超时要配合:读超时 > 心跳间隔,否则正常心跳也会触发超时。常见值是心跳 30 秒、读超时 35 秒。

4. Oracle 落库:票号表、窗口表和队列状态怎么设计

4.1 三张核心表与序列生成票号

Oracle 里建三张表:TICKET(票号记录)、WINDOW_INFO(窗口状态)、QUEUE_LOG(叫号日志)。票号用序列SEQ_TICKET生成,格式化成A001、A002。下面是对应的 DDL:

-- 票号表:记录每一次取号 CREATE TABLE TICKET ( TICKET_ID NUMBER PRIMARY KEY, TICKET_NO VARCHAR2(10) NOT NULL, -- 如 A023 BIZ_TYPE VARCHAR2(20) NOT NULL, -- 业务类型 CUSTOMER VARCHAR2(50), -- 客户姓名 STATUS NUMBER(1) DEFAULT 0, -- 0等待 1已叫 2完成 CREATE_TIME DATE DEFAULT SYSDATE, CALL_TIME DATE ); -- 窗口表:记录窗口当前状态 CREATE TABLE WINDOW_INFO ( WINDOW_NO NUMBER PRIMARY KEY, -- 窗口号 CURRENT_NO VARCHAR2(10), -- 当前叫的票号 STATUS NUMBER(1) DEFAULT 0, -- 0空闲 1服务中 UPDATE_TIME DATE DEFAULT SYSDATE ); -- 序列:生成票号数字部分 CREATE SEQUENCE SEQ_TICKET START WITH 1 INCREMENT BY 1 NOCACHE; -- 叫号日志:用于统计和审计 CREATE TABLE QUEUE_LOG ( LOG_ID NUMBER PRIMARY KEY, WINDOW_NO NUMBER, TICKET_NO VARCHAR2(10), CALL_TIME DATE DEFAULT SYSDATE );

逻辑说明:TICKET.STATUS用 0/1/2 表示等待、已叫、完成,查询等待队列时WHERE STATUS = 0 ORDER BY CREATE_TIME。SEQ_TICKET用NOCACHE避免 Oracle 重启后序列跳号,但高并发下NOCACHE会有性能损耗;如果取号量不大(银行网点一天几百个号),NOCACHE完全够用。票号格式化在 Java 里做:String.format("A%03d", seq)。WINDOW_INFO的CURRENT_NO在叫号时更新,窗口屏启动时先查这张表恢复状态。

4.2 Java 侧 JDBC 写入与事务边界

服务端每次取号要同时做两件事:往TICKET插一条记录,返回票号给客户端。这两步必须在同一个事务里,否则可能出现“票号给了但没落库”的情况。下面是一个取号服务的 JDBC 实现:

public class TicketService { private static final String URL = "jdbc:oracle:thin:@localhost:1521:ORCL"; private static final String USER = "bank"; private static final String PWD = "bank123"; public static Ticket take(String bizType, String name) throws SQLException { String ticketNo = null; try (Connection conn = DriverManager.getConnection(URL, USER, PWD)) { conn.setAutoCommit(false); // 开启事务 // 1. 取序列下一个值 try (PreparedStatement ps = conn.prepareStatement("SELECT SEQ_TICKET.NEXTVAL FROM DUAL"); ResultSet rs = ps.executeQuery()) { rs.next(); int seq = rs.getInt(1); ticketNo = String.format("A%03d", seq); } // 2. 插入票号记录 try (PreparedStatement ps = conn.prepareStatement( "INSERT INTO TICKET(TICKET_ID, TICKET_NO, BIZ_TYPE, CUSTOMER, STATUS) " + "VALUES(SEQ_TICKET.CURRVAL, ?, ?, ?, 0)")) { ps.setString(1, ticketNo); ps.setString(2, bizType); ps.setString(3, name); ps.executeUpdate(); } conn.commit(); // 提交事务 } return new Ticket(ticketNo, bizType, name); } }

逻辑说明:conn.setAutoCommit(false)关闭自动提交,两步操作要么都成功要么都回滚。SEQ_TICKET.NEXTVAL取下一个序列值,CURRVAL在同一会话里取当前值,保证TICKET_ID和TICKET_NO对应同一个序列号。try-with-resources自动关闭Connection、PreparedStatement、ResultSet,避免连接泄漏。参数上,Oracle 的 JDBC URL 格式是jdbc:oracle:thin:@主机:端口:SID,如果是服务名模式则用jdbc:oracle:thin:@//主机:端口/服务名。生产环境要用连接池(如 HikariCP),不要每次DriverManager.getConnection,否则并发一高就崩。

4.3 叫号时的队列查询与状态更新

窗口按“下一位”时,服务端要从TICKET表里查最早等待的记录,更新状态为已叫,同时更新WINDOW_INFO。这三步也要在一个事务里。下面是对应的 SQL 和 Java 调用:

-- 查询最早等待的票号(Oracle 分页写法) SELECT TICKET_NO, BIZ_TYPE, CUSTOMER FROM ( SELECT TICKET_NO, BIZ_TYPE, CUSTOMER FROM TICKET WHERE STATUS = 0 ORDER BY CREATE_TIME ASC ) WHERE ROWNUM = 1; -- 更新票号状态 UPDATE TICKET SET STATUS = 1, CALL_TIME = SYSDATE WHERE TICKET_NO = ?; -- 更新窗口当前叫号 UPDATE WINDOW_INFO SET CURRENT_NO = ?, STATUS = 1, UPDATE_TIME = SYSDATE WHERE WINDOW_NO = ?;

逻辑说明:Oracle 分页用ROWNUM,注意ROWNUM是在ORDER BY之前分配的,所以要先子查询排序再取ROWNUM = 1。如果直接写WHERE STATUS = 0 AND ROWNUM = 1 ORDER BY CREATE_TIME,取到的是物理存储顺序的第一条,不是最早的。这是 Oracle 分页的经典坑。更新TICKET和WINDOW_INFO要在同一个事务里,conn.commit()之前任何一步失败都要rollback()。参数上,SYSDATE是 Oracle 数据库服务器时间,如果应用服务器和数据库服务器时区不一致,建议用SYSTIMESTAMP或 Java 侧传new java.sql.Timestamp(System.currentTimeMillis())。

5. 避坑与排查:银行排号系统上线前必须过的 5 道坎

5.1 现象:客户端点取号没反应,服务端日志也不报错

原因:PrintWriter没有自动 flush,消息卡在缓冲区里。new PrintWriter(outputStream)默认不自动刷新,只有println且构造时传了true才会 flush。解决:构造PrintWriter时第二个参数传true,或者每次println后手动out.flush()。这个坑在本地测试时不容易发现,因为数据量小,缓冲区没满;一旦并发上来,消息延迟几秒才发出去,客户以为机器坏了。

5.2 现象:Oracle 监听服务无法启动,JDBC 连不上

原因:监听日志文件过大(listener.log超过 4GB),或者listener.ora配置被改坏。解决:先停监听lsnrctl stop,清理$ORACLE_HOME/network/log/listener.log,再lsnrctl start。如果是listener.ora问题,检查SID_LIST_LISTENER里有没有注册数据库实例。Windows 上还要看Windows Socket error: 通常每个套接字地址只允许使用一次,这通常是 1521 端口被其他进程占用,用netstat -ano | findstr 1521找到 PID 后结束进程。

5.3 现象:叫号后大屏更新了,但窗口屏没收到

原因:broadcast遍历客户端列表时,某个ClientHandler的out已经断开,println抛异常导致循环中断,后面的客户端收不到消息。解决:broadcast里对每个客户端单独 try-catch,断开的连接从列表里移除,不要因为一个坏连接影响其他客户端。另外,ALL_CLIENTS要用CopyOnWriteArrayList,避免遍历时其他线程增删连接导致ConcurrentModificationException。

5.4 现象:票号重复,两个客户拿到同一个号

原因:票号生成用了SELECT MAX(TICKET_NO) + 1而不是序列,并发下两个线程读到同一个最大值。解决:用 Oracle 序列SEQ_TICKET.NEXTVAL,序列的NEXTVAL是原子操作,不会重复。如果非要用表存当前票号,必须SELECT ... FOR UPDATE锁行,但性能差很多。序列是银行排号系统里最稳妥的方案。

5.5 现象:Swing 界面点按钮后卡死,鼠标转圈

原因:网络请求或数据库查询写在了 EDT 上。Swing 的所有事件回调默认在 EDT 执行,任何阻塞操作都会冻结界面。解决:所有耗时操作(Socket 收发、JDBC 查询)放到独立线程或SwingWorker里,UI 更新用SwingUtilities.invokeLater切回 EDT。判断标准很简单:如果按钮点下去超过 200 毫秒没反应,就是 EDT 被阻塞了。

6. 进阶技巧:用 Oracle 存储过程做叫号原子操作与压测验证

前面叫号是“查最早等待 + 更新状态 + 更新窗口”三条 SQL,在 Java 里拼事务。并发高的时候,两个窗口可能同时查到同一条等待记录,然后都去更新,导致同一个号被叫两次。虽然UPDATE有行锁,但第二个窗口的UPDATE会等待第一个提交后才执行,此时TICKET_NO已经被改成STATUS = 1,第二个窗口的UPDATE影响行数为 0,需要在 Java 里判断executeUpdate()返回值,如果为 0 就重新查。更优雅的做法是把叫号逻辑封装成 Oracle 存储过程,利用数据库的原子性一次性完成。

CREATE OR REPLACE PROCEDURE PROC_CALL_NEXT( P_WINDOW_NO IN NUMBER, P_TICKET_NO OUT VARCHAR2 ) AS V_TICKET_NO TICKET.TICKET_NO%TYPE; BEGIN -- 锁定最早等待的记录,FOR UPDATE 防止并发抢号 SELECT TICKET_NO INTO V_TICKET_NO FROM TICKET WHERE STATUS = 0 AND ROWNUM = 1 ORDER BY CREATE_TIME ASC FOR UPDATE SKIP LOCKED; -- 跳过已被其他会话锁定的行 -- 更新票号状态 UPDATE TICKET SET STATUS = 1, CALL_TIME = SYSDATE WHERE TICKET_NO = V_TICKET_NO; -- 更新窗口状态 UPDATE WINDOW_INFO SET CURRENT_NO = V_TICKET_NO, STATUS = 1, UPDATE_TIME = SYSDATE WHERE WINDOW_NO = P_WINDOW_NO; P_TICKET_NO := V_TICKET_NO; COMMIT; EXCEPTION WHEN NO_DATA_FOUND THEN P_TICKET_NO := NULL; -- 没有等待的票号 ROLLBACK; END;

逻辑说明:FOR UPDATE SKIP LOCKED是 Oracle 处理并发队列的利器。窗口 A 锁定了第一条等待记录,窗口 B 执行同样的查询时会跳过被锁定的行,直接取第二条,不会互相等待。NO_DATA_FOUND异常表示队列为空,返回NULL给 Java 侧,窗口屏显示“暂无等待客户”。存储过程里COMMIT放在最后,保证三步操作原子提交。Java 侧调用:

try (CallableStatement cs = conn.prepareCall("{call PROC_CALL_NEXT(?, ?)}")) { cs.setInt(1, windowNo); cs.registerOutParameter(2, Types.VARCHAR); cs.execute(); String ticketNo = cs.getString(2); if (ticketNo == null) { // 队列为空,提示窗口 } else { // 广播叫号 } }

参数上,registerOutParameter(2, Types.VARCHAR)注册输出参数类型,cs.getString(2)取存储过程返回的票号。存储过程的优势是把并发控制下沉到数据库,Java 侧不用管事务边界和重试逻辑,代码更干净。

压测验证怎么做:用 JMeter 或自己写一个多线程 Java 客户端,模拟 20 个窗口同时叫号、50 个客户端同时取号。观察三个指标:票号有没有重复、等待队列长度是否单调递减、Oracle 的v$session里有没有长时间持有锁的会话。如果发现enq: TX - row lock contention等待事件,说明FOR UPDATE锁竞争严重,可以调大SKIP LOCKED的粒度或改用QUEUE_LOG表做无锁队列。我自己的习惯是:每次改完叫号逻辑,先跑 1000 次并发取号 + 叫号,确认票号从 A001 到 A1000 连续无重复,再上生产。这个方案值不值得做,取决于你对“叫号不重复”这件事的容忍度——银行场景里,重复叫号是事故,所以存储过程这层保险值得加。希望帮到你。

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

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

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

立即咨询