news 2026/9/4 3:54:58

Java WebSocket与Redis实战:构建高并发银行排号系统

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java WebSocket与Redis实战:构建高并发银行排号系统

简介:本资源是一套完整的基于Java开发的银行排号系统实战项目,面向Java初学者、课程设计学生及中小型业务系统开发者,旨在解决银行、政务大厅等服务场所排队混乱、效率低下、客户体验差等现实问题。项目采用C/S架构,融合Swing图形界面、Socket网络通信、多线程并发控制与MySQL数据库技术,完整覆盖需求分析、系统设计(含用例图、流程图、E-R图)、编码实现与测试验证全流程。压缩包为RAR格式,大小69.98MB,包含源码工程、开题与毕业设计风格的LW文档(含技术原理、数据库设计、模块实现详解)、答辩用PPT、SQL建库脚本及系统操作讲解视频,内容环环相扣,便于理解架构逻辑与调试要点。目前已有150人学习下载,特别适合需要交付课程设计、夯实Java GUI+网络+数据库综合能力的学习者快速上手并拓展至其他叫号类应用场景。

1. 项目概述:从“排队”到“智慧服务”的进化

每次去银行办业务,最头疼的就是取号后那漫长的等待。你不知道前面有多少人,不知道要等多久,只能盯着那不断跳动的数字屏幕,心里干着急。这种体验,相信大家都不陌生。而一个设计良好的银行排号系统,正是为了解决这个核心痛点而生的。它不仅仅是一个简单的“叫号机”,而是一个集成了客户分流、业务预判、窗口调度和数据分析的综合服务平台。今天,我想和大家深入聊聊,如何从零开始,用Java这门经典且强大的语言,亲手搭建一个功能完备、逻辑清晰的银行排号系统。这个项目麻雀虽小,五脏俱全,涵盖了从后端业务逻辑、数据库设计到前端交互的完整流程,非常适合作为Java Web开发的综合练手项目,也能让你深刻理解一个看似简单的系统背后复杂的设计考量。

这个系统要解决的核心问题是什么?首先是秩序与效率。通过电子化取号,杜绝了物理排队可能引发的插队、拥挤问题,营造了公平、有序的办理环境。其次是体验与透明。客户取号后,可以实时查看当前等待人数、预估等待时间,并能通过显示屏或语音清晰获知叫号信息,焦虑感大大降低。最后是管理与优化。银行管理者可以通过系统后台,实时监控各窗口业务量、客户等待时长、业务类型分布等数据,为人力资源的动态调配、服务流程的优化提供数据支撑。所以,我们构建的不仅是一个工具,更是一个提升银行网点服务质量和运营效率的智慧节点。

2. 系统核心设计与架构思路拆解

在动手写代码之前,我们必须把系统的骨架——架构设计清楚。一个好的架构能让后续开发事半功倍,也决定了系统的可维护性和扩展性。

2.1 技术栈选型:为什么是这些组合?

