首页 星云 工具 资源 星选 资讯 热门工具
:

PDF转图片 完全免费 小红书视频下载 无水印 抖音视频下载 无水印 数字星空

大厂员工,手把手教你开发一个高并发、高可用的营销活动

编程知识
2024年08月27日 20:39

前言

这几年工作中做过不少营销活动,无论是电商业务、支付业务、还是信贷业务,营销在整个业务发展过程中都是必不可少的。如果前期营销宣传到位,会给业务带来一波不小的流量。那么作为技术,如何接住这波流量,而不是服务被打挂。今天大厂员工,手把手教你开发出一个高并发、高可用的营销活动。

体验

点我 - 体检地址

点我 - github源码



业务

任何脱离业务的技术都是无用功,所以我们先简单介绍一下业务。

业务希望我们的用户在比如购买商品,下单支付等场景,转化率尽可能的高。那么为了奖励和刺激用户,我们希望通过一些优惠券的方式,来激励符合我们规则的用户,进行下单,进行支付,进行借钱,进行购物等等后续操作。

比如某个用户符合我们活动的规则,第一步我们会给他展示优惠信息,激励他来进行下一步、完成这个任务,然后在给他发奖、核销等后续动作

校验抽奖资格

那么根据以上我们业务的分析,我们第一步就是,用户进来,我们查询活动,并且校验用户是否有资格参加活动。如果有多个活动,我们根据业务规则选择一个活动让用户进行参与。

那么这也是我们营销活动的起点,第一步。如果成千上百万的用户一下子涌进来,我们去查询数据库活动信息,并且校验规则,我们的数据库瞬间就会崩掉。所以我们的核心思路是:逐级分流,逐步分散流量。通过备份、限流、降级、熔断等手段提升可用性。

首先就是加缓存,对于一些静态页面,css,js等文件,可以放在客户端缓存或者CDN里面。对于活动信息以及规则,在活动上线之前,将这些信息缓存到redis里面。用户进来时,我们直接取redis里面查询活动信息,并且计算活动规则,全程不需要和数据库进行交互。最后,评估活动qps,进行降级限流,如果流量过大,直接进行拦截,防止系统雪崩。

public MktActivityInfo checkActivityRule(String phone) {
    // 从redis缓存中取
    MktActivityInfo activityInfo = activityCacheService.getActivityInfo();
    if (activityInfo == null || StringUtils.isEmpty(activityInfo.getActivityId())) {
        return null;
    }

    ActivityRuleContext context = new ActivityRuleContext();
    context.setPhone(phone);
    // redis缓存中取
    List<MktActivityRule> mktActivityRules = activityCacheService.listActivityRule(activityInfo.getActivityId());
    for (MktActivityRule mktActivityRule : mktActivityRules) {
        BaseRuleService baseRuleService = BaseRuleFactory.getBaseRuleService(mktActivityRule.getRuleKey());
        if (baseRuleService == null || !baseRuleService.check(context)) {
            return null;
        }
    }

    return activityInfo;
}

抽奖

一般到达抽奖,基本都是完成了前面的任务,比如支付,下单等等,最终获得抽奖资格

  1. 减库存。将奖品的库存信息提前缓存到redis里面,比如奖品100个缓存到redis里面。如果有100W人来抢100个奖品,最终也只有100个人通过redis的校验
Long num = RedisUtils.decr(CACHE_MKT_ACTIVITY_PRIZE_NUM, stringRedisTemplate);
if (num == null || num < 0) {

    // 将redis库存加回,可做可不做,看业务需求
    RedisUtils.incr(CACHE_MKT_ACTIVITY_PRIZE_NUM, stringRedisTemplate);
    throw new RuntimeException("redis库存不足 - " + ERROR_MSG);
}
  1. 根据业务场景,如果不是必中奖。在减库存之前,做一个随机数。如果在随机数之外,直接返回”奖品被抢完“,限制大部分流量进入到redis减库存
int seed = ThreadLocalRandom.current().nextInt(0, 100) + 1; // 1-100
int random = NumberUtils.toInt(RedisUtils.get(CACHE_MKT_ACTIVITY_PRIZE_RANDOM, stringRedisTemplate));
if (seed > random) {
    //log.warn("随机比例被拦截 seed = {}, random = {}", seed, random);
    throw new RuntimeException("随机比例拦截 - " + ERROR_MSG);
}
  1. 放弃重试
    失败重试会影响系统性能,重试次数越多,对系统性能的影响越大。
    抽奖过程中,从抽奖信息验证到扣库存、中奖信息入库的整个过程中,任何一个环节异常或失败,我们都不会进行重试,全部当做未中奖处理

  2. 防止奖品超发
    一般我们会通过乐观锁,悲观锁,分布式锁来解决。其中乐观锁的效率是最高的。
    下面sql不是标准的乐观锁,标准的乐观锁使用一个version字段来判断。不过下面的sql能很好的解决乐观锁容易失败的弊端

update mkt_activity_prize set num = num - 1 where num  >= 1
// 4. 真正数据库减库存,并且插入发奖记录
// 如果redis预减库存成功,这里大概率会成功,基本不会失败,如果失败,放弃重试,失败重试会影响系统性能,重试次数越多,对系统性能的影响越大。
Boolean execute = transactionTemplate.execute(status -> {
    // 4.1 扣减库存
    Integer update = mktActivityPrizeDao.occupyActivityPrize(activityPrize.getActivityId(), activityPrize.getPrizeId());
    if (update == null || update <= 0) {
        //log.warn("mysql 扣减库存失败 update = {}", update);
        throw new RuntimeException("mysql库存扣减失败 - " + ERROR_MSG);
    }

    // 4.2 插入发奖记录
    MktActivityPrizeGrant grant = buildMktActivityPrizeGrant(phone, activityPrize);
    Integer insert = mktActivityPrizeGrantDao.insert(grant);
    if (insert == null || insert <= 0) {
        //log.warn("mysql 插入发奖记录失败 insert = {}", insert);
        throw new RuntimeException("mysql 插入发奖记录失败 - " + ERROR_MSG);
    }

    return true;
});

