GO在新零售营销领域的实践


许嘉华:讯联数据数据营销事业部技术总监


从事过服务端开发、移动开发、大数据开发、机器学习、项目管理等工作。


前言

      下午好,很荣幸今天来到这里为大家分享。我分享的题目是《Go在新零售营销领域的实践》。与Go有联系,是因为我们有用Go实现了一套优惠券系统。最近有给客户做私有化部署,客户有2000万用户。以下是我认为比较重要的几个点,做下分享:

一、券码设计

二、分库分表:怎么定sharding key

三、分布式事务:用户间转赠

四、性能测试




说下背景。"新"零售,技术上理解就是用线上运营顾客的方式运营线下顾客。

首先要做的是线下的数字化,与线上数据打通。现在大家比较熟悉的超市、便利店让你办会员卡,微信扫一扫就可以了,就是一种线下数据数字化的手段。

有了用户数据,就能刻画出用户画像,做用户分层,与线上那套是一样的。营销的传播方式可能多样,但是送的东西是不变的,比如给沉睡客户送一张券,期望激活它。       

 640?wx_fmt=png   

回到主题,优惠券系统简单来说一般有这几个场景:

1、商家向用户发一张券

2、导出券码,异业交换

3、H5活动,用户主动领一张券

4、APP里用户向用户转赠一张券

      优惠券系统怎么设计的?下面是非常简易的架构,"小米+步枪",最底层是MySQL(包含中间件)、redis,往上是对比如数据库层比较细粒度封装,UseCase就是场景,最后暴露出来的是API。

       640?wx_fmt=png