对于一个典型的Java Web项目,技术选型需要兼顾成熟度、开发效率和团队技能。基于当前的主流实践,我选择了以下组合:

  • 后端核心:Spring Boot + Spring MVC + MyBatis (SSM框架集成)。这是Java企业级开发的事实标准。Spring Boot极大地简化了初始配置,让我们能快速搭建一个可独立运行的、生产级别的应用。Spring MVC提供了清晰的分层模型(Controller, Service, Dao),MyBatis则是一个半自动化的ORM框架,它在SQL的灵活性和对象映射的便利性之间取得了很好的平衡。对于排号系统这种业务逻辑明确、数据库操作频繁的场景,MyBatis比JPA(Hibernate)更具掌控力。
  • 数据库:MySQL。作为最流行的开源关系型数据库,MySQL在事务一致性、并发处理和社区支持方面都非常出色。排号系统的数据模型并不特别复杂,但要求高可靠性和快速的读写响应(如频繁的取号、叫号更新),MySQL完全能够胜任。后续如果数据量激增,也可以考虑分库分表等优化方案。
  • 前端展示:Thymeleaf + Bootstrap + jQuery。考虑到这是一个偏向内部管理和现场展示的系统,对前端交互的实时性要求高但复杂度适中,我没有选择重量级的前端框架(如Vue/React),而是采用了服务端渲染模板Thymeleaf。它的好处是能与Spring Boot无缝集成,开发简单,页面直出速度快。Bootstrap提供了现成的、响应式的UI组件,能快速搭建出美观专业的后台管理界面和客户显示界面。jQuery则用于处理一些简单的DOM操作和Ajax请求,比如实现叫号信息的局部刷新。
  • 关键中间件与工具
    • WebSocket:这是实现叫号信息实时推送的关键。传统的HTTP轮询(不断向服务器询问“轮到我了吗?”)效率低下且延迟高。当柜员点击“叫号”时,服务器通过WebSocket连接,可以瞬间将“请A001号到3号窗口”这条消息推送到大堂的显示屏和客户的手机端(如果有的话),实现真正的实时更新。
    • Redis:用作缓存和分布式锁。高频查询的数据,如当前各队列的等待人数、最新的叫号信息,可以缓存在Redis中,极大减轻数据库压力。更重要的是,在并发取号时,为了防止出现重号(两个客户同时取到同一个号),我们需要一个分布式锁机制。Redis的SETNX命令是实现轻量级分布式锁的经典方案。
    • Maven:项目构建和依赖管理工具,让第三方库的引入和管理变得井然有序。

注意:技术选型没有绝对的对错,只有是否适合当前场景。如果你的团队更熟悉 Vue,完全可以用 Spring Boot 提供 RESTful API,前端用 Vue 来开发,这样前后端分离更彻底。这里的选择是基于“快速实现一个完整可用的系统”这一目标。

2.2 业务模块划分:系统由哪些部分组成?

根据业务流程,我们可以将系统清晰地划分为以下几个核心模块:

  1. 取号模块:客户交互的起点。提供取号界面(可以是触摸屏、小程序入口等),客户选择需要办理的业务类型(如个人现金、对公业务、理财咨询等)。系统根据业务类型,将其归入不同的队列,并生成一个唯一的号码(如A001,B023)。
  2. 队列管理模块:系统的“大脑”。它维护着各个业务队列的状态,包括队列中的号码列表、当前办理的号码、等待人数等。负责接收取号请求,分配号码;接收叫号请求,从队列中取出下一个号码。
  3. 叫号与显示模块:柜员与客户交互的桥梁。柜员端有一个操作界面,登录后可以看到分配给自己的队列,点击“叫号”按钮,系统会从对应队列中取出最前面的号码。取出的号码信息会通过WebSocket实时推送到大堂的LED显示屏、柜员窗口的显示屏以及可能的语音播报系统。
  4. 柜员管理模块:管理办理业务的柜员信息。包括柜员登录、权限分配(普通柜员、经理)、状态管理(空闲、忙碌、暂停服务)、以及柜员与可办理业务类型的关联。
  5. 后台管理模块:提供给银行管理人员使用。功能包括:实时监控(各队列情况、柜员状态)、数据统计(日/月业务量、平均等待时间、客户满意度)、基础数据配置(业务类型管理、窗口管理)等。
  6. 数据统计与分析模块:这个模块可能不像前几个那样有直接的界面,但它至关重要。它负责定时或触发式地分析历史排号数据,生成报表,帮助管理者发现业务高峰时段、评估柜员效率、优化窗口资源配置。

2.3 数据库设计:表结构如何支撑业务?

