有没有试过查3个月前的交易记录,结果页面卡了1分钟还没出来? 小编第一次用CoinPro API拉历史数据时,差点以为电脑死机了!今天咱们就掰开揉碎聊透——怎么用Java高效搞定海量历史数据分页,顺便把查询速度提升10倍以上!
新手最容易踩的坑:无脑用OFFSET
java下载复制运行// 典型错误示范(官方文档常见写法) String sql = "SELECT * FROM trade_history ORDER BY time DESC LIMIT 20 OFFSET 10000";
这行代码看着没问题对吧?但当你查第500页(OFFSET=10000)时,数据库得先扫描前1万条记录再返回20条!相当于让你从《辞海》第一页开始翻,翻到第500页才读内容——不慢才怪!
性能对比吓死人
数据量 | OFFSET位置 | 查询耗时 |
---|---|---|
10万条 | 第10页 | 0.2秒 |
10万条 | 第500页 | 4.8秒 |
100万条 | 第5000页 | 超时崩了 |
原理:记住上一页最后一条记录的时间戳,下页直接从这个点开始查
java下载复制运行// 优化方案 —— 基于时间戳的游标查询 public List queryByTime(Date lastTradeTime, int pageSize) { String sql = "SELECT * FROM trade_history " + "WHERE time < ? ORDER BY time DESC LIMIT ?"; return jdbcTemplate.query(sql, new TradeMapper(), lastTradeTime, pageSize); }
优势:
CoinPro API有个痛点:历史数据接口限流严!每秒只能调2次
解决方案:
图片代码生成失败,换个方式问问吧用户请求 → Redis查缓存 → 有数据?立即返回 : 查数据库 → 存Redis+本地缓存
java下载复制运行// 结合Spring Cache的实战代码 @Cacheable(cacheNames = "tradeHistory", key = "#userId+'-'+#startTime") public Page getHistory(Long userId, Date startTime, int pageSize) { // 真实查询数据库... }
缓存层级建议:
用户体验暴增技巧:
java下载复制运行// 前端翻到第3页时,后端偷偷加载第4页 @Async public void preloadNextPage(Long userId, int nextPage) { // 提前查询并缓存下一页 }
效果:用户点“下一页”时数据已在内存,响应速度<100ms
CoinPro历史表必须建索引:
sql复制CREATE INDEX idx_user_time ON trade_history(user_id, time DESC); -- 组合索引
注意:单建time
索引对user_id
过滤无效
原因:它自动生成count(*)
语句,百万级数据count
一次要5秒!
替代方案:
java下载复制运行// 手动控制总数查询频率 if (pageNum < 10) { // 前10页才查总数 total = countHistory(userId); } else { total = -1; // 返回"-1"提示前端"总数未知" }
血泪教训:CoinPro的Websocket推送频率高达每秒百条,用Java做内存分页直接OOM!
正确做法:
java下载复制运行// 改用Kafka做数据缓冲 kafkaTemplate.send("realtime-trades", trade);
java下载复制运行/** 终极优化版分页服务 */ @Service public class TradeService { @Autowired private JdbcTemplate jdbc; @Autowired private CacheManager cacheManager; public Page queryHistory(String apiKey, Date cursorTime, int pageSize) { // 1. 优先读缓存 List cached = getFromCache(apiKey, cursorTime); if (!cached.isEmpty()) return new Page(cached); // 2. 游标查询数据库 String sql = "SELECT id, time, amount FROM trades " + "WHERE api_key=? AND time < ? " + "ORDER BY time DESC LIMIT ?"; List trades = jdbc.query(sql, this::mapTrade, apiKey, cursorTime, pageSize); // 3. 异步缓存结果 CompletableFuture.runAsync(() -> cacheResult(apiKey, cursorTime, trades)); return new Page(trades); } // 手动实现缓存(比Spring Cache更可控) private void cacheResult(String apiKey, Date cursorTime, List data) { String key = "trades::" + apiKey + "::" + cursorTime.getTime(); redisTemplate.opsForValue().set(key, data, 30, TimeUnit.MINUTES); } }
优化前后对比:
场景 | 优化前耗时 | 优化后耗时 |
---|---|---|
查询第1页 | 1200ms | 80ms |
查询第50页 | 超时 | 200ms |
高频访问重复页 | 800ms | 5ms |
最后说句大实话:CoinPro的历史API文档写得确实糙... 但咱用Java的智慧绕过去就完事了!记住:分页不用OFFSET,缓存不设是傻子,索引不漏保平安。按照这个口诀搞,百万级数据翻页照样丝滑!