聊聊最近的一个项目/你做了什么/你印象最深的功能点

在线广告竞价平台(支付模块) 背景简述

那么我来说一下最近深度参与过的一个项目,就是广告竞价平台,这个平台主要分三大业务线,merchant后台(商户)、operating(运营)以及traffic(流量)平台

merchant后台管理广告主也就是商户的一些平台资质的材料的上传,广告物料的上传和维护,钱包以及充值和支付功能,

operting后台集成了审核系统,主要用来审核商户运营资质,同时对广告物料进行编排和管理,以及im实时通讯客服系统

traffic后台负责流量平台的管理,广告的检索以及投放功能,并且支持现金提现。流量主盈利的主要方式目前平台支持的就是cpm(Cost Per Mille), 即千次广告展现的费用,cpc(CPC(Cost Per Click), 即按照广告被点击的次数来计费。关键词竞价、信息流广告大多是这种模式。),cpa(cost per action),即按照用户行为来计费,一般为注册行为,注册成本。也包括CPD(CostPer Download)每次下载成本CPI(Cost Per Install)每次安装成本

主要负责模块

我个人主要负责的就是涵盖merchant和traffic业务线的支付系统,该系统可以支撑广告主的钱包充值、以及流量主的现金提现功能,通过钱包模块对支付体系进行解耦,分为三大模块,支付、钱包、以及提现,目前支持国内的在线支付方案包括支付宝以及微信,通过工厂模式对两大支付方案进行整合,能够支撑订单总量两千万左右,日增量50万,支付接口qps峰值400左右的高可用支付架构。

订单生成

首先因为我们订单总量已经达到了千万级,虽然每天也会有一些未支付订单的减量,但是单表肯定支撑不了这个量级的数据,一般业内就是800万到1000万开始分表,所以我们根据单表500万级(考虑订单增量,打出富裕)的标准分了4张表(也可以说利用垂直分表,将订单表分为订单和订单明细表,订单表只存储订单id,状态以及商户uid,明细表存储支付价格,订单日期等明细字段,不理解就不要说),分表方案采用水平分表,当时我们其实有两套方案,一套是日期区间分表,按自然月分,但是这种方案可能会造成订单分布不均的情况,加重单表压力,所以我们采用了hash取模算法进行分表,对商户uid进行hash操作后对分表个数取余操作,指定用户订单的对应表,达到正态分布的效果,但是这也带来另外一个问题,由于分表后无法再使用自增字段,所以订单号全局唯一的问题就显现出来,这里我们使用毫秒级时间戳、增长序列、随机序列、以及节点id的方式组成唯一订单号,生成64位的二进制码,再转换为19位的长整型,契合mysql中的bigint字段,同时在订单生成接口之前利用redis的set数据类型作为拦截器,保证订单的全局唯一。

支付流程

您也知道,对于支付系统来说,它最重要的其实是安全,所以整个支付流程采用秘钥加签的方式进行操作,一共四对秘钥,以支付宝在线支付为例子,首先通过RSA2算法生成商户公钥以及商户私钥,同时支付宝平台会提供支付宝公钥和支付宝私钥,将支付宝公钥下载到平台项目中,同时将商户公钥上传到支付宝后台,当用户在前端表单中填写好充值金额后,生成对应商户订单,如果用户选择支付,将会把支付价格、订单号以及订单描述三个参数排列组合后利用商户私钥进行加签操作,同时将密文url进行重定向到支付宝统一支付接口,支付宝平台会利用商户公钥进行验签操作,验签通过后将对支付宝钱包余额进行减量操作,同时利用支付宝私钥对回调参数进行加密,包括商户订单号,支付宝订单号以及支付状态,支付成功后,支付宝平台会将带参进行回跳操作,回调到平台后,利用支付宝公钥对参数进行解密,随后利用回调的商户订单号或者支付宝订单号对平台的钱包余额以及订单状态进行修改操作。

支付一致性问题

由于我们平台的充值业务会面临一些高并发情况,也就是单用户可能同一时间点同时支付充值操作,如果一秒内同时有三笔50元的支付请求成功,后台可能会出现支付一致性问题,也就是余额可能只增加50的情况,这里为了保持数据一致性,我们采用了redis的setnx分布式锁进行操作,当单用户进行余额修改流程之前,先利用商户uid作为key获取分布式锁,余额修改完成后,释放分布式锁,一般情况下,考虑到程序的健壮性,防止服务宕机意外报错等情况发生,会将释放锁放到异常捕获机制的finally中,因为理论上finally肯定会执行,不会出现死锁问题,您觉得finally会百分之百执行吗?其实不一定,因为机房可能会发生物理断电的问题,即使进入try代码块,finally也不一定会执行,这样就造成了死锁问题,所以需要给分布式锁设置一个10秒的生命周期,如果10秒内没有修改成功,我们会认为该操作发生了异常自动释放锁。

分布式锁问题

