支付子系统架构及常见问题

发布于:2025-09-14 ⋅ 阅读:(25) ⋅ 点赞:(0)

支付流程

    对于支付系统来说,它最重要的其实是安全,所以整个支付流程采用秘钥加签的方式进行操作,一共四对秘钥,以支付宝在线支付为例子,首先通过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。

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

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

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

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

一致性问题

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

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

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

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

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

延时双删

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

这就是支付系统的介绍。


网站公告

今日签到

点亮在社区的每一天
去签到