数据库设计是系统的基石。这里我列出几个核心表及其字段,并解释设计意图:

  • ticket(排号票表):核心实体。
    • id(主键),ticket_number(票号,如 ‘A001’),business_type(业务类型),status(状态:等待、办理中、已办理、过号),create_time(取号时间),call_time(叫号时间),start_process_time(开始办理时间),end_process_time(办理完成时间),counter_id(关联的窗口柜员)。
    • 设计思考status字段是流程驱动的关键。call_timestart_process_time可能不同,因为叫号后客户可能未及时前来。记录完整的时间戳,为后续分析等待时长、办理时长提供了数据基础。
  • business_type(业务类型表):用于配置。
    • id,type_code(类型代码,如 ‘PERSONAL’),type_name(类型名称,如 ‘个人现金业务’),prefix(号票前缀,如 ‘A’),description,estimated_time(预估办理时长,分钟)。
    • 设计思考:将业务类型独立成表,便于动态管理。prefix用于生成不同队列的票号。estimated_time可以用于在前端为客户提供更精准的等待时间预估。
  • counter(窗口/柜员表)
    • id,counter_number(窗口编号),clerk_id(关联的柜员ID),current_status(当前状态:空闲、忙碌、暂停),current_ticket_id(当前正在处理的票ID)。
    • 设计思考:将物理窗口与柜员账号解耦,一个柜员可以登录到不同窗口。current_ticket_id直接关联到正在办理的业务,方便追踪。
  • clerk(柜员表)
    • id,username,password(加密存储),real_name,role(角色:柜员、经理)。
  • display(显示设备表):管理大堂显示屏。
    • id,device_code,location,status
    • 设计思考:为多块显示屏的管理预留接口,可以指定不同的显示屏显示不同的队列信息。

表之间的关系通过外键(如ticket.counter_id->counter.id)或逻辑关联来维护。在MyBatis中,可以通过resultMap来实现复杂的对象关联映射。

3. 核心功能实现与关键技术点解析

有了清晰的架构和设计,我们就可以深入每个模块,看看代码是如何落地的。这里我会挑几个最具挑战性和代表性的功能点来详细讲解。

3.1 并发取号与唯一票号生成:如何避免“重号”?

这是系统面临的第一个挑战。想象一下,在业务高峰期,多台取号机或多个在线入口同时有客户点击“取号”。我们必须保证生成的票号全局唯一,并且不能因为并发而出错。

解决方案:数据库序列+Redis分布式锁

单纯依靠数据库的自增ID(AUTO_INCREMENT)生成票号(如A001)是不行的,因为我们需要包含业务前缀和自定义格式。一个常见的做法是使用一张单独的表来维护每个业务类型的最新序号。但并发更新这张表时,需要处理锁竞争。

我采用的方案是结合数据库和Redis:

  1. 格式定义:票号 = 业务前缀 + 日期(YYMMDD)+ 4位自增序号。例如:A2311050001
  2. 序号生成:为每个“业务前缀+日期”的组合,在Redis中维护一个自增键。例如,键为ticket:seq:A:231105,值为最新的序号。
  3. 并发控制
    // 伪代码示例 public String generateTicketNumber(String bizPrefix) { String dateStr = LocalDate.now().format(DateTimeFormatter.ofPattern("yyMMdd")); String redisKey = "ticket:seq:" + bizPrefix + ":" + dateStr; // 使用Redis的INCR命令,原子性自增,避免并发问题 Long seq = redisTemplate.opsForValue().increment(redisKey); // 设置键的过期时间,避免无用数据长期占用内存(例如24小时后过期) redisTemplate.expire(redisKey, 1, TimeUnit.DAYS); // 格式化为4位数字,不足补零 String seqStr = String.format("%04d", seq); return bizPrefix + dateStr + seqStr; }
  4. 容灾考虑:虽然Redis很可靠,但为了防止极端情况下Redis不可用,可以有一个降级方案。例如,在数据库中维护一张ticket_sequence表,通过SELECT ... FOR UPDATE行锁来获取序号,但性能不如Redis。可以在代码中做一个开关,正常情况下走Redis,异常时降级到数据库。

实操心得:使用Redis的INCR命令是核心,它是原子操作,完美解决了并发问题。务必记得给这个序列键设置一个合理的过期时间(比如业务结束后的几小时),否则Redis会被无数个每日的序列键占满。另外,生成的序号重置问题(每天从0001开始)是通过“键名包含日期”自然实现的。

3.2 实时叫号与信息推送:WebSocket如何集成?

叫号信息需要实时出现在大堂屏幕和柜员屏幕上,HTTP的请求-响应模式无法满足。WebSocket提供了全双工通信通道。

