CacheSQL(四):CacheSQLClient------用一张路由表实现水平扩展
前三篇讲了 CacheSQL 内部的架构:工程化升级、主从复制、双引擎与 SQL 查询。但还有一个问题没解决------再多节点,对调用方来说还是一条 HTTP URL 吗?
不是。调用方需要知道哪个表在哪个节点上、Master 是谁、Slave 有谁、读操作打哪个、写操作打哪个。这些不应该让调用方自己管理------应该封装在一个 Client 里。
这就是 CacheSQLClient。
一、一张路由表,隐掉所有拓扑
配置:
properties
# 组 insurance:负责 KCA2、KCA3 两张表
cachesql.group.insurance.master=http://192.168.1.10:8080
cachesql.group.insurance.slaves=http://192.168.1.11:8080,http://192.168.1.12:8080
cachesql.group.insurance.tables=KCA2,KCA3
# 组 finance:负责 KCA4 一张表
cachesql.group.finance.master=http://192.168.2.10:8080
cachesql.group.finance.slaves=http://192.168.2.11:8080
cachesql.group.finance.tables=KCA4
调用方只需要知道三件事:
java
CacheSQLClient client = new CacheSQLClient("cachesql.properties");
// 查------自动打到 Master 或任意 Slave
List<Map<String, Object>> rows = client.get("KCA2", "AAC001", "12345");
// 写------自动打到 Master
boolean ok = client.insert("KCA2", "AAC001", "99999", data);
不需要知道 KCA2 在哪台机器上,不需要区分读操作打谁、写操作打谁。Client 内部按 Table → Group → URL 的路由链自动完成寻址。
二、路由逻辑:读分流,写集中
java
private static class TableGroup {
final String master;
final String[] slaves;
// 读:Master 或任意 Slave,各 50% 概率
String pickReadUrl() {
if (slaves.length == 0) return master;
return random() ? master : slaves[(int)(Math.random() * slaves.length)];
}
// 写:始终 Master
String getWriteUrl() {
return master;
}
}
简单但有效。读操作随机分摊到组内所有节点(Master + Slaves),Master 也不闲置。写操作始终打到 Master------因为只有 Master 有写入权限,Slave 的写操作最终也是转发给 Master。
这个设计借鉴了 MySQL 读写分离的经典模式:写走 Master,读分摊。但因为 CacheSQL 是内存库,没有"主从延时"的问题------一切都是同步写入后广播,读任意节点都是一致的数据。
三、按表分组:不同表走不同节点
不是所有表都在一组机器上。card_info 表数据量大、QPS 高,可能需要独立一组节点。dict 表数据量小、低频访问,可以和其他表共用一组。
TableGroup 的设计支持了这种灵活性:
java
private final Map<String, TableGroup> tableToGroup = new LinkedHashMap<>();
private TableGroup findGroup(String tableName) {
TableGroup group = tableToGroup.get(tableName);
if (group == null)
throw new IllegalArgumentException("No group configured for table: " + tableName);
return group;
}
每个表通过配置文件绑定到一个组。调用方传表名,Client 自动路由到对应的组。这是传统分库分表中间件的简化版------不做数据拆分,只做路由隔离。
四、自带的 JSON 解析器:零外部依赖
CacheSQL 的服务器返回的是 JSON。正常做法是用 Jackson 或 Gson 反序列化。但 CacheSQLClient 只依赖 JDK 标准库------自己实现了一个 JSON 解析器。
这不是为了炫技。是为了"零依赖"------调用方不需要为了用 CacheSQL 引入任何额外的 JAR 包。一个 CacheSQLClient.java 扔进项目就能用。
实现方式:逐字符扫描。区分字符串内/外、跟踪花括号深度、按逗号切字段。对象嵌套用 matchBrace 递归匹配括号对确定边界------不是完整的 JSON 解析器,但覆盖了服务器返回的扁平对象数组格式。
java
private int matchBrace(String s, int start) {
int depth = 0;
boolean inStr = false;
for (int i = start; i < s.length(); i++) {
char c = s.charAt(i);
if (inStr) {
if (c == '\\') i++;
else if (c == '"') inStr = false;
} else {
if (c == '"') inStr = true;
else if (c == '{') depth++;
else if (c == '}') { depth--; if (depth == 0) return i; }
}
}
return -1;
}
这是"够用主义者"的做法------不写完整的 JSON 规范实现,只处理 CacheSQL 服务器实际返回的格式。工程上正确。
五、Client 与服务器之间的分工
CacheSQLClient 把三件事集中在一个类里:
- 路由发现:从配置文件读取 Group 信息
- 负载均衡:读操作随机分摊到组内所有节点
- 协议适配:HTTP GET/POST + JSON 解析
三件事分开都能做------路由用 Nginx,负载用 HAProxy,协议用 Jackson。但合在一起的好处是:调用方的 pom.xml 不需要加任何依赖。一行 new CacheSQLClient("cachesql.properties") 全部搞定。
代价是什么?配置方式的灵活性------分组规则必须写在 properties 文件里,不能动态从注册中心发现。但在政务系统的部署环境下,节点拓扑是静态的------配置文件的方式反而更可控。
六、水平扩展的完整方案
回顾 CacheSQL 的水平扩展全貌:
CacheSQLClient
(路由表 + 读负载均衡)
│
┌────────┼────────┐
▼ ▼ ▼
┌───────┐┌───────┐┌───────┐
│Master ││Slave 1││Slave 2│ ← Group: insurance
│.10 ││.11 ││.12 │
└───┬───┘└───────┘└───────┘
│ 广播
┌───┴───┐┌───────┐┌───────┐
│Master ││Slave 1││Slave 2│ ← Group: finance
│.20 ││.21 ││.22 │
└───────┘└───────┘└───────┘
- Client 层:表名 → 组名 → URL,调用方零感知
- Group 层:一主多从,总共可以定义任意多个组
- Server 层:Master 写入 + 广播,Slave 接收回放
水平扩展不是给你无限 QPS 的------它是让你在想加节点的时候,不用改代码。
系列:CacheSQL 工程化交付实录(共 5 篇,含桥接篇)