选择多商户外卖系统二次开发版本时,可把区域规则修改转成可复现的验收用例。两个管理员读取同一修订号,后续写入需要原子识别冲突,并在重试时返回原请求结果。本文用SQLite事务和条件更新实现这项检查,说明扩展代码如何接续既有业务规则。
假设区域平台需要调整商家抽成。目标版本若已有配置,先按配置演示,确需扩展时再核接口及代码修改。这里专门处理配置写入的一致性,订单、退款和结算需要接续原规则,分别安排业务回归。示例区域A、B与费率都是测试输入。
rules以region_key为主键,保存revision和rate_bps。费率使用整数基点,0到10000对应零到全额。requests以request_id为主键,保存原区域、预期修订号、费率和首次成功修订号。请求编号的作用域在本例中为整个服务。
输入的request_id、key为非空字符串,修订号和费率严格接收整数,布尔值和小数被拒绝。修订号限制在SQLite有符号64位整数的安全递增范围内。生产系统应从可信身份确定区域操作权限,并对接入长度和审计要求作具体配置。
条件 | 事务行为 | 输出 |
|---|---|---|
新区域且预期修订号0 | 创建初始记录并条件更新 | 返回1并记录请求 |
原修订号匹配 | 原子更新且登记请求 | 返回递增版本 |
基于旧版本写入 | 回滚整个事务 | REVISION_CONFLICT |
同请求编号参数一致 | 读取首次请求结果 | 返回首次版本 |
同请求编号参数改变 | 回滚且保留原记录 | REQUEST_CONFLICT |
BEGIN IMMEDIATE在事务开始申请写锁,示例两个连接共享同一文件。首次写入创建区域初始值,再用WHERE revision条件更新。影响行数必须为1,随后保存请求记录,最后统一提交。失败路径rollback会撤回初始记录和配置修改。
已有请求先比较业务参数。原请求在后来配置已经升级的情况下重试,仍返回首次结果。返回修订号1只表示该请求原来成功形成版本1,当前配置可能已经是版本2,客户端需要区分请求回执和当前资源查询。
import json
import sqlite3
import tempfile
from pathlib import Path
def change(connection, request_id, key, expected_revision, rate_bps):
if not isinstance(request_id, str) or not request_id.strip():
raise ValueError('INVALID_REQUEST')
if not isinstance(key, str) or not key.strip():
raise ValueError('INVALID_KEY')
if type(expected_revision) is not int or not 0 <= expected_revision < 9223372036854775807:
raise ValueError('INVALID_REVISION')
if type(rate_bps) is not int or not 0 <= rate_bps <= 10000:
raise ValueError('INVALID_RATE')
connection.execute('BEGIN IMMEDIATE')
try:
prior = connection.execute(
'SELECT region_key, expected_revision, rate_bps, result_revision FROM requests WHERE request_id=?',
(request_id,),
).fetchone()
if prior:
if prior[:3] != (key, expected_revision, rate_bps):
raise ValueError('REQUEST_CONFLICT')
connection.commit()
return prior[3]
connection.execute(
'INSERT OR IGNORE INTO rules(region_key, revision, rate_bps) VALUES (?, 0, 0)', (key,),
)
updated = connection.execute(
'UPDATE rules SET revision=revision+1, rate_bps=? WHERE region_key=? AND revision=?',
(rate_bps, key, expected_revision),
)
if updated.rowcount != 1:
raise ValueError('REVISION_CONFLICT')
result_revision = expected_revision + 1
connection.execute(
'INSERT INTO requests VALUES (?, ?, ?, ?, ?)',
(request_id, key, expected_revision, rate_bps, result_revision),
)
connection.commit()
return result_revision
except Exception:
connection.rollback()
raise
def reject(call, expected):
try:
call()
except ValueError as error:
assert str(error) == expected
else:
raise AssertionError('request unexpectedly accepted')
with tempfile.TemporaryDirectory() as directory:
database = Path(directory) / 'rules.sqlite'
first = sqlite3.connect(database, timeout=5)
second = sqlite3.connect(database, timeout=5)
try:
first.executescript('''
CREATE TABLE rules (
region_key TEXT PRIMARY KEY NOT NULL,
revision INTEGER NOT NULL CHECK(revision >= 0),
rate_bps INTEGER NOT NULL CHECK(rate_bps BETWEEN 0 AND 10000)
);
CREATE TABLE requests (
request_id TEXT PRIMARY KEY NOT NULL,
region_key TEXT NOT NULL REFERENCES rules(region_key),
expected_revision INTEGER NOT NULL,
rate_bps INTEGER NOT NULL,
result_revision INTEGER NOT NULL
);
''')
assert change(first, 'R1', 'A', 0, 500) == 1
reject(lambda: change(second, 'R2', 'A', 0, 900), 'REVISION_CONFLICT')
assert second.execute('SELECT revision,rate_bps FROM rules WHERE region_key=?', ('A',)).fetchone() == (1, 500)
assert change(second, 'R3', 'A', 1, 600) == 2
assert change(first, 'R1', 'A', 0, 500) == 1
reject(lambda: change(first, 'R1', 'A', 0, 700), 'REQUEST_CONFLICT')
assert first.execute('SELECT COUNT(*) FROM requests').fetchone()[0] == 2
reject(lambda: change(first, 'R4', 'A', 2, 10001), 'INVALID_RATE')
reject(lambda: change(first, 'R4', 'A', 2, 0.5), 'INVALID_RATE')
reject(lambda: change(first, 'R4', 'A', True, 500), 'INVALID_REVISION')
reject(lambda: change(first, 'R4', '', 0, 500), 'INVALID_KEY')
reject(lambda: change(first, '', 'A', 2, 500), 'INVALID_REQUEST')
reject(lambda: change(second, 'R5', 'B', 1, 300), 'REVISION_CONFLICT')
assert first.execute('SELECT COUNT(*) FROM rules WHERE region_key=?', ('B',)).fetchone()[0] == 0
assert change(second, 'R6', 'B', 0, 0) == 1
assert change(first, 'R7', 'B', 1, 10000) == 2
output = dict(first.execute('SELECT region_key,revision FROM rules'))
print(json.dumps({'assertions': 16, 'revisions': output, 'connections': 2, 'staleWrite': 'REVISION_CONFLICT', 'retryResult': 1}))
finally:
second.close()
first.close()只依赖Python标准库sqlite3、tempfile、pathlib和json。保存为revision-example.py,执行python revision-example.py。本次Windows本地Python 3.10.11、SQLite 3.40.1,退出码0,16项断言通过。临时数据库在示例结束后由上下文清理。
{"assertions": 16, "revisions": {"A": 2, "B": 2}, "connections": 2, "staleWrite": "REVISION_CONFLICT", "retryResult": 1}
第一个连接用R1把A从版本0改到1、费率500。第二个连接仍带预期0提交R2,得到REVISION_CONFLICT,A保持版本1。第二个连接改带预期1提交R3,A进入版本2。再次执行R1仍返回1,请求表只保留两次成功写入。
同编号R1更换费率触发REQUEST_CONFLICT。超范围或小数费率、布尔修订号、空区域及空请求标识被拒绝。B第一次带预期1写入时,初始记录与更新同在事务,冲突回滚后B不存在。随后预期0可正常创建,0与10000基点边界均有覆盖。
这两个连接按顺序执行,模拟管理者持有陈旧版本的情况。运行结果验证本地SQL条件更新、事务回滚和重试记录,没有进行并发负载、崩溃恢复或真实系统测试。数据库锁超时与连接失败需要由上游识别,结果未确定时沿同一请求编号核对。
评审时要求目标版本演示相同场景,保留配置读值、请求参数、状态和最终结果。还要测试旧订单规则、权限、退款和结算是否接续。SQLite示例验证一项实现思路,目标项目的数据库、事务隔离、账号、存储与备份恢复需要相应环境验证。
源码交付和二次开发权限应列明文件、依赖与构建方法,升级后重跑原业务用例。版本选择结合需求满足方式、可维护性和实际交付资料,局部程序结果不能自动证明整套系统适配。
作者:微订(上海逊柯计算机科技有限公司)。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。