Files
blackbird/db2/README.md
T
2026-08-06 17:42:50 +00:00

254 lines
7.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# db2 — C++20 数据库 RPC 服务(libmsg + SQLite)
db 模块的 C++20 重实现,接口与 Python 版完全兼容:相同的 `execute` 路由、相同的请求/响应格式、相同的 `where` 语法。gateway 的 `DbC::call_msg()` 以及所有使用 pymsg `execute` 路由的调用方**零改动**即可切换。
## 与 Python db 的关系
| 特性 | db(Python) | db2(C++) |
|------|-------------|-----------|
| 传输协议 | pymsg TCP 帧 | pymsg TCP 帧(libmsg) |
| `execute` 路由接口 | ✅ | ✅ 完全对齐 |
| where 运算符 | ✅ | ✅ 完全对齐 |
| 响应格式 | ✅ | ✅ 完全对齐 |
| `$now` 哨兵值 | ✅ | ✅ |
| confs 配置拉取 | ✅ | ✅ |
| Logs 自动建表 + 清理 | ✅ | ✅ |
| 数据库后端 | SQLite / PG / MySQL | SQLite(直接 C API) |
| 依赖 | Python 3 + SQLAlchemy | libmsg + boost::json + libsqlite3 |
## 快速开始
### 编译
```bash
./_set.sh # 等同于 make,产物为 build/db2
```
需要系统已安装:`libboost-json-dev`、`libsqlite3-dev`,以及 `libmsg` 已构建(`../libmsg/build/libus_msg.so`)。
### 启动
```bash
./_run.sh # 读 confs 自动获取配置
./_run.sh --addr 127.0.0.1 --port 8005 --workers 4 # 覆盖监听参数
```
服务启动时强依赖 confs(拉取 `db.ini`)。confs 未就绪时自动重试,不要求启动顺序。
## 配置管理
与 Python db 完全一致:启动时从 confs 服务拉取 `db.ini`,失败则退出(exit code 1)。
### confs 配置文件(db.ini)
配置文件托管于 [confs/configs/db.ini](../confs/configs/db.ini):
```ini
[server]
host = 127.0.0.1
port = 8005
workers = 4
[database]
url = sqlite:///database.db
[logs]
retention_days = 30
max_records = 1000000
cleanup_interval = 3600
```
### 引导配置(confs 连接地址)
| 来源 | 优先级 |
|------|--------|
| 环境变量 `CONFS_HOST` / `CONFS_PORT` | 最高 |
| 默认值 `127.0.0.1:8010` | 兜底 |
### 环境变量(最高优先级,覆盖 confs 配置值)
| 环境变量 | 说明 | 对应配置项 |
|---------|------|-----------|
| `LISTEN_ADDR` | 监听地址 | `[server] host` |
| `LISTEN_PORT` | 监听端口 | `[server] port` |
| `WORKERS` | 工作线程数 | `[server] workers` |
| `DATABASE_URL` | 数据库文件路径 | `[database] url` |
| `CONFS_HOST` | confs 地址(引导) | — |
| `CONFS_PORT` | confs 端口(引导) | — |
### 命令行参数(最高优先级)
```
--addr / -a 监听地址
--port / -p 监听端口
--workers / -w 工作线程数
```
## 协议与 API
与 Python db 完全一致,可直接参考 [db/README.md](../db/README.md)。以下为要点摘要。
### 传输协议(pymsg)
帧格式:`[4 字节大端长度][UTF-8 JSON]`,TCP 长连接。
### execute 路由 — 通用 CRUD
```json
{
"_route": "execute",
"action": "select",
"table": "Users",
"columns": ["id", "username"],
"values": {"username": "bob", "password_hash": "..."},
"where": {
"is_active": 1,
"create_at": {"ge": "2026-01-01T00:00:00"}
},
"order_by": [{"column": "id", "desc": true}],
"limit": 10,
"offset": 0
}
```
### where 条件运算符
| 运算符 | SQL | 示例 |
|--------|-----|------|
| 直接值 | `col = ?` | `"name": "Alice"` |
| `eq` | `=` | `"age": {"eq": 30}` |
| `ne` | `!=` | `"status": {"ne": "deleted"}` |
| `gt` | `>` | `"score": {"gt": 80}` |
| `ge` | `>=` | `"score": {"ge": 60}` |
| `lt` | `<` | `"price": {"lt": 100}` |
| `le` | `<=` | `"price": {"le": 200}` |
| `like` | `LIKE` | `"name": {"like": "%Smith%"}` |
| `in` | `IN (...)` | `"id": {"in": [1,2,3]}` |
多条件自动 **AND** 连接。
### 响应格式
```json
// 成功
{"success": true, "data": [{"id": 1, "username": "Alice"}]}
// insert
{"success": true, "data": [{"lastrowid": 42}]}
// update / delete
{"success": true, "data": [{"rowcount": 3}]}
// 失败
{"success": false, "error": "table not found: Foo"}
```
### 日期时间列
列类型含 `DATETIME`/`DATE`/`TIME`,或列名含 `_at`/`time`/`date` 的列自动视为日期列:
| 写入值 | 行为 |
|--------|------|
| `"2026-07-28T22:30:00"` | 原样存储(UTC) |
| `"$now"` | 替换为服务端当前 UTC 时间 |
| 其他字符串 | 原样存储 |
## 使用示例(C++ gateway)
gateway 的 `DbC::call_msg()` 无需任何修改,直接指向 db2 端口即可:
```cpp
// DbC::init("127.0.0.1", 8005); // 不变
// 查询用户
auto result = DbC::call_msg({
{"action", "select"},
{"table", "Users"},
{"where", {{"username", "alice"}}},
{"limit", 1},
});
// 更新登录时间
DbC::call_msg({
{"action", "update"},
{"table", "Users"},
{"values", {{"last_login", "$now"}}},
{"where", {{"id", user_id}}},
});
```
## 使用示例(Python pymsg)
```python
from pymsg.rpc import RpcClient
client = RpcClient("127.0.0.1", 8005, "myservice")
# 查询
rows = client.call("execute", {
"action": "select",
"table": "Users",
"where": {"is_active": 1},
"order_by": [{"column": "id", "desc": True}],
"limit": 20,
})
# 插入
result = client.call("execute", {
"action": "insert",
"table": "Notes",
"values": {"pid": 0, "owner": "alice", "title": "hello", "content": "world"},
})
print(result[0]["lastrowid"])
```
## 内置日志能力
db2 内置 Logs 表(启动时自动建表 + 索引),设计与 Python db 完全一致。
### Logs 表结构
```sql
CREATE TABLE IF NOT EXISTS Logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts TEXT NOT NULL, -- UTC ISO 8601
service TEXT NOT NULL,
level TEXT NOT NULL, -- DEBUG/INFO/WARNING/ERROR/CRITICAL
message TEXT NOT NULL, -- 写入时截断至 8192 字节
trace_id TEXT,
extra TEXT -- 可选 JSON 字符串,截断至 4096 字节
);
-- 索引: (service,ts) / (level,ts) / (ts) / (trace_id)
```
外部服务通过 `execute` 路由 INSERT 到 Logs 表即可。可复用 [db/example_log_client.py](../db/example_log_client.py)。
### 自动清理
后台线程每 `cleanup_interval` 秒(默认 3600)执行:
1. 删除超过 `retention_days` 天(默认 30)的记录
2. 若总条数超过 `max_records`(默认 100 万),删除最旧的超出部分
## 安全机制
- 表名仅允许字母、数字、下划线,防止 SQL 注入
- 所有 values / where 值通过 `sqlite3_bind_text` 参数绑定,无字符串拼接
- 仅操作已有表(`PRAGMA table_info` 反射),无法建表或删表
- RpcServer 异常自动脱敏(过滤内部路径),错误细节仅写 stderr
## 实现说明
- **per-thread 连接**:每个 worker 线程持有独立 `sqlite3*` 连接(`thread_local`),避免跨线程共享 prepared statement
- **列 schema 缓存**:`PRAGMA table_info` 结果加锁缓存,仅首次请求时反射
- **SQLite PRAGMA**:`journal_mode=WAL`、`synchronous=NORMAL`、`foreign_keys=ON`、`busy_timeout=5000`、`mmap_size=268435456`,对齐 Python 版配置
## 文件说明
| 文件 | 说明 |
|------|------|
| `src/db2.cpp` | 全部实现(配置/连接池/路由/日志/清理) |
| `Makefile` | 编译规则,链接 `libus_msg` + `boost_json` + `sqlite3` |
| `_set.sh` | 一键编译 |
| `_run.sh` | 启动服务 |
| `docker-entrypoint.sh` | 容器入口:初始化 schema + 启动 |