news 2026/10/27 6:49:35

基于Spring Boot与Redis的股票门户网站系统设计与实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于Spring Boot与Redis的股票门户网站系统设计与实现

1. 项目整体定位与方案选型思路拆解

1.1 股票门户网站到底在解决什么问题

很多人看到“股票门户网站系统”这几个字,第一反应是“不就是把行情数据展示出来吗”。真上手做过的人都知道,这类项目的难点从来不是“把数字显示在页面上”,而是在数据高频变化、用户请求集中、页面信息密度极高的三重压力下,把信息组织得让人一眼能看懂。它本质上是一个信息聚合与分发系统,核心链路只有四段:采集数据、清洗入库、按需查询、可视化呈现,但每一段都有大量细节会把新手卡住。

我把这类系统的用户角色拆成三层,后面的所有设计都围绕这三层展开。第一层是游客,进来就是看大盘指数、涨跌幅榜、个股详情,不需要登录,要求首屏快、不白屏;第二层是注册用户,核心诉求是“自选股”和“资讯订阅”,他每天打开就只想看自己关注的那十几只,其他一律不关心;第三层是管理员,负责资讯发布、股票基础信息维护、用户管理。三层角色的数据权限、缓存策略、接口粒度完全不同,如果一开始不分开设计,后面改起来就是灾难。

还有一个容易被忽略的定位问题:门户网站和交易系统是两回事。门户只做信息展示与检索,不涉及任何委托、撮合、资金流转。这个边界必须在开题阶段就写清楚,一方面它决定了你的工作量边界,另一方面它也决定了你的系统在答辩或评审时不会因为“功能不完整”被追问。我见过不少项目做到一半想加“模拟交易”,结果把自己拖进了一个无底洞,最后连基本的行情列表都没打磨好。

适合参考这套内容的人有三类:正在做类似课程设计或毕业设计的同学,想找一个结构完整、能跑通、讲得清的项目;刚入行的后端开发者,想了解一个真实的信息聚合类系统怎么分层、怎么缓存、怎么防并发;还有想自己搭个小工具盯盘的技术爱好者,直接拿骨架改成自己顺手的形态。代码层面的东西我会给到能直接抄的程度,但更重要的是每一步“为什么这么做”。

1.2 技术栈选型:为什么是这套组合而不是别的

标题里“基于”后面的技术名词被截断了,这在同类项目里非常常见,通常是“基于 Spring Boot 的”“基于 SSM 的”“基于 Vue + Spring Boot 的”。我按最常见的、也是我认为最合理的组合来讲:后端 Spring Boot + MyBatis-Plus + MySQL + Redis,前端 Vue 3 + Element Plus + ECharts,采集侧用定时任务 + HTTP 客户端。如果你要换成别的,思路完全通用。

先解释后端为什么选 Spring Boot 而不是原生 Servlet 或 SSM 手写 XML。行情类系统的接口特点是“数量多但逻辑薄”,一个股票详情页可能要并发调用基础信息、最新快照、分时数据、相关资讯四五个接口,每个接口都是查表、组装、返回。Spring Boot 的自动配置和注解式开发能把单个接口的开发成本压到很低,而 SSM 那套 XML 配置在一个有几十个接口的项目里,光是维护 mapper 映射就够喝一壶。这不是技术优劣问题,是投入产出比问题。

前端为什么坚持前后端分离而不是 JSP 或 Thymeleaf 服务端渲染?关键在图表。分时图、K 线图这类可视化组件,主流方案都是基于 Canvas 的 JavaScript 库,需要在前端拿到结构化数组后自行渲染。服务端渲染塞一堆数据进模板,再靠内联脚本初始化图表,页面会变得极其难维护,而且首屏之后的所有交互(切换周期、缩放、悬浮查看)都得写额外的 AJAX,等于两套逻辑并存。前后端分离之后,后端只负责吐 JSON,前端专心处理渲染和交互,职责非常干净。