实现步骤:

  1. 引入依赖:在pom.xml中添加Spring Boot的WebSocket starter。
    <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
  2. 配置WebSocket:创建一个配置类,启用WebSocket并注册一个ServerEndpointExporterBean。更重要的是,配置一个自定义的TextWebSocketHandler来处理消息。
    @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { // 注册处理器,指定连接路径,允许所有源(生产环境应限制) registry.addHandler(new TicketWebSocketHandler(), "/ws/ticket") .setAllowedOrigins("*"); } }
  3. 实现消息处理器TicketWebSocketHandler需要继承TextWebSocketHandler,重写afterConnectionEstablished(连接建立)和handleTextMessage(处理消息)等方法。核心是维护一个全局的ConcurrentHashMap来保存所有在线的客户端会话(WebSocketSession)。
    public class TicketWebSocketHandler extends TextWebSocketHandler { private static final Map<String, WebSocketSession> clients = new ConcurrentHashMap<>(); @Override public void afterConnectionEstablished(WebSocketSession session) { // 客户端连接,将其加入Map。可以用设备ID或窗口ID作为key String clientId = (String) session.getAttributes().get("clientId"); clients.put(clientId, session); } @Override public void handleTextMessage(WebSocketSession session, TextMessage message) { // 处理客户端发来的消息,例如心跳包、特定请求 } // 提供一个静态方法,供业务Service调用,用于向特定或所有客户端推送消息 public static void sendMessageToClient(String clientId, String message) { WebSocketSession session = clients.get(clientId); if (session != null && session.isOpen()) { try { session.sendMessage(new TextMessage(message)); } catch (IOException e) { // 处理异常,可能需从Map中移除失效的session } } } public static void broadcast(String message) { for (WebSocketSession session : clients.values()) { // 向所有连接广播 } } }
  4. 业务层触发推送:在柜员执行叫号的Service方法中,生成叫号信息(JSON格式),然后调用TicketWebSocketHandler.sendMessageToClient(“display_hall”, jsonMessage)推送给大堂显示屏,同时推送给对应柜员的客户端。
    { "type": "call", "ticketNumber": "A2311050008", "counterNumber": "3", "businessType": "个人现金业务" }
  5. 前端连接与接收:在前端页面(显示屏页面),使用JavaScript建立WebSocket连接,并监听onmessage事件,收到消息后解析JSON,并更新DOM显示叫号信息。

注意事项:WebSocket连接不是永久可靠的,网络波动会导致断开。前端需要实现断线重连机制(例如,每隔5秒检查一次连接状态,断开则重连)。同时,为了保持连接活跃,避免被代理服务器或防火墙关闭,可以定期(如每30秒)从客户端向服务器发送一个心跳包(ping/pong)。

3.3 队列管理与叫号算法:不仅仅是“先进先出”

队列管理是业务逻辑的核心。最简单的规则是先进先出(FIFO),但实际业务中可能需要更复杂的策略。

基础FIFO队列实现:我们可以利用数据库来实现一个持久化队列。ticket表中,status='WAITING'且按create_time排序的记录,就是一个天然的FIFO队列。柜员叫号时,执行一条SQL:

SELECT * FROM ticket WHERE business_type = #{bizType} AND status = 'WAITING' ORDER BY create_time ASC LIMIT 1 FOR UPDATE;

FOR UPDATE子句会在事务中锁定这条记录,防止其他柜员同时叫到同一个号。

更复杂的调度策略:

  1. 优先级队列:VIP客户或特定业务(如残疾人服务)可以优先。可以在ticket表中增加一个priority字段,叫号时按priority DESC, create_time ASC排序。
  2. 过号处理:客户被叫后未及时前来(过号),通常的处理方式是允许客户在回来后,以“过号”身份插入当前等待队伍的最前面或特定位置。可以在ticket表中增加一个missed_count字段,记录过号次数,并在叫号逻辑中特殊处理status='MISSED'的票。
  3. 多队列负载均衡:如果一个柜员可以办理多种业务,叫号时不应只从一个队列取。可以设计一个“全局调度器”,根据各队列等待人数、柜员业务熟练度等因素,动态决定下一个叫哪个队列的号。这需要更复杂的算法,可能涉及权重计算。

我的实现建议:对于大多数中小型网点,按业务类型分列的FIFO队列 + 简单的过号重入机制已经完全够用。可以在叫号Service中,通过一个QueueService来封装这些逻辑,未来需要升级调度算法时,只需修改这个Service的实现即可,这是面向接口编程的好处。

4. 数据库操作优化与MyBatis实战

系统对数据库的读写操作非常频繁,尤其是查询等待队列和更新票务状态。优化数据库访问是提升系统性能的关键。

4.1 实体类与Mapper设计

首先,定义与数据库表对应的实体类(如Ticket,BusinessType)。然后,为每个实体创建对应的Mapper接口和XML映射文件。

TicketMapper.java示例:

@Mapper public interface TicketMapper { // 插入一条新的排号记录 int insert(Ticket ticket); // 根据ID查询 Ticket selectById(Long id); // 根据状态和业务类型查询等待列表(分页) List<Ticket> selectWaitingByBizType(@Param("bizType") String bizType, @Param("offset") int offset, @Param("limit") int limit); // 更新票状态(乐观锁版本控制) int updateStatus(@Param("id") Long id, @Param("oldStatus") String oldStatus, @Param("newStatus") String newStatus, @Param("version") Integer version); // 根据条件统计数量(用于分页和监控) Long countByCondition(TicketQueryCondition condition); }

对应的TicketMapper.xml片段:

<!-- 查询等待列表,并锁定最早的一条记录(用于叫号) --> <select id="selectEarliestWaitingForUpdate" resultType="Ticket"> SELECT * FROM ticket WHERE business_type = #{bizType} AND status = 'WAITING' ORDER BY create_time ASC LIMIT 1 FOR UPDATE </select> <!-- 动态SQL,用于复杂条件统计 --> <select id="countByCondition" resultType="java.lang.Long"> SELECT COUNT(*) FROM ticket <where> <if test="businessType != null and businessType != ''"> AND business_type = #{businessType} </if> <if test="status != null and status != ''"> AND status = #{status} </if> <if test="startTime != null"> AND create_time >= #{startTime} </if> <if test="endTime != null"> AND create_time <![CDATA[ <= ]]> #{endTime} </if> </where> </select>

4.2 事务管理与数据一致性

银行系统对数据一致性要求极高。一个完整的“叫号”操作涉及多个步骤:1) 查询并锁定待办票;2) 更新该票状态为“办理中”;3) 更新柜员当前业务;4) 记录日志。这些操作必须在一个数据库事务中完成,要么全部成功,要么全部回滚。

