如何在Cloud Spanner表中限制每个用户仅存20条最新搜索记录?
Cloud Spanner 单用户存储行数限制方案
单事务还是分事务?
直接给结论:高频写入场景下,分事务比单事务更靠谱,原因如下:
- 单事务把写入新记录、查询计数、删除旧记录绑定在一起,事务持有锁的时间过长,百万级请求下极易触发锁冲突,导致事务重试甚至失败,直接拖垮写入性能;而且一旦事务回滚,新的搜索记录也无法写入,影响可用性。
- 你当前的分事务思路方向正确,但可以优化:写入事务只做最简单的插入操作,别加查询计数的步骤——查询本身会增加读锁开销,完全没必要。直接在插入后启动独立的删除事务即可,哪怕用户当前记录没超20条,删除语句也不会生效,反而省去了计数的额外开销。
更优的实现方案
1. 调整表结构,从根源提升操作效率
你的主键是search_id,查询用户旧记录时需要全表扫描或额外建索引,效率低且易加锁。建议改成复合主键:
CREATE TABLE user_searches ( user_id STRING(MAX) NOT NULL, occurred_on TIMESTAMP NOT NULL, search_id STRING(MAX) NOT NULL, search_text STRING(MAX) NOT NULL ) PRIMARY KEY (user_id, occurred_on DESC, search_id);
调整后同一个用户的所有记录会存储在同一个分片内,按时间倒序排列,查询最新/旧记录、执行删除操作的效率会大幅提升,完全不需要额外创建索引。
2. 写入与清理完全解耦(最适配高频场景)
如果业务能接受“短暂超量”(比如用户刚写入第21条记录,过几分钟才被清理),建议直接将写入和清理彻底分开:
- 写入流程:只执行单条插入操作,不添加任何额外逻辑,保证写入速度最快,完全避免锁冲突。
- 清理流程:后台运行定时任务(比如每小时一次),使用Spanner的
Partitioned DML批量清理超量记录:
PARTITIONED DELETE FROM user_searches WHERE user_id = @user_id AND (occurred_on, search_id) NOT IN ( SELECT occurred_on, search_id FROM user_searches WHERE user_id = @user_id LIMIT 20 )
Partitioned DML是无锁操作,不会影响主流程的读写,百万级数据清理也能轻松处理,最终保证每个用户的记录不超过20条。
3. 实时清理的优化版(若无法接受超量)
要是业务要求绝对不能出现超过20条记录的情况,只能使用单事务,但要优化逻辑以缩短锁持有时间:
在同一个读写事务中,先插入新记录,再直接执行删除语句(跳过前20条,删除剩余记录),无需先查询计数——查询计数纯粹是增加锁开销的多余步骤。使用上述的删除SQL即可,这样事务逻辑更简洁,锁持有时间更短,冲突概率会大幅降低。
最后总结
- 高频场景优先选择写入+异步批量清理方案,性能拉满且无锁冲突,只要业务能接受最终一致性就用这个。
- 必须实时限制记录数的话,采用优化后的单事务+复合主键方案,尽量缩短锁持有时间。
- 避免使用原始分事务(先查计数再删除),查询计数属于增加锁开销的冗余操作。
内容的提问来源于stack exchange,提问作者Gabriel Piffaretti
相关产品推荐
相关产品推荐

