MySQL 即将下线
迁移提示
onestep-mysql 现在是 onestep-sql 的转发 shim。新项目请安装 onestep-sql[mysql] 并从 onestep_sql.mysql 导入;旧的 onestep-mysql 包与 from onestep_mysql import ... 仍可用。详见迁移到 onestep-sql。
MySQL Connector 提供三种模式:
- 表队列: 将数据库表作为任务队列
- 增量同步: 基于
(updated_at, id)的 Logstash 风格同步 - 表输出: 将结果写入数据库表
安装
pip install 'onestep-sql[mysql]'
# 或使用 core extra
# pip install 'onestep[mysql]'
onestep-sql是 MySQL 与 PostgreSQL 的规范发行包(issue #133)。旧的pip install onestep-mysql仍可用作转发 shim,但新部署建议使用onestep-sql[mysql]。所有 YAML 资源类型名不变。
表队列 (Table Queue)
将数据库表作为任务队列,通过更新状态字段来"领取"任务。
基本用法
from onestep import OneStepApp
from onestep_mysql import MySQLConnector
app = OneStepApp("orders")
# 创建连接
db = MySQLConnector("mysql+pymysql://root:root@localhost:3306/app")
# 创建表队列 Source
source = db.table_queue(
table="orders",
key="id",
where="status = 0", # 查询条件:待处理
claim={"status": 9}, # 领取时设置:处理中
ack={"status": 1}, # 成功后设置:已完成
nack={"status": 0}, # 失败后设置:待处理(可重试)
batch_size=100, # 每次领取数量
)
# 创建表输出 Sink
sink = db.table_sink(
table="processed_orders",
mode="upsert", # 插入或更新
keys=("id",), # 唯一键
)
@app.task(source=source, emit=sink, concurrency=16)
async def process_order(ctx, row):
return {
"id": row["id"],
"payload": row["payload"],
"status": "done"
}
if __name__ == "__main__":
app.run()工作流程
- 查询
status = 0的记录 - 批量更新
status = 9(领取) - 执行任务
- 成功:更新
status = 1 - 失败:更新
status = 0(可重试)
状态管理
# 状态流转
where="status = 'pending'" # 待处理
claim={"status": "processing"} # 处理中
ack={"status": "completed"} # 已完成
nack={"status": "failed"} # 失败增量同步 (Incremental Sync)
基于 (updated_at, id) 实现增量数据同步,适合数据仓库场景。
基本用法
from onestep import MemoryQueue, OneStepApp
from onestep_mysql import MySQLConnector
app = OneStepApp("sync-users")
db = MySQLConnector("mysql+pymysql://root:root@localhost:3306/app")
# 游标存储(持久化位置)
cursor_store = db.cursor_store(table="onestep_cursor")
# 增量同步 Source
source = db.incremental(
table="users",
key="id",
cursor=("updated_at", "id"), # 游标字段
where="deleted = 0", # 过滤条件
batch_size=1000, # 每批数量
state=cursor_store, # 状态存储
)
# 输出到内存队列
out = MemoryQueue("dw")
@app.task(source=source, emit=out, concurrency=1)
async def sync_user(ctx, row):
return {
"id": row["id"],
"name": row["name"],
"updated_at": row["updated_at"]
}工作原理
- 从
cursor_store读取上次位置 - 查询
updated_at > last_updated OR (updated_at = last_updated AND id > last_id) - 处理数据
- 更新
cursor_store中的位置
游标存储
# 数据库存储(推荐生产环境)
cursor_store = db.cursor_store(table="sync_cursor")
# 或状态存储
state_store = db.state_store(table="onestep_state")表输出 (Table Sink)
将处理结果写入数据库表。
Upsert 模式
sink = db.table_sink(
table="results",
mode="upsert",
keys=("id",), # 唯一键,存在则更新,不存在则插入
)
@app.task(source=..., emit=sink)
async def process(ctx, item):
return {"id": item["id"], "data": item["data"]}注意:
upsert生成INSERT ... ON DUPLICATE KEY UPDATE。即使键已存在、 实际走更新分支,MySQL 仍会对 INSERT 部分做约束检查——目标表存在无默认值的NOT NULL列且载荷未提供这些列时,会产生Field 'xxx' doesn't have a default valuewarning(更新本身仍会成功)。 只需要更新已有行时,请改用mode="update"。
Insert 模式
sink = db.table_sink(
table="logs",
mode="insert", # 仅插入
)Update 模式
只更新已存在的行,绝不插入新行(UPDATE ... WHERE):
sink = db.table_sink(
table="bidding",
mode="update",
keys=("id",), # WHERE 匹配条件
update_columns=("deadline", "tender_deadline"), # 只重写这些列
)- 适合"目标行由其他流程创建、本任务只回填部分字段"的场景。
- 目标行不存在时跳过该条并记录一条 INFO 日志,不报错;MySQL 下"值未变化" 的重复更新同样按 0 行处理。
- 不生成
INSERT语句,目标表存在无默认值的NOT NULL列时也不会触发 warning,且不存在误插新行的风险。
更新控制(Upsert / Update 行为)
upsert 与 update 模式下,可通过 update_columns、update_expr 精确 控制写入的列:
sink = db.table_sink(
table="results",
mode="upsert",
keys=("id",),
update_columns=("data",), # 只重写这些列
update_expr={"updated_at": "NOW(6)"}, # 写入时执行的原始 SQL 表达式
)update_columns:允许重写的白名单列;默认重写除keys外的所有载荷列。 设为空列表()表示不更新任何载荷列,只应用update_expr。update_expr:列名到原始 SQL 表达式的映射,写入时渲染执行(例如updated_at=NOW(6))。- 两者仅适用于
upsert和update模式;update_columns为空且没有update_expr时配置无效。
按列写入策略(null 保护)
update_columns 的条目可以是列名(默认无条件覆盖),也可以是 {name, policy} 对象,按列声明载荷值与库中原值的合并方式。三种策略:
| policy | 行为 | 生成 SQL |
|---|---|---|
overwrite(默认) | 无条件用载荷值覆盖,载荷 null 也会写入 NULL | SET col = :val |
skip_null | 载荷值为 null 时该列不写,保留库中原值 | null → 列从 SET 剔除 |
backfill | 只在库中当前值为 NULL 时写入载荷值,原值非空则保持 | SET col = COALESCE(col, :val) |
rows_sink:
type: mysql_table_sink
connector: downstream_mysql
table: bidding
mode: update
keys: [id]
update_columns:
- deadline # 无条件覆盖
- tender_deadline # 无条件覆盖
- name: tenderee
policy: skip_null # 载荷 null 不写,避免清空已有值
- name: publish_date
policy: backfill # 只回填空值,不覆盖已有值Python 侧同样接受混合条目:
sink = db.table_sink(
table="bidding",
mode="update",
keys=("id",),
update_columns=(
"deadline",
{"name": "tenderee", "policy": "skip_null"},
),
)注意事项:
- 策略对
update和upsert同样生效(ON DUPLICATE KEY UPDATE子句 应用相同规则)。 skip_null过滤后整个SET为空时,该条载荷跳过并记录一条 INFO 日志, 不报错。- 策略列不能是
keys中的列,也不能与update_expr中同列的原始 SQL 表达式同时配置(构造时报错);纯列名条目与update_expr的覆盖关系 保持不变。
JSON 序列化控制
载荷中的 list/dict 值默认按目标列类型自动处理(serialize_json="auto"): 列类型为 JSON 时原样写入,否则序列化为 JSON 字符串:
sink = db.table_sink(
table="results",
mode="insert",
serialize_json="always", # 强制序列化为 JSON 字符串
)serialize_json 可选值:auto(默认)、always(始终序列化为字符串)、 never(永不序列化)。
状态存储
State Store
键值对存储,用于任务状态:
state = db.state_store(table="onestep_state")
# 在任务中使用
@app.task(source=...)
async def process(ctx, item):
count = await ctx.state.get("processed_count", 0)
await ctx.state.set("processed_count", count + 1)Cursor Store
游标存储,用于增量同步位置:
cursor = db.cursor_store(table="sync_cursor")
source = db.incremental(
table="orders",
key="id",
cursor=("updated_at", "id"),
state=cursor,
)YAML 配置
resources:
db:
type: mysql
dsn: "mysql+pymysql://root:root@localhost:3306/app"
order_queue:
type: mysql_table_queue
connector: db
table: "orders"
key: "id"
where: "status = 0"
claim:
status: 9
ack:
status: 1
batch_size: 100
results:
type: mysql_table_sink
connector: db
table: "results"
mode: "upsert"
keys:
- "id"
update_columns:
- "data"
update_expr:
updated_at: "NOW(6)"
serialize_json: "auto"
cursor:
type: mysql_cursor_store
connector: db
table: "sync_cursor"
tasks:
- name: process_orders
source: order_queue
emit: results
concurrency: 16最佳实践
1. 索引优化
-- 表队列:确保查询条件有索引
CREATE INDEX idx_status ON orders(status);
-- 增量同步:确保游标字段有索引
CREATE INDEX idx_cursor ON users(updated_at, id);2. 批量大小
# 小批量:低延迟
batch_size=10
# 大批量:高吞吐
batch_size=10003. 并发控制
# 表队列:可高并发(行级锁)
@app.task(source=source, concurrency=16)
# 增量同步可并发处理;Runner 每轮仍只调用一次 fetch(limit)
# concurrency 限制处理中 Delivery,不会发起 100 条并发 SELECT
@app.task(source=incremental, concurrency=100)4. 连接池
# URL 参数配置连接池
db = MySQLConnector(
"mysql+pymysql://user:pass@host/db"
"?pool_size=10"
"&max_overflow=20"
"&pool_recycle=3600"
)5. 可靠持久游标与重试
生产增量同步应显式绑定 mysql_cursor_store 和稳定 state_key。成功记录可以乱序 完成,但持久游标只推进到连续成功前缀;同一批同时释放的确认会合并为一个状态写。 失败重试会重新投递同一逻辑行并增加 Envelope.attempts,缺口重试期间不会继续发出 后续 SQL 查询。达到任务 max_attempts 后 Source 停在失败行之前。进程重启从已持久 游标恢复,未提交的行会重放。
从 onestep-mysql 0.5.1 起,mysql_cursor_store 能持久化游标中的 MySQL DATETIME 组件:它以带类型标记的 ISO-8601 JSON 保存,重启后恢复为原始 datetime(保留微秒)再参与 keyset 查询。已有的纯 JSON 游标继续兼容;从 0.5.0 升级不需要迁移游标表,也不要手工推进一个因提交失败而尚未确认的游标。
mysql_cursors:
type: mysql_cursor_store
connector: mysql_source
table: onestep_cursor
auto_create: true
order_source:
type: mysql_incremental
connector: mysql_source
table: view_order_sync
key: orderKey
cursor: [orderCreateTime, orderKey]
state: mysql_cursors
state_key: feishu-order-sync-v1完整的生产参数、飞书 Insert 键索引、handler 契约和故障恢复流程参见 实战篇:MySQL 订单流水同步到飞书多维表格。