数据库选 MySQL 而不是时序数据库,理由也很实在:这个量级根本用不上。一只股票一天的分钟级数据是 240 条,全市场按几千只算,一天几十万条,MySQL 加上合适的索引和分区策略完全扛得住。Redis 则承担两个职责:热点行情的短周期缓存和分布式锁。前者是为了扛住开盘时段的集中刷新,后者是为了防止定时采集任务在多实例部署时重复执行。这两个用途后面都会展开讲。

层次选型核心理由替代方案的坑
后端框架Spring Boot注解式开发,接口多时维护成本低原生 Servlet 配置繁琐,SSM 的 XML 易失控
持久层MyBatis-Plus单表 CRUD 零 SQL,复杂查询仍可手写纯 JPA 在复杂关联查询上不灵活
数据库MySQL 8数据量适中,生态成熟,运维简单时序库学习成本高,对本项目属于过度设计
缓存Redis热点数据缓存 + 分布式锁一把梭仅用本地缓存多实例下会不一致
前端Vue 3 + Element Plus组件生态全,后台管理页面开发快纯手写 DOM 在表格和表单上效率极低
图表ECharts分时图、K 线图开箱即用自研 Canvas 渲染周期长、坑多

1.3 分层架构与包结构设计

一个能长期维护的项目,包结构必须在第一天就定好,后面只往里填,不来回挪。我通常按这样的方式组织后端:

com.example.stockportal ├── common 全局返回体、异常、常量、工具类 │ ├── Result.java │ ├── GlobalExceptionHandler.java │ └── constant ├── config 跨域、拦截器、Redis、定时任务配置 ├── controller 对外接口层,只做参数校验和调用 ├── service 业务逻辑,接口 + impl ├── mapper MyBatis-Plus 的 Mapper ├── entity 与表一一对应的实体 ├── dto 入参对象 ├── vo 出参对象(脱敏、聚合后的视图) └── task 定时采集任务

这里有几个我踩过坑才坚持的约定。entity 绝不直接返回给前端,因为表结构里往往有内部字段(比如数据来源标记、更新时间戳、软删除位),直接透出去既冗余又有隐患,统一用 vo 转换。controller 里不写业务判断,只做参数合法性和登录态校验,剩下的全丢给 service,否则同一段逻辑在多个接口里复制粘贴,改一处漏三处。全局返回体统一,形如{code, msg, data},前端写一个统一的请求拦截器就能处理所有错误提示,不用每个接口单独判断。

登录态这块我倾向于用 JWT 放在请求头里,而不是 Session。原因不是 JWT 更“先进”,而是这个系统通常会部署成前后端分离甚至跨端口,Session 的 Cookie 同源策略会带来一堆跨域配置麻烦。JWT 的代价是无法主动失效,解决办法是在 Redis 里存一份 token 白名单或黑名单,登出时删掉对应 key,校验时多查一次 Redis,这点开销可以忽略。

2. 核心模块拆解与数据结构设计

2.1 行情数据模型:先把表设计对,后面少改十次

这类系统最容易返工的地方就是表结构。我的经验是把“基础信息”和“时序数据”彻底分开,前者变化极少,后者天天新增,混在一张表里会导致基础信息被反复冗余写入,索引也会越来越臃肿。

股票基础信息表只存不变的属性:

CREATE TABLE stock_info ( id BIGINT PRIMARY KEY AUTO_INCREMENT, stock_code VARCHAR(16) NOT NULL COMMENT '股票代码', stock_name VARCHAR(64) NOT NULL COMMENT '股票名称', market VARCHAR(16) NOT NULL COMMENT '所属市场标识', industry VARCHAR(64) DEFAULT NULL COMMENT '所属行业', list_date DATE DEFAULT NULL COMMENT '上市日期', status TINYINT NOT NULL DEFAULT 1 COMMENT '1正常 0停牌', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_code (stock_code), KEY idx_industry (industry) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='股票基础信息';

行情快照表存最新一笔,只保留每个代码一行,用唯一索引做覆盖更新:

CREATE TABLE stock_quote ( id BIGINT PRIMARY KEY AUTO_INCREMENT, stock_code VARCHAR(16) NOT NULL, current_price DECIMAL(12,3) DEFAULT NULL COMMENT '最新价', change_amount DECIMAL(12,3) DEFAULT NULL COMMENT '涨跌额', change_rate DECIMAL(8,3) DEFAULT NULL COMMENT '涨跌幅百分比', open_price DECIMAL(12,3) DEFAULT NULL, pre_close DECIMAL(12,3) DEFAULT NULL, high_price DECIMAL(12,3) DEFAULT NULL, low_price DECIMAL(12,3) DEFAULT NULL, volume BIGINT DEFAULT NULL COMMENT '成交量', turnover DECIMAL(20,2) DEFAULT NULL COMMENT '成交额', quote_time DATETIME DEFAULT NULL COMMENT '行情时间', UNIQUE KEY uk_code (stock_code), KEY idx_change_rate (change_rate) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='最新行情快照';

K 线表按周期存,用period字段区分日线、周线、月线,避免为每个周期建一张表:

CREATE TABLE stock_kline ( id BIGINT PRIMARY KEY AUTO_INCREMENT, stock_code VARCHAR(16) NOT NULL, period VARCHAR(8) NOT NULL COMMENT 'D/W/M', trade_date DATE NOT NULL, open_price DECIMAL(12,3) NOT NULL, close_price DECIMAL(12,3) NOT NULL, high_price DECIMAL(12,3) NOT NULL, low_price DECIMAL(12,3) NOT NULL, volume BIGINT DEFAULT NULL, UNIQUE KEY uk_code_period_date (stock_code, period, trade_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='K线数据';

金额字段统一用DECIMAL而不是DOUBLE,这一点必须强调。浮点数在反复累加和格式化时会出现0.1 + 0.2 = 0.30000000000000004这种问题,价格字段一旦出现这种尾巴,前端展示就会变成一长串小数,非常难看。DECIMAL(12,3)能覆盖绝大多数价格精度需求,成交额用DECIMAL(20,2)是因为它的量级大得多。

2.2 数据采集侧:定时任务怎么写得不容易崩

采集是整个系统的心脏,它挂了这个门户就是个空壳。我见过太多项目在这里图省事,直接在 Controller 里挂一个手动触发的接口,每次访问就顺手拉一次数据,结果页面上线第一天接口就被刷爆了。

正确的做法是定时任务独立跑,与请求链路完全解耦。用 Spring 的@Scheduled配合 cron 表达式,按交易时段和非交易时段设置不同的频率:

@Component public class QuoteCollectTask { @Resource private StringRedisTemplate redisTemplate; // 交易时段:每 60 秒采集一次快照 @Scheduled(cron = "0 */1 9-11,13-14 * * MON-FRI") public void collectDuringTrading() { doCollect("trading"); } // 收盘后:补一次收盘数据,并落 K 线 @Scheduled(cron = "0 5 15 * * MON-FRI") public void collectAfterClose() { doCollect("close"); } private void doCollect(String scene) { String lockKey = "lock:quote:collect:" + scene; // 30 秒过期,防止实例崩溃后锁无法释放 Boolean ok = redisTemplate.opsForValue() .setIfAbsent(lockKey, "1", Duration.ofSeconds(30)); if (Boolean.FALSE.equals(ok)) { return; // 已有实例在执行,直接跳过 } try { // 实际采集逻辑:拉取、校验、批量 upsert } finally { redisTemplate.delete(lockKey); } } }

这段代码有三个点值得说。第一,cron 里限定星期一到星期五,周末不跑,减少无谓请求,虽然节假日还会空跑,但影响可控,真要精确可以再维护一张交易日历表做判断。第二,用 Redis 的setIfAbsent做分布式锁,而不是 synchronized,因为线上很可能是多实例部署,JVM 级别的锁在跨进程时形同虚设,会导致同一份数据被重复写入、任务被重复触发。第三,锁必须设置过期时间并且放在 finally 里释放,只做释放不设过期,一旦实例在持锁期间被强杀,这把锁就永远死在那了。

采集到的数据还要做一层校验才能入库,常见的脏数据有三类:价格为空或为 0、涨跌幅算出来明显离谱、时间戳比库里已有数据还旧。这三种情况都要丢弃并记录日志,宁可少一条,不要把脏数据写进去污染整张表。批量写入用INSERT ... ON DUPLICATE KEY UPDATE,依靠唯一索引实现覆盖更新,比“先查再决定插还是改”少一次数据库往返,在批量场景下差距很明显。

2.3 自选股与用户模块:看似简单,坑在并发和一致性

自选股功能表面上就是一张关联表,增删查三条 SQL。但实际操作里有两个问题必须提前想清楚。

第一个是重复添加。用户手抖点两下,或者前端请求超时自动重试,很容易往表里塞两条一样的记录。解决办法是建表时直接加唯一索引,让数据库来兜底,不要指望前端按钮置灰能拦住所有情况:

CREATE TABLE user_watchlist ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, stock_code VARCHAR(16) NOT NULL, sort_no INT NOT NULL DEFAULT 0 COMMENT '排序号', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_code (user_id, stock_code), KEY idx_user (user_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户自选股';

第二个是自选股列表要展示实时价格,这就把“用户模块”和“行情模块”耦合起来了。如果每次打开自选股页面都去关联查询行情表,用户越多压力越大,而且行情表每 60 秒就整体刷新一次,缓存命中率会很难看。我的处理方式是:自选股接口只返回代码列表(带顺序),行情由前端再调一次批量行情接口,后端用一个IN查询加 Redis 缓存兜底。这样职责清晰,缓存也能复用——同一个行情缓存可以被首页榜单、自选股、个股详情三处共用。

密码存储必须用 BCrypt 这类带盐的慢哈希,绝对不能明文或者简单 MD5。这一点在很多学生项目里被忽略,但它属于那种“一旦被指出来就很尴尬”的问题。BCrypt 每次加密结果都不同,校验时用matches方法比对,实现起来只有一行代码,没有理由不用。

2.4 资讯与后台管理:内容运营的骨架

资讯模块的设计重点是状态流转,而不是简单的增删改查。一条资讯从录入到对外可见,中间至少要经过“草稿、待审、已发布、已下线”几个状态,直接插入就展示的做法,一旦录入有误就会直接暴露给所有访客。表里加一个status字段,前台查询永远带上status = 1的条件,后台则按状态筛选,逻辑就清晰了。

后台管理的另一个重点是分页与检索。股票基础信息可能有几千条,管理员要按代码或名称模糊搜索,用LIKE '%xxx%'在数据量上来之后会全表扫描。在几千条的规模下还能忍,但我建议至少给stock_code加上前缀索引意识,并且把列表接口的每页条数限制在 100 以内,防止有人手动改 URL 参数拉一万条出来把数据库拖死。

提示:后台接口一定要做角色校验,不能只判断“有没有登录”。普通注册用户和管理员走的是同一套登录体系时,如果只在拦截器里校验 token 有效性,普通用户拿到后台接口地址照样能调通,这是同类项目里出现频率最高的越权问题。

3. 实操过程与核心环节实现

3.1 环境准备与项目启动顺序

跑通这套系统的顺序很关键,顺序错了会出现“前端起来了但满屏报错”的情况,让人误以为是代码问题。我的标准流程是:数据库 → Redis → 后端 → 前端,一步一验证。

环境版本上,JDK 建议 8 或 17(看你用的 Spring Boot 版本,2.7 配 8,3.x 必须 17),MySQL 8.0,Redis 6 以上,Node 16 以上。版本不匹配是新手最常见的翻车点,尤其是 Spring Boot 3.x 强依赖 JDK 17,用 JDK 8 编译会直接报一堆看不懂的错。

# 1. 建库并导入初始脚本 mysql -u root -p -e "CREATE DATABASE stock_portal DEFAULT CHARSET utf8mb4;" mysql -u root -p stock_portal < sql/init.sql # 2. 启动 Redis(默认端口即可) redis-server # 3. 后端编译运行 mvn clean package -DskipTests java -jar target/stock-portal-1.0.0.jar # 4. 前端安装依赖并启动 npm install npm run dev

配置文件里的数据库连接、Redis 地址、采集接口地址这几项一定要抽到application-{env}.yml里,不要把密码硬编码在代码中。这不是洁癖,是因为你早晚要在本机、测试机、演示机之间来回切换,改代码里的常量迟早会漏改一处。

3.2 批量行情接口的实现与缓存设计

行情列表是这个系统里访问量最大的接口,必须重点打磨。它的逻辑其实很简单:按涨跌幅排序,取前 N 条,关联基础信息拿到名称。问题在于调用频率。

先算一笔账。假设日活 2000 人,其中三成集中在开盘后半小时访问,行情页平均停留 5 分钟、每 3 秒刷新一次,那么这半小时内的请求数大约是2000 × 0.3 × (1800 / 3) ≈ 36 万次,折算下来峰值接近200 QPS。如果每个请求都直接查 MySQL,一台普通配置的机器大概率会开始出现慢查询堆积。

解决办法是给行情列表加一层短 TTL 的 Redis 缓存。因为数据本身 60 秒才更新一次,缓存 3 到 5 秒完全不会影响准确性,却能把这 200 QPS 中的绝大部分挡在数据库之外:

public List<QuoteVO> listTopChange(int limit) { String cacheKey = "quote:top:change:" + limit; String cached = redisTemplate.opsForValue().get(cacheKey); if (StringUtils.hasText(cached)) { return JSON.parseArray(cached, QuoteVO.class); } List<QuoteVO> list = quoteMapper.selectTopByChangeRate(limit); // TTL 设为 3 秒,比采集周期短,保证数据不会太陈旧 redisTemplate.opsForValue() .set(cacheKey, JSON.toJSONString(list), Duration.ofSeconds(3)); return list; }

TTL 的选择是有讲究的。设得太长(比如 60 秒),用户会明显感觉到“榜单个股价格不动”;设得太短(比如 1 秒),缓存基本失效,等于白加。3 秒是个经验值:比采集周期短一个数量级,同时又足以把同一秒内的并发请求合并掉。这里还有个隐藏细节——缓存击穿。如果某个 key 恰好在高并发瞬间过期,所有请求会同时打到数据库。可以在缓存为空时加一个短锁,只放一个请求去查库,其余短暂等待后重试,实现成本很低,收益明显。

3.3 分时图与 K 线图的数据组织

图表是这个系统的门面,数据格式对不上,前端画出来就是一条直线或者一片空白。ECharts 的 K 线图接收的数据是一个二维数组,每项形如[开盘, 收盘, 最低, 最高],顺序不能错,错了图形会完全变形。

后端返回结构我一般设计成这样:

{ "code": 200, "data": { "dates": ["2024-01-02", "2024-01-03", "2024-01-04"], "values": [[10.20, 10.55, 10.10, 10.60], [10.55, 10.30, 10.25, 10.62], [10.30, 10.78, 10.28, 10.80]], "volumes": [120000, 98000, 156000] } }

前端拿到之后直接喂给配置项,不需要再做转换:

const option = { xAxis: { type: 'category', data: res.data.dates }, yAxis: { scale: true }, series: [ { type: 'candlestick', data: res.data.values, itemStyle: { color: '#e34b4b', // 上涨颜色 color0: '#2ba36a', // 下跌颜色 borderColor: '#e34b4b', borderColor0: '#2ba36a' } } ] }

这里有个必须提前处理的问题:数据量。日线一年 240 多条还好,但如果用户切到分钟级,一天就是 240 条,一年几万条,全量返回会让接口响应变慢、前端渲染卡顿。做法是后端按周期和区间做限制,比如分钟级最多返回最近 5 个交易日,日线最多返回 500 根,超出部分通过缩放交互触发增量加载。别指望前端一次吃下几万个点还能流畅缩放,实测下来超过两千个点,拖拽就开始掉帧。

分时图则是另一套数据:横轴是交易时间点,纵轴是价格,通常还要叠一条均价线。均价线的计算方式是用累计成交额除以累计成交量,这个计算放在后端做,前端只负责画,能省掉不少浏览器里的浮点运算开销。

3.4 数据库查询优化与索引验证

写完功能之后一定要做一件事:打开慢查询日志,把每个接口实际跑的 SQL 都看一遍。我见过太多项目的列表页在本地几十条数据下飞快,一导入全量数据就卡死,原因无一例外是全表扫描。

验证方法很直接,用EXPLAIN看执行计划:

EXPLAIN SELECT s.stock_code, s.stock_name, q.current_price, q.change_rate FROM stock_quote q JOIN stock_info s ON s.stock_code = q.stock_code WHERE s.status = 1 ORDER BY q.change_rate DESC LIMIT 20;

如果type列出现ALL,说明在做全表扫描,需要补索引。涨跌幅榜单这种按change_rate排序取前 N 的场景,给该字段建索引之后,配合LIMIT可以走索引顺序扫描,避免额外排序。关联字段stock_code在两张表上都必须有索引,尤其是被驱动表,否则每匹配一行都要扫一遍。

还有一个隐性的性能陷阱:N+1 查询。比如资讯列表要展示每条资讯关联的股票名称,如果写成“先查 20 条资讯,再循环查 20 次股票表”,就是典型的 N+1。正确做法是一次性把涉及的所有代码收集起来,用一条IN查询捞回来在内存里做映射。这个改动通常能把一个接口的耗时从几百毫秒压到几十毫秒,且代码量并不增加。

3.5 打包部署与反向代理配置

本地跑通只是第一步,能稳定部署才算完成。前端npm run build之后产物是一堆静态文件,后端是一个 jar 包,两者放在同一台机器上,用反向代理统一入口,可以避免跨域配置带来的各种麻烦。

server { listen 80; server_name localhost; # 前端静态资源 location / { root /opt/app/dist; index index.html; # 单页应用刷新 404 的经典解法 try_files $uri $uri/ /index.html; } # 后端接口转发 location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_read_timeout 30s; } # 静态资源缓存,减少重复下载 location ~* \.(js|css|png|jpg|woff2)$ { root /opt/app/dist; expires 7d; } }

配置里try_files那一行是必须的,否则用户在前端某个子路由页面按 F5,服务器会去找一个不存在的文件,直接返回 404。这个问题在开发阶段用 dev server 时不会暴露,一上线就立刻出现,属于“必踩”的坑。

后端启动脚本建议加上内存参数和日志输出,别用默认配置裸跑:

nohup java -Xms512m -Xmx1024m \ -Dspring.profiles.active=prod \ -jar stock-portal-1.0.0.jar \ > logs/app.log 2>&1 &

4. 常见问题与排查技巧实录

4.1 行情数据不准或不刷新

这是反馈最多的问题,现象是首页数字长时间不动,或者个股详情和列表页的价格对不上。排查顺序我固定为三步。

第一步看采集任务有没有真的在跑。在任务方法首尾打日志,如果只有开始日志没有结束日志,说明中间抛异常了,多半是外部接口超时或返回格式变了。加个超时时间和异常捕获,别让一次失败把整个任务链断掉。

第二步看缓存有没有及时失效。如果缓存 TTL 设得比采集周期还长,就会出现“库里已经更新了,页面上还是老数据”。我一般把缓存 TTL 设成采集周期的三分之一左右,并且把采集完成的时间戳单独存一个 key,前端展示时可以带上“数据更新时间”,让用户心里有数。

第三步看数据源本身的一致性。列表页和详情页如果走了不同的缓存 key 或者不同的查询路径,就有可能出现短暂不一致。解决办法是让它们共享同一个数据源和同一套缓存逻辑,本质上是“单一数据源”原则。

4.2 图表渲染卡顿与内存泄漏

分时图和 K 线图切来切去之后页面越来越卡,最后浏览器直接卡死,这个问题在图表类项目里极其常见。根因通常是每次切换都新建一个图表实例,却没有销毁旧的。ECharts 的实例会挂载大量事件监听和 Canvas 上下文,不销毁就会一直堆积。

正确写法是在组件卸载或重新初始化前,先判断实例是否存在并调用dispose:

import * as echarts from 'echarts' let chartInstance = null function renderChart(dom, option) { if (!chartInstance) { chartInstance = echarts.init(dom) } else { // 复用实例,只更新数据,比重新 init 快得多 chartInstance.clear() } chartInstance.setOption(option) } // 组件销毁时务必调用 function destroyChart() { if (chartInstance) { chartInstance.dispose() chartInstance = null } }

另一个性能点是定时刷新时的整图重绘。如果每 3 秒就重新setOption一次完整配置,图表会频繁闪烁。用setOption(option, { notMerge: false })做增量更新,只传变化的数据部分,视觉上会平滑很多。

4.3 定时任务重复执行与数据重复写入

现象是数据库里出现了两条完全一样的行情记录,或者同一分钟内采集日志打了两次。前面提过分布式锁是主要解法,但还有一个容易忽略的来源:开发和部署时的多实例。本地调试时开着 IDE 跑一份,同时又跑着打包后的 jar,两个进程都注册了定时任务,自然就重复了。

排查时先看进程数,ps -ef | grep java看看是不是起了多个。确认只有一个进程还重复,就去看锁的 key 是否一致、锁的过期时间是否短于任务执行时间。如果任务本身执行超过 30 秒而锁只有 30 秒过期,第二个实例就会在第一个还没跑完时拿到锁,照样重复。锁的过期时间必须显著大于任务最大执行耗时,这一点比锁本身还重要。

4.4 接口跨域与登录态失效

本地开发时前端跑在 5173 端口,后端在 8080,跨域问题必现。配置里加允许来源、允许方法、允许请求头,同时要注意如果用了自定义的认证头,需要显式声明allowedHeaders,否则浏览器预检请求会失败,报错信息还挺迷惑人。

登录态失效的典型表现是“登录成功了,但一刷新就变回未登录”。如果用的是内存变量存 token,页面刷新就丢了,必须落到localStorage或sessionStorage。如果用了 Redis 存 token 白名单,则要检查 Redis 的连接配置和 key 的过期时间,很多时候是 token 的 Redis TTL 设得比 JWT 自身的过期时间短,导致 JWT 还没过期,白名单记录先没了。

4.5 常见问题速查表

现象大概率原因快速验证方式处理办法
首页行情不刷新缓存 TTL 过长 / 采集任务异常直接查库看数据是否更新缩短 TTL,给采集加异常捕获与日志
列表与详情价格不一致两处走了不同缓存 key对比两接口返回的 quote_time统一数据源与缓存 key
图表切换后越来越卡图表实例未销毁打开性能面板看内存曲线卸载时调用 dispose
行情记录重复多实例部署 / 锁过期过早查进程数,看采集日志条数加分布式锁并延长过期时间
刷新页面 404单页应用路由未回退直接访问子路由地址反向代理加 try_files
登录后刷新丢失状态token 存内存 / Redis TTL 过短看浏览器存储与 Redis key落地存储,统一过期时间
列表页加载慢全表扫描 / N+1 查询EXPLAIN 看执行计划补索引,改批量 IN 查询
价格显示一长串小数用了浮点类型查表字段类型改 DECIMAL 并按位格式化

4.6 几条用血换来的实操心得

别在采集任务里做业务计算。我一开始图省事,在采集时顺便把涨跌幅、均价线都算好存库,看起来很高效,结果后来业务要改计算口径,历史数据全得重刷。正确做法是采集只负责“拿到原始数据并入库”,计算放到查询侧或者单独的聚合任务里,随时可以重跑。

给数据加时间戳,永远比你想的重要。每个快照、每条 K 线、每次采集都带上时间,出问题时第一眼看时间就能判断是数据源的问题还是你系统的问题,能省掉大量猜谜时间。

前端先做骨架,后端再填肉。我习惯先把页面布局、路由、接口 mock 全部搭完,看着像个完整系统了,再回头一个个实现真实接口。这样能避免“后端写了一堆没人用的接口”,也能在早期就发现数据结构设计不合理的地方。

导入全量数据再谈优化。本地拿十条数据测出来的性能毫无意义。上线前一定造一批接近真实的量(比如几千只股票 × 几百个交易日的 K 线),在这个基础上压测和看慢查询,才知道哪里真的需要加缓存、加索引。

写开题和文档时,把“边界”写清楚。明确说明系统只做信息展示、不涉及任何交易功能,明确说明数据来源是外部公开接口而非自建行情源。这既是技术诚实,也能避免后续被追问一堆超出范围的需求。文档里的架构图、时序图用文字和表格描述清楚数据流向,比堆一堆花哨的图形更有说服力。

最后分享一个我在实际项目里养成的小习惯:给每个核心接口都加一个极简的耗时日志,用拦截器统一打印接口名、耗时、是否命中缓存。上线初期这份日志能让你在用户反馈之前就发现问题接口,尤其是在行情刷新这种高频路径上,效果非常直接。

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

AI终端如何通过OpenClaw实现秒级故障自动修复

1. 项目概述&#xff1a;AI终端如何重塑故障修复流程去年双十一大促期间&#xff0c;我负责的电商系统突然出现订单支付成功率暴跌的紧急故障。当运维团队还在手忙脚乱查日志时&#xff0c;部署在边缘节点的OpenClaw已经自动识别出是第三方支付接口超时导致&#xff0c;并在30秒…

作者头像 李华
网站建设 2026/10/27 6:47:51

Java实现形式语言核心概念:集合运算、笛卡尔积、等价关系与等价类

形式语言与自动机这门课&#xff0c;很多人第一次接触时会被一堆符号吓退&#xff1a;等价关系、笛卡尔积、集合运算、等价类&#xff0c;教材上写得云山雾罩&#xff0c;考试要考&#xff0c;代码里又好像用不上。我当初也是这么想的&#xff0c;直到真正动手用 Java 把这几个…

作者头像 李华
网站建设 2026/10/27 6:47:32

Jetpack Compose入门实战:WanAndroid客户端开发指南

1. 项目概述&#xff1a;为什么选择WanAndroid作为Compose入门项目2026年的安卓开发生态已经发生了翻天覆地的变化&#xff0c;Jetpack Compose作为声明式UI框架早已成为Android开发的标配。对于刚接触安卓开发的新手而言&#xff0c;选择一个合适的练手项目至关重要。WanAndro…

作者头像 李华
网站建设 2026/10/27 6:47:31

HHO-RF混合算法:优化随机森林参数的新方法

1. 项目概述&#xff1a;当哈里斯鹰遇上随机森林在预测建模领域&#xff0c;算法融合正成为提升模型性能的新趋势。最近我在一个能源消耗预测项目中尝试了将哈里斯鹰优化算法(HHO)与随机森林回归(RF)相结合的方法&#xff0c;效果出乎意料——在测试集上相对传统RF模型平均降低…

作者头像 李华
网站建设 2026/10/27 6:47:13

C#与SQL Server实现高可靠邮件数据处理系统

1. 项目概述邮件用户分配数据的自动化处理是企业IT系统中常见的业务场景&#xff0c;我们团队最近用C#和SQL Server构建了一套稳定的数据处理系统。这套系统每天要处理数万封邮件的用户归属分配&#xff0c;核心挑战在于如何确保数据一致性&#xff0c;同时优雅地处理各种异常情…

作者头像 李华