当前位置: 首页 > news >正文

CacheSQL(三):双 HTTP 引擎与 SQL 查询——接口抽象的价值

CacheSQL(三):双 HTTP 引擎与 SQL 查询——接口抽象的价值


CacheSQL 有两种用法。一种是 Java API 内嵌调用,另一台机器上的 Python、Node、shell 脚本 HTTP 调用。

两种场景对应两套引擎:JDK 内置和 Undertow NIO。如果一开始没做好抽象,这两套引擎会写成两套重复代码。好在一开始就定义了一个接口。


一、HttpServerEngine:一个接口,两个实现

publicinterfaceHttpServerEngine{voidstart(intport,intthreads);voidstop();voidregisterRoute(Stringpath,RouteHandlerhandler);interfaceRouteHandler{voidhandle(Requestreq,Responseresp)throwsException;}}

就三个方法:启动、停止、注册路由。具体的 HTTP 实现(JDK / Undertow)各写各的,业务代码只依赖接口。

一键切换:

# 开发/测试:零外部依赖 server.http.engine=jdk # 生产:换 Undertow,吞吐量提升 50-60% server.http.engine=undertow

切换了之后一行代码不用改。启动时HttpCacheServer读配置、反射加载引擎、绑定端口。

为什么保留两套引擎

JDK 引擎:com.sun.net.httpserver.HttpServer,Java 标准库自带,零依赖。适合开发环境、内嵌部署。缺点是 BIO——每连接一个线程,高并发天花板低。

Undertow 引擎:NIO 多路复用,Worker 线程和 IO 线程分离。8 线程读取 QPS 比 JDK 引擎高了 60%,写入高了 50%。代价是多一个 1.2MB 的依赖包。

两套引擎不是"一套高级一套低级",是不同场景下的最优选择。JAR 包 80KB 的内嵌调用,挂 Undertow 太浪费。HTTP 服务对外暴露,必须上 NIO。


二、为什么 JSQLParser:不是造轮子,是站在轮子上

SQL 查询有三种做法:

  1. 自己写词法分析器→ 2000 行起跳,bug 无数
  2. 用正则抠字段→ 能跑但SELECT * FROM t WHERE name='O''Brien'会崩
  3. 用现成的解析库→ JSQLParser

JSQLParser 把 SQL 文本解析成结构化语法树——字段列表、表名、WHERE 条件的逻辑关系——全都是结构化的对象,不用自己去抠字符串。4.9 版本稳定多年,450KB,一个依赖搞定。

这不是偷懒。这是工程上正确的事——用成熟的轮子,把精力留给有价值的部分。


三、执行计划缓存:省掉 99% 的重复解析

同一模板的 SQL 会反复出现。WHERE AAC001 = 12345WHERE AAC001 = 67890的结构一模一样,只是值不同。

如果在缓存时保存完整的 WHERE 条件(包含值12345),每次查询都匹配不上,缓存完全失效。解决方案是模板化——用正则把 SQL 中的字符串和数字替换成?

原始:SELECT * FROM KCA2 WHERE AAC001 = 12345 AND AAC003 = '张三' 模板:SELECT * FROM KCA2 WHERE AAC001 = ? AND AAC003 = ?

以"模板"作为缓存 key,同一个 key 永远命中同一个执行计划。第二次查询直接走缓存,省掉解析过程。只有第一次 SQL 模板才是真正的 JSQLParser 解析开销。

取出缓存后,把具体参数(12345张三)注入到执行计划的 PlanCondition 里,执行查询。具体参数从原始 SQL 中用正则提取:

privatestaticfinalPatternVALUE_PATTERN=Pattern.compile("('[^']*'|\\b\\d+(\\.\\d+)?\\b)");

缓存的一个细节问题

解析后的 PlanCondition 存入缓存,多线程并发查询都需要读同一个条件和修改它的值——如果不隔离,线程 A 可能在线程 B 把值改成67890之后才开始用12345查询,但读到的已经是67890

不是 synchronized 的问题——synchronized 保证不崩,不保证不串。解法是深拷贝

// 每次查询前深拷贝 PlanConditionPlanCondition[]copyConditions(){PlanCondition[]copy=newPlanCondition[conditions.length];for(inti=0;i<conditions.length;i++){copy[i]=newPlanCondition(conditions[i].op,conditions[i].column,conditions[i].value);copy[i].value2=conditions[i].value2;}returncopy;}

语义上是一个新对象——值互不干扰。多线程同时执行,每个线程各自填充自身拷贝的值,不串读。

另一个细节:缓存无上限

planCache是一个ConcurrentHashMap,没有配置容量上限。理论上,如果 SQL 模板不断增多,缓存会无限膨胀。

目前用了简单的兜底:超过 1024 个模板后直接planCache.clear()——粗暴但有效。CacheSQL 的使用场景是有缓的(几十条业务 SQL),模板数不会无限增长。但对比 OpLog 的环形缓冲区,这里的处理明显粗糙——承认了"当前场景够了,后面再说"。


四、索引优先级:等值优先

多条件查询中最常见的是WHERE AAC001 = 12345 AND AAC003 = '张三'——两个条件都要满足,两个字段都有索引。

但 B+ 树一次只能选择一个索引执行——不能"先查 AAC001 的 B+ 树,又查 AAC003 的 B+ 树,然后取交集"。因为没有下标层面的集合操作 API——两个 B+ 树分别返回对象列表,在 Java 里"取交集"只能遍历,O(N)。

所以只能选一个索引。优选等值:

