{"id":20041256,"url":"https://github.com/wenbochang888/marketing","last_synced_at":"2025-05-05T08:32:15.132Z","repository":{"id":254812244,"uuid":"846424061","full_name":"wenbochang888/marketing","owner":"wenbochang888","description":"大厂员工，手把手教你开发一个高并发、高可用的营销活动","archived":false,"fork":false,"pushed_at":"2024-10-10T14:42:16.000Z","size":617,"stargazers_count":9,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-08T19:47:45.448Z","etag":null,"topics":["cache","java","marketing"],"latest_commit_sha":null,"homepage":"https://www.gdufe888.top/marketing/rule/check","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/wenbochang888.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-08-23T07:14:48.000Z","updated_at":"2025-02-11T13:04:38.000Z","dependencies_parsed_at":null,"dependency_job_id":"fb3273d7-aa9b-4bbf-8e33-e72d467b057c","html_url":"https://github.com/wenbochang888/marketing","commit_stats":null,"previous_names":["wenbochang888/marketing"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wenbochang888%2Fmarketing","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wenbochang888%2Fmarketing/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wenbochang888%2Fmarketing/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wenbochang888%2Fmarketing/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/wenbochang888","download_url":"https://codeload.github.com/wenbochang888/marketing/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252466784,"owners_count":21752432,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cache","java","marketing"],"created_at":"2024-11-13T10:45:59.918Z","updated_at":"2025-05-05T08:32:14.567Z","avatar_url":"https://github.com/wenbochang888.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 如何使用\n1. 首先创建数据库表 [marketing.sql](src%2Fmain%2Fresources%2Fmarketing.sql)\n2. git clone下项目\n3. 直接使用idea打开\n4. 补充mysql用户名密码，redis用户名密码 [application.properties](src%2Fmain%2Fresources%2Fapplication.properties)\n5. 请求MarketingActivityController 校验抽奖资格 + 进行抽奖\n\n# [点我 - 体验地址](https://www.gdufe888.top/marketing/rule/check)\n此前端页面完全是用GPT生成的，感谢GPT的贡献，远远提高了我的生产力\n\n\n大家没有必要刷我接口哈，对你对我都没有什么好处，此项目仅仅用于学习\n![示例图片](src/main/resources/img/6.png)\n\n\n ![示例图片](src/main/resources/img/1.png)\n ![示例图片](src/main/resources/img/4.png)\n ![示例图片](src/main/resources/img/2.png)\n ![示例图片](src/main/resources/img/3.png)\n\n# 前言\n这几年工作中做过不少营销活动，无论是电商业务、支付业务、还是信贷业务，营销在整个业务发展过程中都是必不可少的。如果前期营销宣传到位，会给业务带来一波不小的流量。那么作为技术，如何接住这波流量，而不是服务被打挂。今天大厂员工，手把手教你开发出一个高并发、高可用的营销活动。\n\n\n# 业务\n任何脱离业务的技术都是无用功，所以我们先简单介绍一下业务。\n\n业务希望我们的用户在比如购买商品，下单支付等场景，转化率尽可能的高。那么为了奖励和刺激用户，**我们希望通过一些优惠券的方式**，来激励符合我们规则的用户，进行下单，进行支付，进行借钱，进行购物等等后续操作。\n\n比如某个用户符合我们活动的规则，第一步我们会给他展示优惠信息，激励他来进行下一步、完成这个任务，然后在给他发奖、核销等后续动作\n![示例图片](src/main/resources/img/5.png)\n\n\n# 校验抽奖资格\n那么根据以上我们业务的分析，我们第一步就是，用户进来，我们查询活动，并且校验用户是否有资格参加活动。如果有多个活动，我们根据业务规则选择一个活动让用户进行参与。\n\n那么这也是我们营销活动的起点，第一步。如果成千上百万的用户一下子涌进来，我们去查询数据库活动信息，并且校验规则，我们的数据库瞬间就会崩掉。所以我们的核心思路是：**逐级分流，逐步分散流量**。通过**备份、限流、降级、熔断**等手段提升可用性。\n\n首先就是加缓存，对于一些静态页面，css，js等文件，可以放在客户端缓存或者CDN里面。对于活动信息以及规则，在活动上线之前，将这些信息缓存到redis里面。用户进来时，我们直接取redis里面查询活动信息，并且计算活动规则，全程不需要和数据库进行交互。最后，评估活动qps，进行降级限流，如果流量过大，直接进行拦截，防止系统雪崩。\n\n```java\npublic MktActivityInfo checkActivityRule(String phone) {\n    // 从redis缓存中取\n    MktActivityInfo activityInfo = activityCacheService.getActivityInfo();\n    if (activityInfo == null || StringUtils.isEmpty(activityInfo.getActivityId())) {\n        return null;\n    }\n\n    ActivityRuleContext context = new ActivityRuleContext();\n    context.setPhone(phone);\n    // redis缓存中取\n    List\u003cMktActivityRule\u003e mktActivityRules = activityCacheService.listActivityRule(activityInfo.getActivityId());\n    for (MktActivityRule mktActivityRule : mktActivityRules) {\n        BaseRuleService baseRuleService = BaseRuleFactory.getBaseRuleService(mktActivityRule.getRuleKey());\n        if (baseRuleService == null || !baseRuleService.check(context)) {\n            return null;\n        }\n    }\n\n    return activityInfo;\n}\n```\n\n# 抽奖\n一般到达抽奖，基本都是完成了前面的任务，比如支付，下单等等，最终获得抽奖资格\n\n1. 减库存。将奖品的库存信息提前缓存到redis里面，比如奖品100个缓存到redis里面。如果有100W人来抢100个奖品，最终也只有100个人通过redis的校验\n```java\nLong num = RedisUtils.decr(CACHE_MKT_ACTIVITY_PRIZE_NUM, stringRedisTemplate);\nif (num == null || num \u003c 0) {\n\n    // 将redis库存加回，可做可不做，看业务需求\n    RedisUtils.incr(CACHE_MKT_ACTIVITY_PRIZE_NUM, stringRedisTemplate);\n    throw new RuntimeException(\"redis库存不足 - \" + ERROR_MSG);\n}\n```\n\n2. 根据业务场景，如果不是必中奖。在减库存之前，做一个随机数。如果在随机数之外，直接返回”奖品被抢完“，限制大部分流量进入到redis减库存\n```java\nint seed = ThreadLocalRandom.current().nextInt(0, 100) + 1; // 1-100\nint random = NumberUtils.toInt(RedisUtils.get(CACHE_MKT_ACTIVITY_PRIZE_RANDOM, stringRedisTemplate));\nif (seed \u003e random) {\n    //log.warn(\"随机比例被拦截 seed = {}, random = {}\", seed, random);\n    throw new RuntimeException(\"随机比例拦截 - \" + ERROR_MSG);\n}\n```\n\n3. 放弃重试\n   失败重试会影响系统性能，重试次数越多，对系统性能的影响越大。\n   抽奖过程中，从抽奖信息验证到扣库存、中奖信息入库的整个过程中，任何一个环节异常或失败，我们都不会进行重试，全部当做未中奖处理\n\n4. 防止奖品超发\n   一般我们会通过乐观锁，悲观锁，分布式锁来解决。其中乐观锁的效率是最高的。\n   下面sql不是标准的乐观锁，标准的乐观锁使用一个version字段来判断。不过下面的sql能很好的解决乐观锁容易失败的弊端\n\n```sql\nupdate mkt_activity_prize set num = num - 1 where num  \u003e= 1\n```\n```java\n// 4. 真正数据库减库存，并且插入发奖记录\n// 如果redis预减库存成功，这里大概率会成功，基本不会失败，如果失败，放弃重试，失败重试会影响系统性能，重试次数越多，对系统性能的影响越大。\nBoolean execute = transactionTemplate.execute(status -\u003e {\n    // 4.1 扣减库存\n    Integer update = mktActivityPrizeDao.occupyActivityPrize(activityPrize.getActivityId(), activityPrize.getPrizeId());\n    if (update == null || update \u003c= 0) {\n        //log.warn(\"mysql 扣减库存失败 update = {}\", update);\n        throw new RuntimeException(\"mysql库存扣减失败 - \" + ERROR_MSG);\n    }\n\n    // 4.2 插入发奖记录\n    MktActivityPrizeGrant grant = buildMktActivityPrizeGrant(phone, activityPrize);\n    Integer insert = mktActivityPrizeGrantDao.insert(grant);\n    if (insert == null || insert \u003c= 0) {\n        //log.warn(\"mysql 插入发奖记录失败 insert = {}\", insert);\n        throw new RuntimeException(\"mysql 插入发奖记录失败 - \" + ERROR_MSG);\n    }\n\n    return true;\n});\n```\n\n那么从以上几个步骤我们可以看出，在真正的数据库减少库存的时候，随机拦截 + redis减库存已经帮我们拦截了大部分流量了，也就只有少部分流量会进入到我们真正的减库存环节。如果减库存的流量还是特别的大，我们还可以调整随机比列，同时减库存可以放到mq中，直接异步化发放奖品，基本少整个流程不会与数据库进行交互，瓶颈点几乎可以说是没有。这种架构，支撑百万，千万qps一点问题都没有。\n\n\n# 最后\n本文根据真实的业务场景，详细的剖析了一场营销活动从技术的角度如何设计规划，做到真正的高并发，高可用，支撑业务稳定的运行。其中涉及到的技术点还是比较多的，很多细节没有一一列举，包括如何保证redis库存和mysql一致，如果业务在活动中想修改库存怎么办，怎么保证不重复领取等等问题。\n强烈建议大家有空可以自己实现一版，其中的一些细节还是非常考验技术的，实现下来，一定会有不少的收获，谢谢大家。\n\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwenbochang888%2Fmarketing","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwenbochang888%2Fmarketing","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwenbochang888%2Fmarketing/lists"}