关于java:实战问题-高并发架构设计以及超领现象解决

34次阅读

共计 3434 个字符,预计需要花费 9 分钟才能阅读完成。

当初 有一个场景,支付礼品,每个用户有次数限度,用户通过前端点击,调用了利用 A 的接口,外面调用了服务 B,服务 B 外面去调用了服务 C,留神服务 C 是其余部门的服务。服务 C 负责真正的发放礼品。(假如这个服务 C 咱们是不可批改的,A,B 是本人团队负责的,并且可能呈现高并发的状况)

咱们应该如何做这个次数限度呢?

假如每次支付礼品的流动有一个activityId,一个用户一个流动能够支付一件礼品,礼品有giftId,不能够多领,每个用户对应一个uid

查问是否能够支付

首先对于前端而言,进入零碎,首先须要获取用户是否曾经支付过,而这个是否曾经支付过,具体的实现咱们应该写在 B 服务中,用户通过利用 A,申请到服务 B,返回用户是否曾经支付的后果。

查问是否支付的流程大抵如下:
用户进入页面,前端如果有缓存的话,能够为他展现之前缓存的后果,假如没有缓存,就会申请 A 利用,A 利用会去申请 B 服务,B 服务首先须要判断礼品或者流动是否存在。

去 redis 外面取流动或者礼品是否存在,如果 redis 没有查问到,那么就查询数据库,返回后果,如果数据库都没有,阐明这个前端申请很可能是捏造的,间接返回后果“流动或者礼品不存在”,如果此时查问进去,的确存在,那么就须要去查问是否支付过,同样是查问 redis,不存在的状况下,查询数据库,再返回后果。,如果支付过,则会有支付后果,前端将按键置灰,否者用户按键能够支付。

下面的 redis 必定是须要咱们保护的,这里不开展讲。比方减少流动的时候,除了改数据库,同时须要 redis 外面写一份数据,key 能够是 activityId_giftId,记录曾经有的流动,用户胜利支付的时候,同样是不仅减少数据库记录,也须要往redis 写一份数据,key 能够是activityId_giftId_uid,记录该用户曾经支付过该流动的奖品。

然而下面的零碎,有一个问题,就是流动 / 礼品不存在的时候,申请会每一次都间接打到数据库,如果是歹意攻打,数据库就挂了。这里当然能够思考应用布隆过滤器,对申请参数中的流动 / 礼品做过滤,同时也能够思考其余的防爬虫伎俩,比方滑动窗口统计申请数,依据 ip,客户端id,uid 等等。

当然,如果能够保障 redis 数据牢靠,稳固,能够不申请数据库,redis不蕴含则阐明不存在,间接返回。然而这种做法须要在减少流动 / 批改商品的时候,同时将 redis 一起批改同步。如果 redis 挂掉的状况,或者申请 redis 异样,再去查询数据库。如果能承受批改数据库流动信息不立马更新,也能够思考更新完数据库,用音讯队列发一条音讯,收到再做 redis 更新。当然,这个不是一种好的做法,解耦合之后,减少了复杂度。后面说的做法,只有 redis 挂了,数据库实践上也支撑不了多久(极其状况)。

(当然,下面不是完满的计划,是个大抵流程)

支付礼品接口怎么解决?

首先流程上与下面的查问是否支付过有些相似,,然而在查问是否支付过这一步之后,有所不同。如果曾经支付过,则间接返回,然而如果没有支付过,须要调用 C 服务进行支付,如果调用 C 接口失败,或者返回支付失败,B 服务须要做的事,就是记录日志或者告警,同时返回失败。
如果 C 服务返回支付胜利,那么须要记录支付记录到数据库,并且更新缓存,示意曾经支付过该礼品,这也是下面为什么个别能间接查问缓存就能够晓得用户是否支付过的起因。

这个设计中,其实 C 服务才是真正实现办法奖品的服务,咱们做的 A 和 B 相当于调用他人的服务,做了两头服务,这种状况更须要记录日志,管制爬虫,歹意攻打等等,同时做好异样解决。

