文章目录

  • 一、背景介绍
  • 二、鸣谢
  • 三、为什么会决定参与内卖?
  • 四、第一次内卖
    • 1. 前言
    • 2. 技术方案设计
    • 3、内卖过程中遇到的问题
    • 4、回顾总结
  • 四、第二次内卖
    • 1、前言
    • 2、技术方案设计
    • 3、回顾总结

一、背景介绍

公司内部福利社的同事牵头组织,将福利社的退货商品,低价售卖给公司内部员工,算是员工福利吧。
内卖举办过挺多次了,这里仅记录我参与的两次内卖。

二、鸣谢

特别感谢雪兔、小伊、白明、安迪,留白、云流,公生、阿力、路飞、莫一兮
感谢其他在内卖过程中给与各种各样支持的同事们

三、为什么会决定参与内卖?

套用经典名言:如今机会就在眼前,我不知道何时才能再有机会去参与一个真实的秒杀项目。
对于程序猿来说,秒杀是一个经典的高技术难度场景,绝佳的镀金项目。
在这里要感谢公司的ExtraMile计划,才能给与我这个机会,在公司内,去做一些非本职工作的项目,去做一些技术、业务上的挑战。纸上得来终觉浅,看再多的书籍、博客,如果没有在真实业务场景下去实现过,就只能叫纸上谈兵,吹牛都没人信的。

四、第一次内卖

1. 前言

第一次内卖,后端开发人员只有我一个。
因为仓库那边堆积压力很大,所以从我接到任务开始,开发时间只有一周多,还要尽量不影响工作,所以技术方案设计的时候,快速实现就很重要,一切不稳定因素都应当剔除。

2. 技术方案设计

  1. 缓存:不使用redis,采用单机内存做缓存
    我当时刚跑路到小红书,在内部基础设施服务的使用上,接连踩了各种各样的坑,在时间如此紧张的情况下,我对于接入公司内部redis实在是没有信心。不仅是redis,内部基础设施服务都是能不用就不用。

  2. 采用java的Semaphore来做限购。
    内卖限制每个人只能购买四件商品,那么就用Semaphore来做令牌发放,获取到令牌的请求才能进行购买,购买失败就返回令牌,购买成功则不返回。

  3. 限流:用guava的RateLimiter。
    因为是单机器,怕撑不住,所以得加一个限流才行。

  4. 内卖整体业务流程
    内卖原则上不支持退货,用户收到货物之后,根据实际到货情况和货物质量,扫描官方支付宝付款。
    (流程图链接)

  5. 秒杀流程
    每人最多购买四件商品,每件商品限购一件
    (流程图链接)

  6. 核心秒杀代码:

/*** 用户购买商品的数量限制*/
public static volatile Integer userBuyLimit = 0;
/*** QPS限流,限流100*/
public static final RateLimiter qpsLimter = RateLimiter.create(100);
/*** 空的vector实例*/
private static final Vector emptyVector = new Vector();
/*** 用户购买的商品*/
private static volatile Map<Integer, Vector<Integer>> userGoodsMap = new ConcurrentHashMap<>();
/*** 商品的存量数据*/
public static volatile Map<Integer, AtomicInteger> goodsRemainCountMap = new HashMap<>();
/*** 用户的购物令牌,需要获取到令牌之后,该用户才可以购物*/
private static volatile Map<Integer, Semaphore> userTokenMap = new HashMap<>();
/*** 商品的可购买令牌,需要获取到商品的令牌之后,才可以购买该商品*/
private static volatile Map<Integer, Semaphore> goodsTokenMap = new HashMap<>();
public void buy(User operator, Integer goodsId) {// 当前内卖秒杀是否开启if (!BatchStatusEnum.SEC_kILL.equals(Constant.CURRENT_BATCH_STATUS)) {throw new RuntimeException("内卖秒杀还未开始");}// 用户购买商品数量是否已到限额Vector vector = userGoodsMap.getOrDefault(operator.getId(), emptyVector);if (vector.size() >= userBuyLimit) {throw new RuntimeException(String.format("您已购买%s件商品,无法再购买", userBuyLimit));}// 用户是否已购买if (vector.contains(goodsId)) {throw new RuntimeException("每件商品限购一件,您已购买该商品,无法再购买");}// 商品是否还有存量if (goodsRemainCountMap.get(goodsId).get() < 1) {throw new RuntimeException("该商品已被抢购一空");}// QPS限流boolean pass = qpsLimter.tryAcquire();if (!pass) {throw new RuntimeException("竞争太激烈了,请重试");}// 获取令牌Semaphore userTokens = userTokenMap.get(operator.getId());try {userTokens.tryAcquire();Semaphore goodsTokens = goodsTokenMap.get(goodsId);try {goodsTokens.tryAcquire(10);} catch (Exception ex) {goodsTokens.release();throw ex;}} catch (Exception ex) {logger.warn("秒杀请购失败:" + ex.getMessage(), ex);userTokens.release();throw new RuntimeException("竞争太激烈了,请重试");}// 购买成功,记录相关信息userGoodsMap.get(operator.getId()).add(goodsId);goodsRemainCountMap.get(goodsId).decrementAndGet();
}