设置了过期时间,如果业务还没有执行完成,但是redis锁过期了,怎么办?

加锁的时间是30秒.如果加锁的业务没有执行完,那么到 30-10 = 20秒的时候,就会进行一次续期,把锁重置成30秒.那业务的机器万一宕机了呢?宕机了定时任务跑不了,就续不了期,那自然30秒之后锁就解开了.

具体使用redisson模块

友商QPS问题

在做三方支付平台对接的时候,实际上支付宝的统一支付接口是有qps限制的,qps限制是100,也就是一秒内只支持100单的支付请求,超出的会直接返回403状态码,其实这种设计也是合理的,因为友商没有必要帮我们承担高并发请求,所以我在订单支付和请求支付宝接口之间做了一个缓冲区,生产者不会直接和消费者产生关系,而是通过缓冲区解耦,这个缓冲区就是异步任务队列,队列容器我采用redis数据库,因为redis性能优势比较明显,同时内置的list数据类型比较契合队列这种数据结构,工具类内置了,初始化方法,入队方法,出队方法,队列长度,以及查重唯一方法。每当商户提交支付请求,将订单id进行入队操作,遵循fifo原则,在消费者端使用多线程的方式进行消费,也就是出队操作,这里的线程数我们可以通过变量进行控制,峰值线程数大概维持在80左右,不会突破100,起到一个削峰填谷的作用。

支付回跳问题

这是QA给我提的一个问题,就是在支付过程中,会有因为网络因素或者其他原因导致支付宝没有回跳成功,此时客户端就会停留在支付页面动不了,造成问题,其实没有回跳成功,不外乎两种结果,就是支付成功,或者支付失败,解决这个问题可以采用定时任务,每隔十秒检测订单状态为支付中的订单,通过订单id做为参数,请求支付宝的订单查询接口,用来判断是否支付成功,随后定时任务会自动将接口返回的订单状态同步到数据库的订单状态中,这里定时任务我采用的是redis中的有序集合,利用zadd方法,将支付中状态订单id作为key,delay参数设置为当前时间戳加10秒后时间,入库。将时间作为score标识物,出队调用zrangebyscore方法,min_score永远为0,max_score就是当前时间戳,这样遍历会形成一个实践窗口,只要定时任务进入时间窗口,就会自动执行,非常方便。

延时队列实现

class DelayRedisQueue:

    def __init__(self,key):

        self.key = key

        self.r = redis.Redis(decode_responses=True)


    # 入队
    def add(self,uid,delay=0):

        print("延时队列入队,%s秒后执行删除uid%s的任务" %(delay,uid))

        self.r.zadd(self.key,{uid:time.time()+delay})

    # 删除延时任务
    def remove(self,uid):

        return self.r.zrem(self.key,uid)

    # 出队逻辑
    def pop(self):

        # 起始位置
        min_score = 0

        # 区间结束为止
        max_score = time.time()

        # 获取队列
        res = self.r.zrangebyscore(self.key,min_score,max_score,start=0,num=1,withscores=False)

        if res == None:

            print("暂无延时任务")

            return False

        if len(res) == 1:

            print("延时任务到期,返回执行任务的uid%s" % res[0])

            return res[0]

        else:

            print("延时任务没有到时间")

            return False

订单缓存问题 mysql-redis数据一致性问题

我的订单模块由于读取的是订单表,为了分担数据库压力,我们使用redis进行缓存操作,但是如果订单状态修改了,redis中的数据需要做同步,这就带来了mysql-redis的数据同步问题。

最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。

所以,我们追求的是尽可能保证缓存和数据库的最终一致性。

在开始之前,我们先来科普一下缓存+数据库读写,最经典的Cache Aside Pattern。

读取:先读取缓存,缓存里没有,读取数据库,然后返回响应,顺斌保存缓存

更新:先更新数据库,然后删除缓存

为什么是删除缓存,而不是更新缓存?

并发情况下更新缓存可能会带来种种问题,直接删除缓存更加稳妥。 缓存更新在很多时候需要耗费资源,直接删除,用时再从数据库读取,写进缓存,更省性能。

一致性问题

那么我们采用这种先更新数据库,再删除缓存,可能会出现什么问题呢?

假如,我们更新数据库成功,接下来还没来删除缓存,或者删除缓存失败怎么办?

那么很明显,这时候其它线程进来读的就是脏数据。

先删除缓存,再更新数据库一致性问题

我们看一下,如果先删除缓存,再更新数据库可能会带来什么问题。在并发情况下,先删除缓存,再更新数据库,此时数据库还未更新成功,这时候有其它线程进来了,读取缓存,缓存不存在,读取数据库,读取的是旧值,这时候,缓存不一致就发生了。

延时双删

就是在删除缓存,更新数据库之后,休眠一段时间后,再次删除缓存。利用的也是延时队列操作

这就是支付系统的介绍。

results matching ""

    No results matching ""