虚拟货币的实际消费
项目背景
最近由于项目业务原因,需要为系统设计虚拟币的充值及消费功能。公司内已经有成熟的支付网关服务,所以重点变成了如何设计项目内虚拟币的充值流程,让整个充值流程都实现幂等,确保用户的虚拟币余额不会重复增加或扣减。
商品购买及支付流程
微信支付时序图
(1)用户购买商品,商户后台请求生成支付订单并返回相关信息到客户端。
(2)客户端根据返回的信息唤起支付SDK,用户确认支付。
(3)用户完成支付后,支付系统会异步通知商户后台支付结果。
(4)商户后台接收支付回调,在回调接口内完成自己的业务逻辑。
(5)客户端在支付完成后延时一定时间从商户后台查询支付结果,此时若尚未接收到支付回调,可主动同步支付结果(保底策略)。
支付宝支付流程和微信支付类似,此处省略。正常情况下支付回调都会在毫秒级别进行通知回调。
虚拟币充值流程
虚拟币充值流程会嵌套支付回调流程中。若虚拟币没有完成完整的业务流程,支付系统会进行重试。因此业务流程需要支持幂等。
虚拟币充值流程
在实践过程遇到以下问题并最终得到解决:
(1)如何支持订单按用户维度分表?
支付回调信息只包括订单ID信息,在这种情况下一般只能根据订单ID进行分表。考虑到订单会越来越多,我们一开始就把订单按用户维度进行分表。一般情况下按用户维度的查询是很多的,而单纯按订单维度的查询会比较少。所以在预下单的时候把用户ID信息写入attach附加信息,支付回调时会携带上原先的附加信息,这样就可以知道用户及订单ID信息,完成后续操作。
在后来的优化中,订单ID生成时预留低位段存储用户订单表ID信息,这样完全不依赖附加信息进行传递,在用户进行自动扣费授权时的回调通知也可以适用。
(2)虚拟币如何做事务操作?
若只有用户虚拟币的数量信息,很容易会出现错误的重复操作。因此需要流水表配合进行事务操作,当流水表已经有相同的记录时说明当前操作是重复的,需要回滚虚拟币数值。通过数据库的单机事务即可实现虚拟币的正确变更,支持重入。
(3)如何做虚拟币的版本控制?
我们虚拟币每次变更都会对应一个版本号,在对虚拟币的并发操作时一般都是通过判断version是否符合预期时才进行数据变更。这个和乐观锁的控制类似,可是在这种情况下容易出现死锁,尤其是数据库性能差更容易触发。因此不再严格判断version版本号,只需要变更后的虚拟币数量不小于0即可,所有符合这个条件的变更都视为有效变更。这个情景适合不用版本号,只更新是做数据安全校验,适合库存模型,性能更高。
update table_xxx set avai_amount=avai_amount+:deltaAmount where user_id=:userId and avai_amount+:deltaAmount >= 0
有虚拟币消费才会出现并发修改,因此我们只需要严格控制虚拟币不出现余额不足以扣除的情况。
苹果内购虚拟币充值流程
用户在应用内购买商品时,客户端可以获取到用户ID、交易凭证receipt和交易ID等信息。整体购买流程和Android端差异比较大,因为对receipt验证流程参考Android下单流程做了拆解,更容易做到重入。
(1)客户端获取到充值列表;
(2)客户端支付成功后提交交易凭证receipt给服务端验证,服务端创建对应的凭证和订单记录,更新状态,完成充值;
苹果内购充值流程
苹果内购有以下事项需要注意:
(1)如何避免receipt被重复使用?
iOS客户端支付成功后能获取到transactionId和receipt信息,两者唯一对应。因此服务端在验证receipt有效后,可创建对应的transaction记录,根据transactionId进行分表,transactionId作为唯一键。这样可避免receipt被重复使用。
(2)transaction和订单记录的映射关系
订单记录包含渠道transactionId信息,这样在创建transaction记录后也可以唯一绑定到订单信息,避免重复创建订单。
设计总结
在设计订单系统时幂等性是首要考虑的问题,需要严格保证金额的准确性,不能给用户多扣款或多打款。一般情况下我们通过数据库单机事务和幂等重试等方式提高订单系统的健壮性。根据需要也可以选用消息队列等。