在Spring中,使用@Transactional注解可以轻松声明事务。

@Service public class CounterServiceImpl implements CounterService { @Autowired private TicketMapper ticketMapper; @Autowired private CounterMapper counterMapper; @Transactional(rollbackFor = Exception.class) // 发生任何异常都回滚 public CallResult callNextTicket(String clerkId, String bizType) { // 1. 获取当前柜员信息 Counter counter = counterMapper.selectByClerkId(clerkId); if (counter == null || !"IDLE".equals(counter.getCurrentStatus())) { throw new BusinessException("柜员状态异常,无法叫号"); } // 2. 查询并锁定最早的一张等待票(悲观锁) Ticket nextTicket = ticketMapper.selectEarliestWaitingForUpdate(bizType); if (nextTicket == null) { throw new BusinessException("当前队列无等待客户"); } // 3. 更新票状态 nextTicket.setStatus("PROCESSING"); nextTicket.setCallTime(new Date()); nextTicket.setCounterId(counter.getId()); ticketMapper.update(nextTicket); // 4. 更新柜员状态 counter.setCurrentStatus("BUSY"); counter.setCurrentTicketId(nextTicket.getId()); counterMapper.update(counter); // 5. 构建叫号结果,准备推送 CallResult result = new CallResult(nextTicket, counter); // 6. 异步推送WebSocket消息(注意:事务提交后再推送更稳妥) // 可以通过 @TransactionalEventListener 监听事务提交事件后再推送 websocketService.sendCallInfo(result); return result; } }

踩坑记录:这里有一个常见的陷阱。如果在@Transactional方法内直接进行WebSocket推送或发送HTTP请求(外部调用),而这些操作又非常耗时,会导致数据库连接被长时间占用,影响性能甚至引发死锁。最佳实践是将这类非数据库操作(如消息推送、日志记录到外部系统)放在事务提交之后执行。可以使用Spring的@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)来监听事务提交成功的事件,然后在这个监听器里执行推送操作。