privatestaticPlanConditionfindBestIndex(Tabletable,PlanCondition[]conditions){PlanConditionbest=null;intbestPriority=-1;for(PlanConditionc:conditions){if(table.getIndex().containsKey(c.column)){intpri=c.op==Op.EQ?10:5;// 等值 = 10,范围 = 5if(pri>bestPriority){bestPriority=pri;best=c;}}}returnbest;}

选择最优索引后,其余条件在内存中逐行过滤——只对索引查出的候选行做。如果索引过滤效果高(大部分行被过滤掉),内存过滤就不是瓶颈。

范围查询上还有个细节优化:WHERE age >= 20 AND age <= 30——两个条件在同一列,都是范围查询。如果分别调两次 B+ 树再取交集,耗时不翻倍吗?所以检测到>= + <=组合时直接合并为一次getMoreAndLessThen(lower, upper)调用:

privatestaticPlanConditionfindRangePartner(PlanCondition[]conditions,PlanConditionindexed){for(PlanConditionc:conditions){if(c.column.equals(indexed.column)){if((indexed.op==GE||indexed.op==GT)&&(c.op==LE||c.op==LT))returnc;if((indexed.op==LE||indexed.op==LT)&&(c.op==GE||c.op==GT))returnc;}}returnnull;// 没有范围搭档,单次调用一种边界方法}

五、LIKE 前缀查询:转化为范围查询

LIKE '张%'怎么走 B+ 树?B+ 树不支持模糊匹配。但它支持两个能力:

  1. 沿着链表从某个位置开始扫(searchmore找到第一个 ≥ 该值的位置)
  2. 沿着链表扫到某个位置停下(再自己判断超出上界即退出)

所以LIKE '张%'转换为"范围查询"——下界是,上界是的字典序后一位(即起始的 Unicode 码点):

privatestaticStringprefixUpperBound(Stringprefix){charlast=prefix.charAt(prefix.length()-1);returnprefix.substring(0,prefix.length()-1)+(char)(last+1);}

“张%” → 范围 [张, 矛)。B+ 树的searchmore找到从哪开始,沿链表扫到第一个不在该范围内的 key 就停。不是LIKE真正变成了BETWEEN——是 B+ 树的物理结构天然支持这种"扫链表到截止点"的模式,而LIKE 'prefix%'恰恰能用这个模式高效执行。

为什么只支持prefix%而不支持%middle%?因为%middle%没有下界——"第一个 ≥ ‘’"就是所有行。没有任何过滤效果,退化成全表扫描。不是做不了,是 B+ 树做这件事没有意义。


六、总结

三个技术决策的共同点:接口抽象把复杂度隔离在单一模块里。

  • HttpServerEngine → 两套 HTTP 引擎切换不碰业务代码
  • PlanCondition 深拷贝 → 并发安全不侵入查询逻辑
  • rangePartner 合并 → 范围查询优化不干扰其他条件处理

不是哪个算法最复杂,是哪个设计让系统在后续扩展中不积累耦合。


下一篇:[CacheSQL(四):CacheSQLClient——用一张路由表实现水平扩展]


系列:CacheSQL 工程化交付实录(共 5 篇,含桥接篇)

http://www.jsqmd.com/news/735572/

相关文章:

  • 基于MCP协议的AI代理控制服务器:安全赋能AI操作本地系统
  • 告别双系统!保姆级教程:在Ubuntu 22.04上用Wine+PlayOnLinux搞定微信和Keil5
  • DeepSeek总结的最好的 PostgreSQL 数据库是有意无聊的
  • 第三部分-纹理与贴图——15. 纹理类型
  • GORL框架:在线强化学习的策略生成与优化分离新范式
  • python sphinx-autodoc
  • Windows 11任务栏拖放功能失效?这个高效修复工具让你重拾流畅体验
  • 类似 X-13ARIMA-SEATS 功能的 JDemetra+ 安装和使用
  • Java+AI<AI的使用与Java的基础学习5>
  • Graph扩散Transformer在分子生成与优化中的应用
  • python sphinx-rtd-theme
  • 纯HTML+CSS像素级克隆Cursor官网:前端基础还原实战
  • 使用taotokencli工具一键配置团队开发环境中的大模型密钥
  • 终极数据恢复指南:如何使用TestDisk和PhotoRec从灾难中拯救你的宝贵数据
  • Silk v3音频解码实战:30分钟搞定微信QQ语音转MP3
  • 可恢复功能设计理念:可恢复功能设计理念
  • 2026年国内婚庆公司梯队盘点:礼仪公司、舞台搭建公司、舞狮表演、LED租赁、会展公司、会议策划公司、启动球租赁选择指南 - 优质品牌商家
  • 苹果手机照片去背景怎么操作?2026年最全指南+免费工具推荐
  • 解释一下NGINX的反向代理和正向代理的区别?
  • AI表格可视化:ShowTable如何实现数据与美观的平衡
  • python myst-parser
  • OpenClaw技能安全扫描实战:静态模式匹配防御AI智能体指令风险
  • 逻辑推理引擎Chrysippus:从哲学到代码的自动推理实践
  • 几何约束增强视觉语言模型的空间推理能力
  • 别再装第三方了!深度体验统信UOS 1050/1060自带的远程连接工具,到底香不香?
  • python nbconvert
  • 基于Chrome DevTools Protocol的Go浏览器自动化:Gbrow实战与优化
  • GORL框架:强化学习中策略生成与优化的解耦实践
  • 2026年Q2免疫细胞回输权威机构盘点:免疫细胞治疗结节、免疫细胞治疗肿瘤、干细胞价格、干细胞储存多少钱、干细胞回输选择指南 - 优质品牌商家
  • LintConfig:专为代码重构设计的静态分析规则库