两个经营主体都可能存在编号为1的配送任务。查询条件只写WHERE id=1时,结果可能跨越组织边界;幂等记录如果也只用request_id做全局键,还可能把A的操作结果直接返回给B。单独部署或共用数据库,都需要把这层业务范围说清楚。
下面使用SQLite复合主键,展示任务读取、条件写入与请求结果如何使用同一租户范围。代码中的Session是本地测试对象,用来模拟已经认证的上下文,不提供登录、令牌校验或真正的角色系统。
记录 | 本例主键 | 避免的混淆 |
|---|---|---|
task | tenant、id | 不同租户的相同任务编号 |
operation | tenant、actor、request_id | 不同租户或操作者复用请求号 |
claim函数不接收来自请求正文的tenant参数,只从session取范围。实际服务应在认证与授权层构造这个上下文,不能让客户端直接创建Session。示例权限can_claim同时限制业务处理与结果回放,用于验证撤权后的旧请求不会直接读取缓存结果。
请求内容另存task_id与expected_version。联合主键判断“哪一次请求”,参数比较判断“是不是同一份输入”;两者都需要保留。相同请求号换了任务编号,不能返回旧成功。
read_task使用tenant与id。claim查询operation时使用tenant、actor和request_id;写任务时再检查tenant、id、state与version。业务更新与成功结果写入处在同一事务里,插入结果失败也不能留下已改动但没有记录的任务。
本例只保存成功操作,冲突返回conflict。调用方不得因冲突而不受控地更换请求号重试;本例也没有给冲突结果提供持久回放保证。下面代码可以直接运行。
import json
import sqlite3
from contextlib import closing
from dataclasses import dataclass
@dataclass(frozen=True)
class Session:
tenant: str
actor: str
can_claim: bool
def read_task(db, session, task_id):
if not session.can_claim:
raise PermissionError("permission revoked")
return db.execute(
"SELECT state, version FROM task WHERE tenant=? AND id=?",
(session.tenant, task_id)
).fetchone()
def claim(db, session, task_id, request_id, expected):
if not session.can_claim:
raise PermissionError("permission revoked")
payload = json.dumps([task_id, expected], separators=(",", ":"))
scope = (session.tenant, session.actor, request_id)
with db:
prior = db.execute(
"SELECT payload, result FROM operation "
"WHERE tenant=? AND actor=? AND request_id=?", scope
).fetchone()
if prior:
if prior[0] != payload:
raise ValueError("request payload conflict")
return prior[1]
changed = db.execute(
"UPDATE task SET state='claimed', version=version+1 "
"WHERE tenant=? AND id=? AND state='ready' AND version=?",
(session.tenant, task_id, expected)
).rowcount
if changed != 1:
return "conflict"
db.execute("INSERT INTO operation VALUES (?, ?, ?, ?, ?)",
(*scope, payload, "accepted"))
return "accepted"
def main():
with closing(sqlite3.connect(":memory:")) as db:
db.executescript("""
CREATE TABLE task (tenant TEXT, id INTEGER, state TEXT, version INTEGER,
PRIMARY KEY(tenant, id));
CREATE TABLE operation (tenant TEXT, actor TEXT, request_id TEXT,
payload TEXT, result TEXT, PRIMARY KEY(tenant, actor, request_id));
INSERT INTO task VALUES ('A', 1, 'ready', 1);
INSERT INTO task VALUES ('B', 1, 'ready', 1);
""")
a = Session("A", "rider", True)
b = Session("B", "rider", True)
assert len(db.execute("SELECT tenant FROM task WHERE id=1").fetchall()) == 2
assert claim(db, a, 1, "req-1", 1) == "accepted"
assert read_task(db, a, 1) == ("claimed", 2)
assert read_task(db, b, 1) == ("ready", 1)
assert claim(db, a, 1, "req-1", 1) == "accepted"
assert claim(db, a, 1, "new-key", 1) == "conflict"
assert claim(db, b, 1, "req-1", 1) == "accepted"
try:
claim(db, a, 2, "req-1", 1)
except ValueError:
pass
else:
raise AssertionError("key reused for another task")
try:
claim(db, Session("A", "rider", False), 1, "req-1", 1)
except PermissionError:
pass
else:
raise AssertionError("revoked session read cached result")
assert db.execute("SELECT COUNT(*) FROM operation").fetchone()[0] == 2
print("unscoped task lookup: 2 rows")
print("A claim: B still ready")
print("same request key: independent tenant results")
print("stale version: conflict")
print("changed payload and revoked permission: rejected")
print("operation records: 2")
if __name__ == "__main__":
main()python demo.py本次使用Python 3.12.10、SQLite 3.49.1执行,实际输出为:
unscoped task lookup: 2 rows
A claim: B still ready
same request key: independent tenant results
stale version: conflict
changed payload and revoked permission: rejected
operation records: 2代码首先故意做一次不带租户条件的只读查询,得到两行,作为缺少范围的反例;没有执行不带租户条件的更新。随后A接手自己的任务1,立即读取B任务1,仍为ready、版本1,证明本次写入只修改了A的行。
A重试req-1得到原accepted,而带旧版本的新请求返回conflict。B也使用req-1接手自己的任务1,可以正常成功;最终operation只有两条,分别属于A和B。使用同一请求号更换任务,以及撤权后回放旧请求,都由代码中的异常断言检查。
这份例子由服务函数统一拼出范围条件,但数据库连接本身仍能读取整张表。它没有实现数据库行级安全、数据加密、管理员隔离或导出权限。要验证真实边界,还应检查批量导出、附件、缓存键、搜索索引和后台任务,而不是只看这一条UPDATE。
can_claim只是测试开关。真实授权通常还涉及任务区域、人员在岗状态与动作权限;授权检查必须在读取幂等结果之前进行,不能因为“已经处理过”就跳过当前权限。
示例使用单连接、固定顺序与内存数据库,验证的是查询范围和结果键的语义。迁移到多实例后,应保留数据库唯一约束与事务,另测并发争用;进程内字典或锁不能替代共享数据库的一致性约束。
作者:微订(上海逊柯计算机科技有限公司)。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。