4.3 性能优化:索引与缓存

数据库索引:这是提升查询效率最直接的手段。必须在频繁作为查询条件的字段上建立索引。

  • ticket表:(business_type, status, create_time)组合索引,这对“查询某类业务的等待队列”这个核心查询至关重要。statuscounter_id上的索引也对更新和关联查询有帮助。
  • counter表:clerk_id上建立唯一索引,方便快速通过柜员ID查找窗口。

应用层缓存(Redis)

  • 热点数据缓存:例如“今日总取号数”、“各队列当前等待人数”。这些数据查询频繁但更新不实时(每隔几分钟更新一次即可)。我们可以定时(如每5分钟)从数据库统计一次,存入Redis并设置过期时间。前端查询时直接读Redis。
    @Scheduled(fixedDelay = 300000) // 每5分钟执行一次 public void refreshQueueStats() { Map<String, Long> stats = ticketService.calcWaitingCountByBizType(); redisTemplate.opsForHash().putAll("stats:queue:waiting", stats); redisTemplate.expire("stats:queue:waiting", 10, TimeUnit.MINUTES); }
  • 配置信息缓存business_type(业务类型)表的数据很少变动,完全可以启动时加载到Redis或本地内存中,避免每次取号都查数据库。

5. 前端界面与用户体验关键点

系统主要有三类用户界面:客户取号界面、柜员操作界面、大堂显示屏和管理后台。这里重点讲前两者。

5.1 客户取号界面:简洁与引导

取号界面通常运行在触摸屏设备或嵌入到银行官网/小程序。核心要求是简单、明了、防误操作

  • 布局:大按钮、大字体。清晰列出所有可办理的业务类型,每个类型配以简明的图标和文字说明。
  • 交互:客户点击业务类型后,界面应给出明确反馈(如按钮变色),并立即打印出凭条(或显示取号成功页面,包含票号、前方等待人数、预估等待时间)。
  • 预估等待时间:这是一个能极大提升体验的功能。算法可以很简单:预估时间 = 当前该队列等待人数 * 该业务平均办理时长。平均办理时长可以从历史数据中动态计算,并维护在business_type表中。在取号时,将这个估算值显示给客户。
  • 技术实现:一个简单的Thymeleaf页面,通过Ajax提交取号请求,后端返回JSON包含票号和信息,前端再渲染结果页。

5.2 柜员操作界面:高效与稳定

柜员界面是生产力工具,核心是稳定、快速、减少不必要的操作

  • 登录与状态:柜员使用工号和密码登录。登录后,界面中央醒目显示“叫号”按钮。上方显示柜员当前状态(空闲/忙碌)和当前正在服务的客户票号。
  • 一键叫号:点击“叫号”,系统自动根据该柜员配置的可办理业务类型,从对应的队列中取出下一个号码。所有复杂的队列查询、状态更新逻辑都在后端完成,前端只负责触发和显示结果。
  • 业务办理与完成:叫号后,界面应能显示客户的基本信息(如果与客户系统关联)或票号信息。办理完成后,点击“完成”按钮,系统将票状态更新为“已办结”,柜员状态恢复为“空闲”,并自动准备下一次叫号。
  • 特殊操作:提供“暂停服务”(柜员临时离开)、“过号”(客户未到)、“重呼”(再叫一次当前号)等按钮。这些操作都应通过清晰的二次确认对话框,防止误触。
  • 实时同步:柜员界面需要通过WebSocket与服务器保持连接,以便接收管理端下发的指令(如强制签退、消息通知)。