3、内卖过程中遇到的问题

  1. 内卖刚开始就崩掉了,原因是前端资源加载有瓶颈。
    从来没写过前端的我,从来没想过,前端加载竟然竟然会是个瓶颈,我一直以为只要我后端hold住就万事大吉了。然后能怎么办呢?大家就随缘进入购物页面了。

  2. 秒杀购物体验很差。
    商品列表没有展示剩余库存,也没有展示已购买订单,所以大家的购物体验就是:进入商品列表,然后点点点,买到没,不知道。

  3. 因为都使用机器内存做缓存,所以服务如果重启就会丢失数据。可是最后生成订单数据时,意外报错了。幸好排查之后发现是数据异常导致的,删除异常数据之后,就能正常生成订单了。如果是代码bug的话,那我给大家伙跪下求原谅了。

  4. 只关注了主要的秒杀流程,做了各种并发控制,但是用户注册、地址填写等没有做并发控制,导致一个人多个账户、一个账户多个收货地址等数据异常情况。

4、回顾总结

  1. 不可重复操作,一定要做好并发控制。
    不能认为在业务流程上不存在并发问题,就不需要做并发限制处理。

  2. 完善的测试与压测。
    问题无法完全避免,但是完善的测试与压测能帮助我们尽量去避免问题。
    测试与压测,需要尽可能的去模拟真实用户的使用场景,这样才能发现更多的问题,比如前端资源瓶颈。

  3. 迫不得已的情况下,选择机器内存做缓存,这个可以理解,但是没有做好缓存持久化,导致秒杀开始后,重启项目就会丢失数据,一切归零,这是整个方案的最大风险点。

  4. 没有做完善的数据监控,导致时候无法回顾整个秒杀过程中的各种性能指标,尤其是qps,幸好还有第二次内卖,不然装逼都没机会了。

四、第二次内卖

1、前言

这次时间充裕,后端还有三个人,人力是足够的。美中不足的是,直到秒杀开始前,也没找到前端小伙伴,导致只能在以前的后端接口基础上做修改,原本设想的所有设计前端的优化、新功能点都无法做。

2、技术方案设计

  1. 内卖整体业务流程(链接)
  2. 秒杀下单流程(链接)
    MySQL订单入库限流,主要是为了避免MySQL被压垮
    实际下单落库,只需要在关系表中增加一个用户id与商品id的关联关系即可,MySQL语句是极其简单的,考虑到用户量与商品都不是特别大,而且限流之后qps也不会特别高,MySQL的处理速度足以满足下单需求,所以这里直接同步方式落库,而不采用异步落库方式。
  3. 核心秒杀代码:
public void buy(User operator, Integer goodsId) {// 内卖是否开始checkBatchProcess();// 商品是否还有库存String goodsCountKey = String.format(GOODS_REMAIN_FORMAT, goodsId);Integer goodsRemain = cacheService.getIntOrDefault0(goodsCountKey);if (goodsRemain < 1) {throw new RuntimeException("该商品已无库存");}// 用户是否还有额度BatchConfig currentBatch = batchConfigService.getCurrentBatch();String userBuyCountKey = String.format(USER_BUY_COUNT, operator.getId(), currentBatch.getId());Integer userBuyCount = cacheService.getIntOrDefault0(userBuyCountKey);if (userBuyCount >= currentBatch.getBuyLimit()) {throw new RuntimeException("您已达到购买限额,无法再购买商品");}// 用户是否已购买该商品String boughtGoodsKey = String.format(USER_BOUGHT_GOODS, operator.getId());if (cacheService.isMember(boughtGoodsKey, goodsId)) {throw new RuntimeException("您已购买过该商品,每人每件商品限购一件");}// 获取商品锁String goodsLockKey = String.format(GOODS_COUNT_MODIFY_LOCK, goodsId);boolean success = false;//if (cacheService.setnx(goodsLockKey, System.currentTimeMillis())) {if (cacheService.set(goodsLockKey, String.valueOf(System.currentTimeMillis()), "NX", "PX", DEFAULT_GODOS_EXPIRE_TIME)) {try {goodsRemain = cacheService.getIntOrDefault0(goodsCountKey);if (goodsRemain < 1) {throw new RuntimeException("该商品已无库存");}// 获取用户锁String userLock = String.format(USER_BUY_LOCK, operator.getId(), currentBatch.getId());//if (cacheService.setnx(userLock, System.currentTimeMillis())) {if (cacheService.set(userLock, String.valueOf(System.currentTimeMillis()), "NX", "PX", DEFAULT_USER_EXPIRE_TIME)) {try {userBuyCount = cacheService.getIntOrDefault0(userBuyCountKey);if (userBuyCount >= currentBatch.getBuyLimit()) {throw new RuntimeException("您已达到购买限额,无法再购买商品");}// MySQL限流int mysqlQpsLimit = ConfigService.getAppConfig().getIntProperty("redersale.mysql.qps.limit", 500);try {if (cacheService.incrBy(MYSQL_QPS_LIMIT, 1) > mysqlQpsLimit) {throw new RuntimeException("购买失败,请重试");}// 成功购买商品cacheService.setByDefaultExpire(goodsCountKey, goodsRemain - 1);cacheService.setByDefaultExpire(userBuyCountKey, userBuyCount + 1);cacheService.addMemberToSet(boughtGoodsKey, goodsId);goodsOrderMapper.insertOrder("秒杀下单", currentBatch.getId(), operator.getId(),operator.getRedName(), goodsId);success = true;} finally {cacheService.incrBy(MYSQL_QPS_LIMIT, -1);}// 获取mysql的qps} finally {cacheService.delete(userLock);}}} finally {cacheService.delete(goodsLockKey);}}if (!success) {throw new RuntimeException("抢购失败,请重试");}
}

3、回顾总结

  1. 主要指标:

    • 峰值qps:3k
    • 卖出商品sku数量:2313
    • 生成订单数量:11238
    • 售卖总金额:609,653
  2. 相比第一次,不仅秒杀期间,整个app没有崩溃,而且秒杀使用体验也比第一次好了很多,算是比较成功了。
  3. 数据监控
    做任何活动,需要充分考虑到运维监控的需求,作为业务方,需要知道当前关键业务数据的趋势情况,作为技术方,需要知道当前关键技术指标情况(判断服务是否还能支撑的住)。在第二次内卖中,考虑到了数据监控的需求,这点不错,但是部分数据监控是人工每次sql查询的,可以改成程序自动查询并在相关业务群里报数会更好点。
  4. 异常处理预案
    这次内卖提前准备了预案,比如当用户锁未正常释放时,手动为用户释放等等,这也是比较可喜的进步了。
  5. 技术评审
    复杂、重要的业务,需要有技术评审环节,群策群力,独自一人制定方案,难免会有各种遗漏。
  6. 信息沟通
    要有一个统一的群,用来活动各方及时沟通各种信息。所有相关信息,要有文档记录,并且文档目录要放在沟通群的公告中,方便随时查找。