那么从以上几个步骤我们可以看出,在真正的数据库减少库存的时候,随机拦截 + redis减库存已经帮我们拦截了大部分流量了,也就只有少部分流量会进入到我们真正的减库存环节。如果减库存的流量还是特别的大,我们还可以调整随机比列,同时减库存可以放到mq中,直接异步化发放奖品,基本少整个流程不会与数据库进行交互,瓶颈点几乎可以说是没有。这种架构,支撑百万,千万qps一点问题都没有。

最后

本文根据真实的业务场景,详细的剖析了一场营销活动从技术的角度如何设计规划,做到真正的高并发,高可用,支撑业务稳定的运行。其中涉及到的技术点还是比较多的,很多细节没有一一列举,包括如何保证redis库存和mysql一致,如果业务在活动中想修改库存怎么办,怎么保证不重复领取等等问题。
强烈建议大家有空可以自己实现一版,其中的一些细节还是非常考验技术的,实现下来,一定会有不少的收获,谢谢大家。

From:https://www.cnblogs.com/wenbochang/p/18383581
本文地址: http://shuzixingkong.net/article/1502
0评论
提交 加载更多评论
其他文章 注解是如何实现的?
注解是否支持继承 不支持继承 不能使用关键字extends来继承某个@interface,但注解在编译后,编译器会自动继承java.lang.annotation.Annotation接口. 虽然反编译后发现注解继承了Annotation接口,但即使Java的接口可以实现多继承,但定义注解时依然无法
生产者消费者模式,以及基于BlockingQueue的快速实现
生产者消费者模式,以及基于BlockingQueue的快速实现什么是生产者消费者模式,简单来说就是有两个角色,一个角色主要负责生产数据,一个角色主要负责消费(使用)数据。那么生产者直接依赖消费者,然后直接调用是否可以?答案是可以的,但是有些场景无法及时解决,典型的就是生产者消费者的速度无法同步,导致
生产者消费者模式,以及基于BlockingQueue的快速实现 生产者消费者模式,以及基于BlockingQueue的快速实现 生产者消费者模式,以及基于BlockingQueue的快速实现
k8s网络原理之Calico
之前整理总结过有关flannel的相关原理以及详细的传输过程,一直都想总结一篇通俗易懂的有关calico的相关原理和传输过程的,因为平常事情较多,没想到一拖就是这么久,这回借着复习的机会,将calico的原理,传输过程,以及各组件的作用还有两种模式的对比进行了细致的讲解和分析对比,也是对自己之前学习
k8s网络原理之Calico k8s网络原理之Calico k8s网络原理之Calico
windows权限维持汇总
Windows 权限维持 一、文件层面 1)attrib 使用 Attrib +s +a +h +r 命令 s:设置系统属性(System) a:设置存档属性(Archive) h:设置隐藏属性(Hidden) r:设置只读属性(Read-only) attrib +s +a +h +r c:\te
windows权限维持汇总 windows权限维持汇总 windows权限维持汇总
Java元注解介绍
Java四种元注解相关介绍 概述 注解从Java1.5引入以来,不断地简化我们编写代码的流程,逐渐的也成为了我们必学的一项技术。我们学习了各种注解,学习了他们的用法,学习了他们的限制,是否想过他们的组成呢,下面我将我对元注解的理解分享给大家。 元注解是用来修饰注解的注解,在java.lang.ann
Java元注解介绍 Java元注解介绍 Java元注解介绍
全网最适合入门的面向对象编程教程:40 Python常用复合数据类型-枚举和enum模块的使用
在 Python 中,枚举(Enumeration, Enum)是一种复合数据类型,用于表示一组相关但不同的常量值。枚举类型允许我们使用人类可读的名称代替代码中的魔法数字或字符串,Python 提供了 enum 模块来实现枚举类型。
全网最适合入门的面向对象编程教程:40 Python常用复合数据类型-枚举和enum模块的使用 全网最适合入门的面向对象编程教程:40 Python常用复合数据类型-枚举和enum模块的使用 全网最适合入门的面向对象编程教程:40 Python常用复合数据类型-枚举和enum模块的使用
Kafka Topic 中明明有可拉取的消息,为什么 poll 不到
开心一刻 今天小学女同学给我发消息她:你现在是毕业了吗我:嗯,今年刚毕业她给我发了一张照片,怀里抱着一只大橘猫她:我的眯眯长这么大了,好看吗我:你把猫挪开点,它挡住了,我看不到她:你是 sb 吗,滚我解释道:你说的是猫呀可消息刚发出,就出现了红色感叹号,并提示:消息已发出,但被对方拒收了 kafka
Kafka Topic 中明明有可拉取的消息,为什么 poll 不到 Kafka Topic 中明明有可拉取的消息,为什么 poll 不到 Kafka Topic 中明明有可拉取的消息,为什么 poll 不到
《HelloGitHub》第 101 期
每月 28 号更新的开源月刊,这里有实战项目、入门教程、黑科技、开源书籍、大厂开源项目等,涵盖多种编程语言 Python、Java、Go、C/C++、Swift...让你在短时间内感受到开源的魅力,对编程产生兴趣!
《HelloGitHub》第 101 期 《HelloGitHub》第 101 期 《HelloGitHub》第 101 期