5.3 大堂显示屏:实时与醒目

这是一个“只读”界面,核心是信息实时、字体巨大、布局清晰

  • 内容:通常分为几个区域:当前正在办理的号码(按窗口排列)、最新叫到的号码(滚动显示)、各队列等待人数。
  • 技术:一个全屏显示的网页,通过WebSocket与服务器保持长连接。收到叫号消息后,使用JavaScript动态更新DOM元素。为了达到“醒目”效果,需要用到CSS动画,比如新叫到的号码有一个放大、高亮、然后滚入列表的动画效果。
  • 容错:显示屏客户端必须考虑网络中断的情况。除了断线重连,在初始化时应该从服务器拉取一次完整的状态数据,避免刚连接时屏幕空白。

6. 项目部署、监控与常见问题排查

开发完成只是第一步,让系统稳定运行在生产环境同样重要。

6.1 部署架构建议

对于单个网点,可以采用单体应用部署:

  • 一台应用服务器(部署Spring Boot Jar包)。
  • 一台数据库服务器(MySQL)。
  • 一台Redis服务器。
  • 所有客户端(取号机、柜员电脑、显示屏电脑)通过网点内部网络访问应用服务器。

为了高可用,可以对数据库和Redis做主从复制。应用服务器也可以部署两台,前面用Nginx做负载均衡和反向代理。

Spring Boot应用打包与启动:

# 打包 mvn clean package -DskipTests # 会在target目录生成一个可执行的jar文件,如 bank-queue-system-1.0.0.jar # 启动(生产环境建议使用 nohup 或 systemd 服务) java -jar -Dspring.profiles.active=prod bank-queue-system-1.0.0.jar

application-prod.properties配置文件中,需要设置生产环境的数据库连接、Redis连接、日志路径等。

6.2 系统监控与日志

