如何在Sanity中为文档实现递增属性?解决订单deliveryNumber冲突
Sanity中实现递增deliveryNumber的方案
一、Sanity有没有内置支持?
很明确:没有。Sanity的核心字段体系里没有全局递增序列的内置功能——毕竟它是面向文档的分布式架构,默认依赖的是随机/基于slug的_id,而非强一致性的自增数值。
二、怎么实现?还要避免竞态冲突!
直接读取最后一个订单的deliveryNumber再加1的做法在高并发场景下肯定会翻车(两个请求同时读到同一个最大值,然后生成重复的配送号)。可靠的做法是用独立计数器文档+事务性原子更新来解决,具体步骤如下:
1. 先创建一个计数器文档的Schema
在你的Sanity项目里新增一个专门用来维护配送号最大值的文档类型,比如叫deliveryCounter:
// schemas/deliveryCounter.js export default { name: 'deliveryCounter', title: '配送号计数器', type: 'document', fields: [ { name: 'currentNumber', title: '当前最大配送号', type: 'number', validation: Rule => Rule.required().min(0) } ], preview: { prepare() { return { title: '配送号全局计数器' } } } }
然后去Sanity Studio里手动创建一个这个类型的文档,把currentNumber初始值设为0(或者你现有订单里的最大配送号,避免重复)。
2. 用事务+版本校验实现原子递增
当创建新订单时,我们要把「更新计数器」和「创建订单」放在同一个事务里,同时用版本校验避免并发冲突。用Sanity的JS客户端示例代码如下:
import { createClient } from '@sanity/client' const client = createClient({ projectId: '你的项目ID', dataset: '你的数据集', token: '具有写入权限的token', useCdn: false // 写入操作不能用CDN }) async function createOrderWithDeliveryNumber(orderData) { let retryCount = 0 const maxRetries = 3 // 遇到并发冲突时重试3次足够了 while (retryCount < maxRetries) { try { // 第一步:获取当前计数器的最新状态 const counter = await client.fetch('*[_type == "deliveryCounter"][0]') const nextDeliveryNumber = counter.currentNumber + 1 // 第二步:执行事务,确保两个操作要么都成,要么都败 await client.transaction() // 条件更新:只有计数器的版本和我们读取到的一致时才允许更新 .patch(counter._id, { set: { currentNumber: nextDeliveryNumber } }, { ifRevisionID: counter._rev // 核心:避免并发覆盖 }) // 创建新订单,用刚生成的配送号 .create({ _type: 'order', deliveryNumber: nextDeliveryNumber, ...orderData // 你的其他订单字段,比如用户信息、商品列表等 }) .commit() console.log(`订单创建成功,配送号:${nextDeliveryNumber}`) return nextDeliveryNumber } catch (error) { if (error.message.includes('revision mismatch')) { // 说明有其他请求同时更新了计数器,重试即可 retryCount++ console.log(`遇到并发冲突,正在重试第${retryCount}次`) continue } // 其他错误直接抛出,比如权限问题、网络问题 throw error } } throw new Error('重试次数过多,暂时无法创建订单,请稍后再试') }
3. 核心避坑点
- 事务绑定:把计数器更新和订单创建绑在一个事务里,不会出现“计数器加了但订单没创建”的情况,保证数据一致性。
- 版本校验:
ifRevisionID参数是关键——如果有另一个请求已经更新了计数器,当前请求的更新会失败,我们通过重试来处理这种临时冲突。 - 重试机制:并发冲突是小概率事件,重试3次基本能解决大部分情况,不用过度重试。
4. 更简洁的备选:用Increment原子操作
Sanity的Patch API支持inc原子操作,不用手动计算数值,直接让服务器帮你加1:
async function getNextDeliveryNumber() { const counter = await client.fetch('*[_type == "deliveryCounter"][0]') // 原子递增计数器,返回更新后的文档 const updatedCounter = await client.patch(counter._id) .inc({ currentNumber: 1 }) .commit() return updatedCounter.currentNumber } // 创建订单时调用这个方法获取配送号 async function createOrder(orderData) { const deliveryNumber = await getNextDeliveryNumber() return client.create({ _type: 'order', deliveryNumber, ...orderData }) }
不过这个方法的小缺点是:如果递增计数器后创建订单失败,会导致配送号“跳号”。所以如果你的业务不允许跳号,还是优先用事务绑定的方案。
三、额外提醒
- 权限控制:一定要限制计数器文档的修改权限,只让后台服务或者授权的客户端能操作,防止恶意篡改。
- 初始值校准:如果是已有订单的老系统迁移,先统计现有订单的最大deliveryNumber,把计数器的初始值设为这个数,避免重复。
- 错误反馈:给用户清晰的错误提示,比如重试失败时告诉用户“暂时无法创建订单,请稍后再试”,不要让用户摸不着头脑。
内容的提问来源于stack exchange,提问作者ArneHugo
相关产品推荐
相关产品推荐

