65行写个微信红包
docstore是一种创新的文档数据库,帮助开发者更快地完成复杂的业务需求。这个例子里,我们来看这样的一个需求:
- 发红包时设置可以抢到的人数
- 多个人可以并发的抢,然后知道自己抢到没有,抢到了多少
- 可以查看红包当前被抢的情况,都有谁抢到了
下面我们来看如何用不到65行代码实现一个微信红包
最后完成的代码:v2pro/quokka
业务逻辑的定义
docstore.Entity("Hongbao", `struct taken { 1: i64 taken_at 2: i64 taken_amount // unit fen}struct Doc { 1: i64 total_count 2: i64 total_amount // unit fen 3: i64 remaining_amount // unit fen 4: map<string, taken> takens}`).Command("create", `function handle(doc, req) { doc.total_count = req.count; doc.total_amount = req.amount; doc.remaining_amount = req.amount; doc.takens = {}; return {};}`, `struct Request { 1: float64 amount 2: i64 count}struct Response {}`).Command("take", `function handle(doc, req) { if (doc.takens[req.username]) { throw 'one user can not take twice' } var takens_count = Object.keys(doc.takens).length; if (takens_count == doc.total_count) { throw 'nothing left' } if (takens_count == doc.total_count - 1) { // last one, take all var taken_amount = doc.remaining_amount; doc.remaining_amount = 0; doc.takens[req.username] = {taken_at: Date.now(), taken_amount: taken_amount}; return {taken_amount: taken_amount}; } var taken_amount = calcRandomAmount(doc.remaining_amount, doc.total_amount, doc.total_count); doc.remaining_amount -= taken_amount; doc.takens[req.username] = { taken_at: Date.now(), taken_amount: taken_amount }; return {taken_amount: taken_amount};}function calcRandomAmount(remaining_amount, total_amount, total_count) { var cap_amount = 2 * total_amount / total_count; if (remaining_amount < cap_amount) { cap_amount = remaining_amount; } return Math.floor(randBetween(1, cap_amount));}function randBetween(min, max) { return Math.random() * (max - min) + min;}`, `struct Request { 1: string username}struct Response { 1: float64 taken_amount}`)
可以数数,一共就65行。这里先定义了一个了业务对象(entity)叫“Hongbao”。
struct taken { 1: i64 taken_at 2: i64 taken_amount // unit fen}struct Doc { 1: i64 total_count 2: i64 total_amount // unit fen 3: i64 remaining_amount // unit fen 4: map<string, taken> takens}
其中的“taken”对象的含义是一次红包的领取。map<string, taken>的key是领取人,value是领取的详情,包括什么时候领取的,抢到了多少钱。
然后定义了两个命令,create和take。先来看create
function handle(doc, req) { doc.total_count = req.count; doc.total_amount = req.amount; doc.remaining_amount = req.amount; doc.takens = {}; return {};}
虽然docstore框架是用golang实现的,但是命令的handler是拿javascript写的。这个命令很简单,就是初始化一下Hongbao。
然后是take命令
function handle(doc, req) { if (doc.takens[req.username]) { throw 'one user can not take twice' } var takens_count = Object.keys(doc.takens).length; if (takens_count == doc.total_count) { throw 'nothing left' } if (takens_count == doc.total_count - 1) { // last one, take all var taken_amount = doc.remaining_amount; doc.remaining_amount = 0; doc.takens[req.username] = {taken_at: Date.now(), taken_amount: taken_amount}; return {taken_amount: taken_amount}; } var taken_amount = calcRandomAmount(doc.remaining_amount, doc.total_amount, doc.total_count); doc.remaining_amount -= taken_amount; doc.takens[req.username] = { taken_at: Date.now(), taken_amount: taken_amount }; return {taken_amount: taken_amount};}
这里实现的是抢红包的主流程,分别对应了这么几条规则
- 抢过了不能再抢
- 抢完了就没得抢了
- 最后一次领取拿走全部的红包余额
- 其余情况用随机的算法计算这次能领到多少
具体的随机算法实现在了calcRandomAmount的子函数里:
function calcRandomAmount(remaining_amount, total_amount, total_count) { var cap_amount = 2 * total_amount / total_count; if (remaining_amount < cap_amount) { cap_amount = remaining_amount; } return Math.floor(randBetween(1, cap_amount));}function randBetween(min, max) { return Math.random() * (max - min) + min;}
注意的是计算是用分作为单位,用整数计算的,保证了不会出零头的问题。
然后我们来看一下这个红包后台怎么被调用:
func Test_take_hongbao(t *testing.T) { should := require.New(t) execAndExpectSuccess(t, "/Hongbao/create", "EntityId", "123", "CommandRequest", runtime.NewObject("amount", 1314, "count", 3)) resp := execAndExpectSuccess(t, "/Hongbao/take", "EntityId", "123", "CommandRequest", runtime.NewObject("username", "tom")) taken1 := jsoniter.Get(resp, "data", "taken_amount").ToInt() resp = execAndExpectError(t, "/Hongbao/take", docstore.ErrBusinessRuleViolated, "EntityId", "123", "CommandRequest", runtime.NewObject("username", "tom")) should.Equal("one user can not take twice", jsoniter.Get(resp, "errmsg").ToString()) resp = execAndExpectSuccess(t, "/Hongbao/take", "EntityId", "123", "CommandRequest", runtime.NewObject("username", "jerry")) taken2 := jsoniter.Get(resp, "data", "taken_amount").ToInt() resp = execAndExpectSuccess(t, "/Hongbao/take", "EntityId", "123", "CommandRequest", runtime.NewObject("username", "donald")) taken3 := jsoniter.Get(resp, "data", "taken_amount").ToInt() should.Equal(1314, taken1+taken2+taken3) resp = execAndExpectError(t, "/Hongbao/take", docstore.ErrBusinessRuleViolated, "EntityId", "123", "CommandRequest", runtime.NewObject("username", "lily")) should.Equal("nothing left", jsoniter.Get(resp, "errmsg").ToString()) resp = queryAndExpectSuccess(t, "/entities/Hongbao/123") fmt.Println(string(resp))}
这个测试调用的就是这么三个url
- POST http://127.0.0.1:9865/docstore/Hongbao/create
- POST http://127.0.0.1:9865/docstore/Hongbao/take
- GET http://127.0.0.1:9865/docstore/entities/Hongbao/123
最后一条println打印出来的json
{ "errno":0, "entity_version":6, "data":{ "remaining_amount":0, "takens":{ "tom":{ "taken_at":1512130112473, "taken_amount":302 }, "jerry":{ "taken_at":1512130112473, "taken_amount":143 }, "donald":{ "taken_amount":869, "taken_at":1512130112473 } }, "total_count":3, "total_amount":1314 }}
那么docstore除了把javascript翻译成了go进行编译执行之外,提供了什么好处呢?
- 并发控制:命令是串行处理的。在javascript代码里完全就按照没并发的情况来写
- 性能:整个计算都是在内存里进行的,每次落盘只是插入一条event log。因为计算前就进行了命令的排队(先确保发到同一个master节点,然后master节点内部用go channel进行了串行化),所以没有并发写入带来的锁的成本。
- 强类型:虽然是动态类型的,但是所有对象(doc,request,response)都是有schema定义的。
参见:创新的主存储方案
参见:我们需要什么样的数据库