没有监控的系统就是在“裸奔”。

  • 应用健康监控:Spring Boot Actuator提供了丰富的端点(/actuator/health,/actuator/metrics),可以集成到监控平台(如Prometheus+Grafana)中,监控JVM内存、线程池、数据库连接池状态等。
  • 业务日志:使用SLF4J + Logback。针对关键业务节点,记录结构化日志。
    @Slf4j @Service public class TicketService { public Ticket createTicket(String bizType) { // ... 业务逻辑 log.info("Ticket created. ticketNo:{}, bizType:{}, waitCount:{}", ticket.getTicketNumber(), bizType, currentWaitCount); return ticket; } }
    日志文件要按日期滚动,并定期归档。排查问题时,通过票号、时间等关键词能快速定位相关日志。
  • 数据库慢查询日志:在MySQL中开启慢查询日志,定期分析,对执行时间过长的SQL进行优化。

6.3 常见问题与排查技巧实录

在实际运行中,你肯定会遇到各种各样的问题。下面是我总结的一些常见问题及其排查思路:

问题现象可能原因排查步骤与解决方案
取号缓慢,界面卡顿1. 数据库查询慢。
2. Redis连接超时或阻塞。
3. 应用服务器GC频繁。
1. 查看应用日志和数据库慢查询日志,优化对应SQL,检查索引。
2. 使用redis-cliinfo命令查看Redis状态,检查网络和内存使用。
3. 使用jstat或VisualVM监控JVM GC情况,调整堆内存参数。
叫号后,显示屏更新有延迟1. WebSocket连接断开,前端重连机制失效。
2. 网络问题导致消息丢失。
3. 后端推送消息时发生异常。
1. 打开浏览器开发者工具,查看WebSocket连接状态和网络请求。检查前端重连代码。
2. 在后端推送消息处增加日志,确认消息是否成功发出。检查服务器网络带宽和防火墙设置。
3. 确保推送消息的逻辑不在数据库事务内长时间执行。
出现重复票号1. 票号生成逻辑在极高并发下出现线程安全问题。
2. Redis的INCR命令在集群模式下可能出现极端情况(如主从切换)。
1.复查代码:确保票号生成使用了Redis的原子操作INCR,并且键的设计包含了日期。
2.加强校验:在ticket表的ticket_number字段上建立唯一索引,作为最后一道防线。插入重复数据时会抛出数据库异常,可以在业务层捕获并重试取号逻辑。
3.考虑更严格的方案:对于金融级要求,可以使用数据库序列或分布式发号器(如雪花算法)。
柜员点击“叫号”无反应1. 前端JavaScript错误。
2. 后端接口返回错误或超时。
3. 该柜员状态异常(如已被管理员强制下线)。
1. 打开浏览器控制台,查看是否有JS报错。查看点击按钮触发的网络请求,看状态码和响应内容。
2. 查看后端应用日志,定位接口执行过程中的异常。
3. 检查数据库中和该柜员关联的counter表状态字段。
后台统计报表数据不准1. 统计SQL逻辑错误。
2. 数据统计时,有未提交的事务导致数据不一致。
3. 缓存数据未及时更新。
1. 复核统计SQL,最好在测试环境用真实数据验证。
2. 对于需要高实时性的统计,可以考虑读取从库,或者使用事务隔离级别相关的提示。
3. 检查缓存更新策略,确保在数据变更时(如票状态更新)能及时清除或更新相关缓存。

我个人在实际开发这个系统时,最深的一点体会是:边界情况和异常处理的重要性远大于主流程开发。比如,网络闪断时WebSocket如何优雅重连?取号机在打印凭条时打印机卡纸了怎么办?柜员端界面在交易过程中浏览器崩溃了,如何恢复状态?这些“倒霉”的情况虽然发生概率低,但一旦发生就会严重影响用户体验和银行信誉。在设计和编码阶段,就必须为这些异常流留出处理空间,比如增加事务补偿机制、操作日志记录、状态恢复查询等功能。一个健壮的系统,正是在对这些细节的不断打磨中构建起来的。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/4 3:54:22

STM32家居环境监测系统开源实战:温湿度烟雾检测与OLED显示

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/4 3:52:59

基于YOLOv5与树莓派的驾驶员危险行为检测系统实战

简介&#xff1a;这是一套面向嵌入式AI初学者与高校实践者的驾驶员危险驾驶行为检测预警系统&#xff0c;基于YOLOv5深度学习模型开发&#xff0c;专为树莓派等边缘设备轻量化部署优化&#xff0c;适用于毕业设计、课程设计、学科竞赛及工程实训等场景。资源包共105个文件&…

作者头像 李华
网站建设 2026/9/4 3:52:49

IEEE 33节点配电网模型:从原理到MATLAB/Simulink仿真实战

简介&#xff1a;本资源为电力系统领域经典的IEEE 33节点配电网仿真模型&#xff0c;面向高校电气工程专业师生、微电网与分布式能源研究者及电力系统仿真初学者&#xff0c;用于开展潮流计算、稳定性分析、DERs并网控制策略验证及故障响应测试等核心任务。压缩包共2个文件&…

作者头像 李华
网站建设 2026/9/4 3:52:39

工业相机软触发与参数控制实战:从曝光增益到OPT SDK编程

简介&#xff1a;本资源是一套基于C#开发的OPT相机控制完整工程&#xff0c;面向工业视觉、科研图像采集等领域的开发者与自动化工程师&#xff0c;解决相机实时采集、软触发同步、曝光/增益参数动态调节及生命周期管理等核心控制问题。压缩包共69个文件&#xff0c;包含9个关键…

作者头像 李华
网站建设 2026/9/4 3:52:32

Python LDA主题模型与情感分析实战:电商评论数据挖掘全流程解析

简介&#xff1a;本资源是一套完整的电商评论情感分析高分课程设计项目&#xff0c;面向计算机、数据科学及相关专业本科生&#xff0c;解决电商场景下海量用户评论的主题挖掘与情感倾向判别问题。项目基于Python实现LDA主题建模&#xff0c;并融合文本预处理、词频统计、情感词…

作者头像 李华