下面的设计,如果咱们来写段伪代码,来看看有什么问题?

    public String receiveGitf(int activityId,int giftId,String uid){
        // isExist 判断流动是否存在,外部包含 redis 和数据库申请,省略
        if(isActivityExist(activityId,giftId)){
            // 流动和礼品无效, 判断是否支付过
            if(!userReceived(uid,activityId,giftId)){
                // 没有支付过,调用 C 零碎
                try {boolean receivedResult = Http.getMethod(C_Client.class, "distributeGift");
                    if(receivedResult){
                        // 支付胜利更新 mysql
                        updateMysql(uid,activityId,giftId);
                        // 支付胜利更新 redis
                        updateRedis(uid,activityId,giftId);
                    }else{return "曾经领过 / 支付失败";}
                }catch (Exception e){
                    // 记录日志
                    logHelper.log(e);
                    return "调用领券零碎失败,请重试";
                }
            }
        }
        return "支付失败,流动不存在";
    }

看起来如同没有什么问题,支付胜利写redis,之后读到就不会再支付。然而高并发环境下呢?高并发环境下,很有可能呈现支付屡次的状况,因为网络申请不是刹时能够返回的,如果有很多个同一个 uid 的申请,同时进来,C 服务的解决或者提早比拟高。所有的申请都会梗塞在申请 C 服务这里。(网络申请须要工夫!!!)

这时候还没有任何申请胜利,所以 redis 基本不会更新,数据库也不会,所以的申请都会打到 C 服务,假如他人的服务是不牢靠的,能够屡次支付,那么所有的申请都会胜利,并且会有多条胜利的记录!!!

那如何来改良这个问题呢?
咱们能够应用 setnx 来解决,先申请setnx,更新缓存,而后只有一个能够胜利进来,如果真的胜利,再写数据库,如果异样或者申请失败,将缓存删除。

    public String receiveGitf(int activityId,int giftId,String uid){
        // isExist 判断流动是否存在,外部包含 redis 和数据库申请,省略
        if(isActivityExist(activityId,giftId)){
            // 流动和礼品无效, 判断是否支付过
            if(!userReceived(uid,activityId,giftId)){
                // 没有支付过,调用 C 零碎
                try {
                    // setnx
                    if(redis.setnx("uid_activityId_giftId")){boolean receivedResult = Http.getMethod(C_Client.class, "distributeGift");
                        if(receivedResult){
                            // 支付胜利更新 mysql
                            updateMysql(uid,activityId,giftId);
                        }else{
                            // 支付胜利更新 redis
                            deleteRedis(uid,activityId,giftId);
                            return "曾经领过 / 支付失败";
                        }
                    }else{return "曾经领过 / 支付失败";}
                }catch (Exception e){
                    // 记录日志
                    logHelper.log(e);
                    return "调用领券零碎失败,请重试";
                }
            }
        }
        return "支付失败,流动不存在";
    }

Redis 里,所谓 SETNX,是 「SET if Not eXists」 缩写,也就是只有 key 不存在的时候才设置,能够利用它来实现锁的成果。这样只有一个申请能够进入。


redis> EXISTS id                # id 不存在

redis> SETNX id "1"    # id 设置胜利 1

redis> SETNX id "2"   # 尝试笼罩 id,返回失败 0

redis> GET job                   # 没有被笼罩 "2"

这个场景下的问题曾经失去初步的解决,那这个 setnx 有没有坑呢?下次咱们聊一下 …

【刷题笔记】
Github 仓库地址:https://github.com/Damaer/cod…
笔记地址:https://damaer.github.io/code…

【作者简介】
秦怀,公众号【秦怀杂货店】作者,技术之路不在一时,山高水长,纵使迟缓,驰而不息。集体写作方向:Java 源码解析,JDBC,Mybatis,Spring,redis,分布式,剑指 Offer,LeetCode 等,认真写好每一篇文章,不喜爱题目党,不喜爱花里胡哨,大多写系列文章,不能保障我写的都完全正确,然而我保障所写的均通过实际或者查找材料。脱漏或者谬误之处,还望斧正。

2020 年我写了什么?

开源刷题笔记

素日工夫贵重,只能应用早晨以及周末工夫学习写作,关注我,咱们一起成长吧~

正文完
 0