带GSI的DynamoDB表设计:满足多场景唯一键查询需求
DynamoDB
person 表设计落地方案 主表核心键规则
- 分区键:
uuid(字符串类型),完全匹配按UUID查询单用户全量数据的需求,单条点查性能稳定在毫秒级,无额外开销。 - 写入约束:所有写入记录必须携带全局唯一的
uuid,其余业务字段(name/state/city/bio/age)无强制写入要求。
第一类查询(按UUID查全量)
直接用主表点查即可,不需要走索引,是DynamoDB性能最优、成本最低的查询方式。
第二类查询(按城市、州查人员姓名列表)
配置第一个GSI(记为GSI_Location_Name)即可,不需要做额外的表维护:
- GSI分区键:
state_city(字符串类型,写入时固定拼接为{state}#{city}格式,例如马萨诸塞州波士顿对应值为Massachusetts#Boston) - GSI排序键:
name(字符串类型) - 投影配置:KEYS_ONLY 即可,不需要投影全量字段,最大程度降低存储和读取成本
- 查询方式:指定GSI分区键为目标
state_city值,直接查询该分区下的所有排序键,就能拿到对应地区的全部人员姓名,不需要跨分区扫描,性能和成本都可控。
第三类查询(获取所有唯一城市/州组合)
先明确你提到的GSI全表扫方案的问题
直接扫描GSI_Location_Name只提取分区键的方案成本随人员数据量线性增长,不适合线上常规调用:
DynamoDB扫描操作的RCU消耗是按实际扫描过的数据量计算的,哪怕你通过投影表达式只取分区键字段,只要扫描过的用户条目都会计入读取容量。如果你的人员表有上百万条记录,哪怕城市组合只有几千个,也要扫完上百万条索引条目,不仅RCU消耗高,扫描耗时也会到秒级,只适合一次性离线统计,不能作为线上接口的实现方案。
标准无额外成本方案:稀疏GSI自动去重
不需要加Lambda、不需要维护二级表、不需要引入外部缓存,只用加第二个稀疏GSI(记为GSI_Unique_Location)就能实现,零额外运维成本:
- 给主表增加一个可选字段
loc_marker,写入规则非常简单:写入新人员记录时,只有当该记录所属的
state_city组合是第一次出现时,才给loc_marker赋值为固定字符串"EXISTS";同地区后续写入的所有人员记录,该字段留空不写入。
并发写入同地区新用户时如果出现重复标记的极端情况,客户端扫描后做一次内存去重即可,概率极低几乎不产生额外开销。 GSI_Unique_Location键配置:- 分区键:
state_city(和第一个GSI的拼接值完全一致) - 排序键:
loc_marker - 投影配置:KEYS_ONLY
- 分区键:
- 原理:DynamoDB的GSI只会对分区键、排序键字段同时存在的记录建立索引,同个
state_city下只有第一条记录带loc_marker字段,因此这个GSI里每个城市/州组合只会对应1条索引记录,天然完成去重。 - 查询方式:全量扫描这个GSI即可,扫描的总条目数等于你全表唯一的城市/州组合总数——哪怕总人员数据有上千万,只要全国/全球的城市/州组合只有几万个,扫描只需要读几万条记录,RCU消耗极低,速度也能稳定在百毫秒级。
- 删除适配:如果某个地区的最后一条人员记录被删除,同步删除对应GSI里的marker条目即可,逻辑简单没有一致性问题。
你提到的两个备选方案的明显缺陷
- DynamoDB流+Lambda维护单条二级表记录:DynamoDB单条记录有400KB的硬大小上限,城市/州组合数量多了会直接写不下;同时高并发下多个Lambda同时更新同一条记录会触发写冲突,需要加乐观锁重试,逻辑复杂容易丢数据。
- 引入Redis/Memcached:除了额外的资源成本,还要处理缓存和数据库的一致性问题,新增/删除地区时双写逻辑会增加大量故障点,完全没有必要。
内容的提问来源于stack exchange,提问作者mmachenry
相关产品推荐
相关产品推荐