小红书-内卖秒杀项目总结相关推荐

  1. 亲测,小红书涨粉变现项目,前期涨粉,后期实现躺赚,月入3000+!

    今天给你们分享一个小红书涨粉变现项目,新号数据亲测,这种擦边作品容易爆,涨粉非常容易,就上传了三个作品,涨粉50+,一分钟一个作品,文末有免费的视频素材,直接拿去用,下面给你们分享一下操作流程和变现. ...

  2. 如何进行小红书推广?小红书平台适合推广什么产品?

    小红书作为一个生活方式社区,其最大独特性就在于,大部分互联网社区更多是依靠线上的虚拟身份,而小红书用户发布的内容都来自于真实生活,一个分享用户必须具备丰富的生活和消费经验,才能有内容在小红书分享,继而 ...

  3. 推广引流秘籍:利用小红书精准加客源

    引流从来都是属于开头,手上有再多好的项目,没有流量,再多再好都是白搭,万事开头难,引流更是难上加难. 引流开好头,相当于已经成功了80%. 现在粉丝的获客趋势,普通是:获客难,成本高,渠道少,流量小. ...

  4. 1.6 这些都是小红书里面的名词术语【玩赚小红书】

    给大家整理了小红书里面的名词术语大全,看看你能看懂多少个? 1.小红薯 小红书全体用户的爱称,不管是普通用户,还是博主都统称为小红薯! 红薯号就是咱们的账号id啦. 2.官方薯 小红书的官方吉祥物为红 ...

  5. 小红书怎么运营推广?小红书运营推广流程介绍

    一.做小红书的核心目的? 1.在小红书中,运营推广的直接目的就是获客,通过小红书这个公域转到品牌私域平台,比如微信,微博等,从公域获客到私域成交的一个目的. 2.在品牌用户认知低,想要对品牌进行宣传, ...

  6. 小红书2022上半年品牌营销数据报告

    随着618年中购物节正式落下帷幕,2022上半年小红书品牌营销工作也告一段落.千瓜数据独家推出<2022上半年千瓜品牌营销数据报告(小红书平台)>,通过对小红书行业数据大盘.品牌投放策略. ...

  7. 小红书是失控了,还是故意在收割...

    来源:Tech星球(ID:tech618)丨文:李晓蕾 数据猿官网 | www.datayuan.cn 今日头条丨一点资讯丨腾讯丨搜狐丨网易丨凤凰丨阿里UC大鱼丨新浪微博丨新浪看点丨百度百家丨博客中国 ...

  8. 4.1 小红书的涨粉逻辑是什么?【玩赚小红书】

    小红书给人的感觉普遍都是: 用户很有价值 . 只要做到1万粉丝,就会有品牌方来找你合作,就可以获得不菲的广告收入.然而对于小红书上的创作者,想要能够轻松涨粉并不是那么容易,更不要说导流或者变现了. 原 ...

  9. 【营销案例】小红书进入娱乐圈 密签众明星

    从年初冠名<偶像练习生>再到作为<创造101>最主要的赞助商出现,小红书吸引了大量练习生粉丝涌入.与此同时,范冰冰.张雨绮等明星也陆续来到小红书,在其它人还没有反应过来时,在这 ...

最新文章

  1. 丢失/root目录导致命令行-bash-4.1#,解决方法
  2. java 文本编辑器替换特殊字符_linux中批量替换文本中字符串--转载
  3. 第十四次ScrumMeeting博客
  4. 调用webservice或wcf时,提示:无法加载协定为的终结点配置部分,因为找到了该协定的多个终结点配置。请按名称指示首选的终结点配置部分。
  5. 客座编辑:刘克,男,博士,国家自然科学基金委员会信息科学部二处(计算机学科)处长。...
  6. Spring Boot @ SpringBootApplication,SpringApplication类
  7. 【OpenCV学习笔记】【函数学习】二(MFC+OpenCV2.4.7读取摄像头之CvvImage::CopyOf 的通道选择问题)
  8. kettle的mysql驱动应该放哪里_MySQL数据库之kettle 安装mysql 驱动
  9. 分享一位大佬开发的驱动级的虚拟键盘鼠标,支持DD键鼠接口
  10. js判断页面第一次加载或者是否执行了刷新操作
  11. 服务器135、137、138、139、445等端口解释和关闭方法
  12. Flex + BlazeDS 学习笔记 (一) --- BlazeDS的功能原理及配置实例
  13. python27.dll引起的appcrash_Python已经停止工作(APPCRASH)Python
  14. python 数据分析 电信_基于Python的电信客户流失分析和预测
  15. 沉浸式逆向某汽车app
  16. LiveData介绍
  17. dataguard 日志的应用
  18. 奥巴马演讲:我们需要的变革
  19. Difficulties vanish when faced boldly.
  20. 如何彻底删除mysql_如何彻底删除mysql - 注册表问题

热门文章

  1. ubuntu单网卡NAT配置局域网共享上网
  2. 计算机后面板音乐开关,如何开关Windows启动与关机时的音乐声
  3. 蓝桥杯 ADV-201 算法提高 我们的征途是星辰大海
  4. 使用花生壳,idea外网访问oracle数据库
  5. shiro登录验证原理
  6. 《白日梦想家》影评笔记
  7. 用栈实现计算后缀表达式(0-9数值运算示例)
  8. AndroidStudio查找快捷键
  9. UEditor图片居中问题
  10. 做普惠AI实干家,华为云“Cloud+X”助力杭州打造数字经济第一城