券码设计

     640?wx_fmt=png      

       券码的设计是这么定的,产品说券码不可重复,这是当然的,重复发出去就是一个BUG。券码也是不可预测的,比如有001、002、003,那从第三张就开始薅羊毛了,因为用户有了券码生成的逻辑。B端商户的服务员除了扫码核销还有手工核销的场景,因此券码不能太长,然后是要纯数字。有了这些前提条件,以往的ID生成规则基本上都够用了。比如UUID,虽然是非常的唯一,但是太长又不是纯数字,MongoDB的生成方式也挺不错的,但是MongoDB瞬时连续生成的ID是顺序的,也就是可预测了。同理,Twitter Snowflake的生成方式也是一样的问题。


        这时候需要使用一些伪随机数生成算法。所谓的伪随机数就是假的,是通过一些数学规则或数学工程式推演出来的,最多做一些高斯分布、均匀分布之类的变换,我找了一个最简单随机数生成算法,就是线性同余(Java的Random就是利用了这个算法)。它的公式很简单,是一个线性方程,Xn是一个种子,a、c、m都是参数,m是设置随机数最得大上限,通过种子生成下一个,下一个作为种子生成下下一个随机数。工程上非常简单。附带说下,如果直接使用语言的随机数包,因为随机数种子是固定或是没指定的,每次生成的随机数都是一样的。

       640?wx_fmt=png     

      线性同余还有这样的特性,精心挑选的参数可保证在整个周期内(m)随机数都是不重复的,需要满足的条件就是m和c是互为质数,a-1能被m的所有质数整除,如果m能被4整除,a-1也能被4整除。精心挑选这些,假设m定的是100,最后就会出现完全不会重复的100个数字,生成超过100个,因为数字总共就一百,那就进入下一个周期。

       640?wx_fmt=png      

       前面是提的一个假设,我们需要做试验验证下。先讲线性同余,参数acm再加一个种子,next就是线性公式,生成的就是输入m是模数,m是100就生成一百个数。这些都是图表显示,如果我们没有精心挑选的acm会导致什么样的结果?就会有很明显的周期性,比如m是3,这里生成的就是99,你能看到的99都能出现。如果是用直方图显示,那不是所有的数字都是随机产生的,会有很多是空白,也有很多是重复被生成,这样一定是不合格的,如果精心挑选的a是21,c是3,然后m是100,3是初始值,看起来好像是有一点重复,但是其实并不是,你看这个直方图就比较漂亮,最高是出现的次数就是x轴上面的一百,这个是出现的0-100有多少个数字,然后左边就会出现的频次,这样的随机就是最完美的。(https://github.com/XUJiahua/gomeetup20181021/blob/master/code/lcg_randomness/lcg_randomness.ipynb)


       640?wx_fmt=png        

试验下来确实不错,这样就已经满足了随机(人看起来),确实是随机并且不会重复,这样生成一百个,那一百个都是不同的。可预测性其实还是有问题(下文分解),知道了公式,如果知道了x0,X1,X2,就可以解方程组了,反正是初中的水平了就可以搞定,这样就可以推导出来a、c、m。


640?wx_fmt=png      

      前面讲的是单进程的LCG,种子都维护在进程里。从高可用与高性能的角度讲,多进程LCG是必须的。种子保存在数据库,LCG每次去生成,都要去从数据库里读种子出来,读完种子再更新一下。这个是有写锁的,如果说生成一个又一个,每次都要读数据库,相当于每次都是从数据库读的数据,这样对性能是非常差,所有的压力都压在数据库上面了,因此我们要生成一批。生成一批除了提高并发,还有解决了可预测性的问题,因为我知道了X0,X1,X2,知道10个以上就可以解出方程组推导出参数了,这个地方一定做shuffle,乱序之后就摸不准顺序。

       640?wx_fmt=png      

分库分表


      为什么分库分表,前面有提到用户特别多的情况。我们私有化案例里客户有2000万,券也会达到上10亿级,百亿级这么多的情况。而优惠券表有两个独立的查询场景,一是我要看我APP里券包,要根据uid查出所有我的券。第二个是根据券码查,服务员扫描券码做核销就可以了。这里,因为sharding key只有一个,比较纠结。其实有一些非常成熟的方案,异构索引,原理上就是维护两张内容一样的表,一张为uid索引,一张为code索引,通过冗余提高查询性能;但是这个是有成本的,维护第二份数据服务器的成本,为了保证两份数据的一致性,在考虑真正要落实异构索引这个方案之前,就要引入很多的中间件。不妨考虑一下其他的轻量方案。下面是一个朴素的想法,也只是一个假设。


640?wx_fmt=png

引入了一个sharding key,根据UID和券码都能推导出这个sharding key。这样两个索引都能用了,通过提取券码中shardID就知道在哪个分片上;如果是uid查,就从uid上面生成shardId就行了。至于两次HASH是否影响数据分片平衡性呢?做一次试验吧。


640?wx_fmt=png       

      这个是HASH取模,默认都是按UID做HASH,现在用shardid做HASH,需要说明的是,一次Hash就是根据UID做分片只有一次数据库分的HASH。对数据库分片就是HASH取模这么简单,如果是16进制的,那就想象是非常大的整数,16进制度给它32取模,因为最多是32,所以最多取两位就可以。这里生成了UID,左边的直方图是对一次HASH的结果,第二次是两次HASH的结果,这里代码也都有显示,大家后面可根据链接再去看一下。看起来效果是不错的,然后我们就可以放到测试,或者是生产上,生产上线之前生产还是做测试的,就是试验一下,最后结果还是不错的,看出来其实没有什么区别,HASH的比较均匀。(https://github.com/XUJiahua/gomeetup20181021/blob/master/code/hash_randomness/hash_randomness.ipynb)


分布式事务


      现在比较常用的就是基于队列的分布式事务。假如我要对A做删除然后对B做新增,以前一个MySQL事务就可以搞定,但现在数据不一定是在同一个实例和机器,需要MQ中间件来辅助。 

     

640?wx_fmt=png      

       比如有一个转赠的场景,A用户要对B用户做转赠操作,他需要把A的券码删掉,然后再插入别的券码。现在变成两阶段,第一阶段先删除A券码,添加一个转赠的消息到队列里,这里是一个本地事务操作。然后第二个是从队列里面取消消息插入再清理消息。


       因为没有装备事务的消息,如果消息服务不是事务的,上面有一种情况,把这个添加转赠消息包在删除事务里,这里转赠可能成功了,但是删除可能是失败的。这里的处理方式,是在第二阶段做一次预检,检查一下A券码是不是已经删掉了。还有一个问题,就是第二阶段的清理消息,如果插入成功后清理失败。这里的处理方式,是对插入操作做幂等处理。总之,在没有事务消息中间件的情况下我们就做一些业务上的补偿。


640?wx_fmt=png

  

    数据库事务与业务逻辑怎么写?我的方法是所有跟数据库有关的都包在DAL层,业务逻辑通过方法回调的方式进入。

       640?wx_fmt=png      

性能测试


     对于性能测试,我之前也用过WRK,AB这种,WRK和AB这种只能处理一些简单的场景,如一次HTTP GET之类的。但是优惠券这种场景,用户领一张券、查询一次、B端核销一次,没法方便得模拟。Locust介绍给大家,比起Jmeter,这是写代码而不是配置的方式。如果自动化测试是用Python写的,可以把性能测试和自动化测试放一起。因为是Python写的,本身有GIL限制,一个进程只能利用一个CPU,所以有多少个CPU就起多少个Slave。也有Go的Slave实现,这样Slave的数量可以少一些。性能测试解决两个问题,一个是瓶颈调优,一个是预估生产配置。

       640?wx_fmt=png       

       生产上采样,如果你有无数台机器,可以找一台机器每60秒采样到本地,然后传回自己电脑在线下分析,不用直接暴露端口出来。测试环境你可以直接pprof,然后后面跟一下地址。最新的pprof已经自带了火焰图,Y轴是调用栈,水平的宽度越宽,代表了时间占用比较长一点。然后越宽说明越是一个瓶颈,然后通过不断消除宽的长条来解决性能问题。

       640?wx_fmt=png         640?wx_fmt=png       

Best Practice

最后再讲一些有关Go方面有趣的经验。go generate真的很好用,我们公司会用它做API文档的生成。可以使用开源的swagger,也可以自己写。基于代码生成文档,这样文档更新和程序打包都是同步的,这样代码只要是最新的文档也是最新的,不会有那种文档太老,然后要去查代码的情况。

       640?wx_fmt=png       

      举个例子,我们之前枚举定义的比较失败,类型是int。脱离代码久了,也不知道哪些是哪些。如果要定位一个问题,我要从1开始数,数错了重新开始,后面该怎么解决呢?通过反射,然后再加上go generate,直接生成文档,有什么问题直接看下。

 640?wx_fmt=png       

       另外一个是proxy,是用struct embedding+interface。Go里面没有继承,通过embedding的方式就“继承“了原有的方法。如果原有的struct实现了A接口,如果做了Embedding,struct2也相当于实现了A的接口,这样就可以选择性的覆盖一些方法。比如做缓存或者是埋点,这样可以隔离mysql, redis。每一层、每个方法都做单一的事情,这个也是设计模式里提倡的东西。


640?wx_fmt=png

以上就是我的分享,谢谢大家!


2018 Gopher Meetup 巡回第五站-广州站火热报名中!

点击“阅读原文”即可报名



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值