个人项目中技术落地的基础入门(2)

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 Tair(兼容Redis),内存型 2GB
简介: 个人项目中技术落地的基础入门

秒杀项目


 技术选型


秒杀用到的基础组件,主要有框架、KV 存储、关系型数据库、MQ。
框架主要有 Web 框架和 RPC 框架。
其中,Web 框架主要用于提供 HTTP 接口给浏览器访问,所以 Web 框架的选型在秒杀服务中非常重要。在这里,我推荐Gin,它的性能和易用性都不错,在 GitHub 上的 Star 达到了 44k。对比性能最好的 fasthttp,虽然 fasthttp 在请求延迟低于 10ms 时性能优势明显,但其底层使用的对象池容易让人踩坑,导致其易用性较差,所以没必要过于追求性能而忽略了稳定性。
至于 RPC 框架,我推荐选用 gRPC,因为它的扩展性和性能都非常不错。在秒杀系统中,Redis 中的数据主要是给秒杀接口服务使用,以便将配置从管理后台同步到 Redis 缓存中。
KV 存储方面,秒杀系统中主要是用 Redis 缓存活动配置,用 etcd 存储集群信息。
关系型数据库中,MySQL 技术成熟且稳定可靠,秒杀系统用它存储活动配置数据很合适。主要 原因还是秒杀活动信息和库存数据都缓存在 Redis 中,活动过程中秒杀服务不操作数据库, 使用 MySQL 完全能够满足需求。
MQ 有很多种,其中 Kafka 在业界认可度最高,技术也非常成熟,性能很不错,非常适合用在秒杀系统中。Kafka 支持自动创建队列,秒杀服务各个节点可以用它自动创建属于自己的队列。

 方案设计


背景

  1. 秒杀业务简单,每个秒杀活动的商品是事先定义好的,商品有明确的类型和数量,卖完即止;
  2. 秒杀活动定时上架,消费者可以在活动开始后,通过秒杀入口进行抢购秒杀活动;
  3. 秒杀活动由于商品物美价廉,开始售卖后,会被快速抢购一空;


现象

  1. 秒杀活动持续时间短,访问冲击量大,秒杀系统需要应对这种爆发性的访问模型;
  2. 业务的请求量远远大于售卖量,大部分是陪跑的请求,秒杀系统需要提前规划好处理策略;
  3. 前端访问量巨大,系统对后端数据的访问量也会短时间爆增,对数据存储资源进行良好设计;
  4. 活动期间会给整个业务系统带来超大负荷,需要制定各种策略,避免系统过载而宕机;
  5. 售卖活动商品价格低廉,存在套利空间,各种非法作弊手段层出,需要提前规划预防策略;


秒杀系统设计首先,要尽力将请求拦截在系统上游,层层设阻拦截,过滤掉无效或超量的请求。因为访问量远远大于商品数量,所有的请求打到后端服务的最后一步,其实并没有必要,反而会严重拖慢真正能成交的请求,降低用户体验。秒杀系统专为秒杀活动服务,售卖商品确定,因此可以在设计秒杀商品页面时,将商品信息提前设计为静态信息,将静态的商品信息以及常规的 CSS、JS、宣传图片等静态资源,一起独立存放到 CDN 节点,加速访问,且降低系统访问压力,在访问前端也可以制定种种限制策略,比如活动没开始时,抢购按钮置灰,避免抢先访问,用户抢购一次后,也将按钮置灰,让用户排队等待,避免反复刷新。
其次,要充分利用缓存,提升系统的性能和可用性。用户所有的请求进入秒杀系统前,通过负载均衡策略均匀分发到不同 Web 服务器,避免节点过载。在 Web 服务器中,首先检查用户的访问权限,识别并发刷订单的行为。如果发现售出数量已经达到秒杀数量,则直接返回结束,要将秒杀业务系统和其他业务系统进行功能分拆,尽量将秒杀系统及依赖服务独立分拆部署,避免影响其他核心业务系统。秒杀系统需要构建访问记录缓存,记录访问 IP、用户的访问行为,发现异常访问,提前进行阻断及返回。同时还需要构建用户缓存,并针对历史数据分析,提前缓存僵尸强刷专业户,方便在秒杀期间对其进行策略限制。这些访问记录、用户数据,通过缓存进行存储,可以加速访问,另外,对用户数据还进行缓存预热,避免活动期间大量穿透。

  • 如何解决超卖?


mysql乐观锁+redis预减库存+redis缓存卖完标记 :

  1. 第一是基于数据库乐观锁的方式保证数据并发扣减的强一致性;
  2. 第二是基于数据库的事务实现批量扣减部分失败时的数据回滚。


在扣减指定数量前应先做一次前置数量校验的读请求(参考读写分离 + 全缓存方案)。

纯数据库乐观锁+事务的方式性能比较差,但是如果不计成本和考虑场景的话也完全够用,因为任何没有机器配置的指标,都是耍流氓。如果我采用 Oracle 的数据库、100 多核的刀锋服务器、SSD 的硬盘,即使是纯数据库的扣减方案,也是可以达到单机上万的 TPS 的。


单线程Redis 的 lua 脚本实现批量扣减。
当用户调用扣减接口时,将扣减的 对应数量 + 脚本标示传递至 Redis 即可,所有的扣减判断逻辑均在 Redis 中的 lua 脚本中执行,lua 脚本执行完成之后返还是否成功给客户端。




Redis 中的 lua 脚本执行时,首先会使用 get 命令查询 uuid 进行查重。当防重通过后,会批量获取对应的剩余库存状态并进行判断,如果一个扣减的数量大于剩余数量,则返回错误并提示数量不足。
Redis 的单线程模型,确保不会出现当所有扣减数量在判断均满足后,在实际扣减时却数量不够。同时,单线程保证判断数量的步骤和后续扣减步骤之间,没有其他任何线程出现并发的执行。
当 Redis 扣减成功后,扣减接口会异步的将此次扣减内容保存至数据库。异步保存数据库的目的是防止出现极端情况—— Redis 宕机后数据未持久化到磁盘,此时我们可以使用数据库恢复或者校准数据。
最后,运营后台直连数据库,是运营和商家修改库存的入口。商家在运营后台进货物进行补充。同时,运营后台的实现需要将此数量同步的增加至 Redis,因为当前方案的所有实际扣减都在 Redis 中。

纯缓存方案虽不会导致超卖,但因缓存不具备事务特性,极端情况下会存在缓存里的数据无法回滚,导致出现少卖的情况。且架构中的异步写库,也可能发生失败,导致多扣的数据丢失。


可以借助顺序写的特性,将扣减任务同步插入任务表,发现异常时,将任务表作为undolog进行回滚。
可以解决由于网络不通、调用缓存扣减超时、在扣减到一半时缓存突然宕机(故障 failover)了。针对上述请求,都有相应的异常抛出,根据异常进行数据库回滚即可,最终任务库里的数据都是准的。
更进一步:由于任务库是无状态的,可以进行水平分库,提升整体性能。

  • 如何解决重复下单?


mysql唯一索引+分布式锁

  • 如何防刷?


IP限流 | 验证码 | 单用户 | 单设备 | IMEI | 源IP |均设置规则

  • 热key问题如何解决?

redis集群+本地缓存+限流+key加随机值分布在多个实例中 :

  1. 缓存集群可以单节点进行主从复制和垂直扩容;
  2. 利用应用内的前置缓存,但是需注意需要设置上限;
  3. 延迟不敏感,定时刷新,实时感知用主动刷新;
  4. 和缓存穿透一样,限制逃逸流量,单请求进行数据回源并刷新前置;
  5. 无论如何设计,最后都要写一个兜底逻辑,千万级流量说来就来;


  • 应对高并发的读请求


使用缓存策略将请求挡在上层中的缓存中使用CDN,能静态化的数据尽量做到静态化加入限流(比如对短时间之内来自某一个用户,某一个IP、某个设备的重复请求做丢弃处理)。

资源隔离限流会将对应的资源按照指定的类型进行隔离,比如线程池和信号量。

  1. 计数器限流,例如5秒内技术1000请求,超数后限流,未超数重新计数;
  2. 滑动窗口限流,解决计数器不够精确的问题,把一个窗口拆分多滚动窗口;
  3. 令牌桶限流,类似景区售票,售票的速度是固定的,拿到令牌才能去处理请求;
  4. 漏桶限流,生产者消费者模型,实现了恒定速度处理请求,能够绝对防止突发流量;


流量控制效果从好到差依次是:漏桶限流 > 令牌桶限流 > 滑动窗口限流 > 计数器限流;

其中,只有漏桶算法真正实现了恒定速度处理请求,能够绝对防止突发流量超过下游系统承载能力。
不过,漏桶限流也有个不足,就是需要分配内存资源缓存请求,这会增加内存的使用率。而令牌桶限流算法中的“桶”可以用一个整数表示,资源占用相对较小,这也让它成为最常用的限流算法。正是因为这些特点,漏桶限流和令牌桶限流经常在一些大流量系统中结合使用。

  • 应对高并发的写请求


  1. 削峰:恶意用户拦截对于单用户多次点击、单设备、IMEI、源IP均设置规则;
  2. 采用比较成熟的漏桶算法、令牌桶算法,也可以使用guava开箱即用的限流算法;可以集群限流,但单机限流更加简洁和稳定;
  3. 当前层直接过滤一定比例的请求,最大承载值前需要加上兜底逻辑;
  4. 对于已经无货的产品,本地缓存直接返回;
  5. 单独部署,减少对系统正常服务的影响,方便扩缩容;


对于一段时间内的秒杀活动,需要保证写成功,我们可以使用 消息队列。

  1. 削去秒杀场景下的峰值写流量——流量削峰
  2. 通过异步处理简化秒杀请求中的业务流程——异步处理
  3. 解耦,实现秒杀系统模块之间松耦合——解耦


削去秒杀场景下的峰值写流量

  1. 将秒杀请求暂存于消息队列,业务服务器响应用户“秒杀结果正在处理中......”,释放系统资源去处理其它用户的请求。
  2. 削峰填谷,削平短暂的流量高峰,消息堆积会造成请求延迟处理,但秒杀用户对于短暂延迟有一定容忍度。秒杀商品有 1000 件,处理一次购买请求的时间是 500ms,那么总共就需要 500s 的时间。这时你部署 10 个队列处理程序,那么秒杀请求的处理时间就是 50s,也就是说用户需要等待 50s 才可以看到秒杀的结果,这是可以接受的。这时会并发 10 个请求到达数据库,并不会对数据库造成很大的压力。


通过异步处理简化秒杀请求中的业务流程先处理主要的业务,异步处理次要的业务。

  1. 如主要流程是生成订单、扣减库存;
  2. 次要流程比如购买成功之后会给用户发优惠券,增加用户的积****分。
  3. 此时秒杀只要处理生成订单,扣减库存的耗时,发放优惠券、增加用户积分异步去处理了。


解耦实现秒杀系统模块之间松耦合将秒杀数据同步给数据团队,有两种思路:

  1. 使用 HTTP 或者 RPC 同步调用,即提供一个接口,实时将数据推送给数据服务。系统的耦合度高,如果其中一个服务有问题,可能会导致另一个服务不可用。
  2. 使用消息队列将数据全部发送给消息队列,然后数据服务订阅这个消息队列,接收数据进行处理。


  • 如何保证数据一致性


CacheAside旁路缓存读请求不命中查询数据库,查询完成写入缓存,写请求更新数据库后删除缓存数据。


// 延迟双删,用以保证最终一致性,防止小概率旧数据读请求在第一次删除后更新数据库public void write(String key,Object data){    redis.delKey(key);    db.updateData(data);    Thread.sleep(1000);    redis.delKey(key);}

为防缓存失效这一信息丢失,可用消息队列确保。

  1. 更新数据库数据;
  2. 数据库会将操作信息写入binlog日志当中;
  3. 另起一段非业务代码,程序订阅提取出所需要的数据以及key;
  4. 尝试删除缓存操作,若删除失败,将这些信息发送至消息队列;
  5. 重新从消息队列中获得该数据,重试操作;

订阅binlog程序在mysql中有现成的中间件叫canal,重试机制,主要采用的是消息队列的方式。
终极方案:请求串行化真正靠谱非秒杀的方案:将访问操作串行化

  1. 先删缓存,将更新数据库的写操作放进有序队列中
  2. 从缓存查不到的读操作也进入有序队列

需要解决的问题:

  1. 读请求积压,大量超时,导致数据库的压力:限流、熔断
  2. 如何避免大量请求积压:将队列水平拆分,提高并行度。


  • 可靠性如何保障


由一个或多个sentinel实例组成sentinel集群可以监视一个或多个主服务器和多个从服务器。哨兵模式适合读请求远多于写请求的业务场景,比如在秒杀系统中用来缓存活动信息。如果写请求较多,当集群 Slave 节点数量多了后,Master 节点同步数据的压力会非常大。


image.png

当主服务器进入下线状态时,sentinel可以将该主服务器下的某一从服务器升级为主服务器继续提供服务,从而保证redis的高可用性。

  • 秒杀系统瓶颈-日志


秒杀服务单节点需要处理的请求 QPS 可能达到 10 万以上。一个请求从进入秒杀服务到处理失败或者成功,至少会产生两条日志。也就是说,高峰期间,一个秒杀节点每秒产生的日志可能达到 30 万条以上。


一块性能比较好的固态硬盘,每秒写的IOPS 大概在 3 万左右。也就是说,一个秒杀节点的每秒日志条数是固态硬盘 IOPS 的 10 倍,磁盘都扛不住,更别说通过网络写入到监控系统中。

  1. 每秒日志量远高于磁盘 IOPS,直接写磁盘会影响服务性能和稳定性
  2. 大量日志导致服务频繁分配,频繁释放内存,影响服务性能。
  3. 服务异常退出丢失大量日志的问题


解决方案

  1. Tmpfs,即临时文件系统,它是一种基于内存的文件系统。我们可以将秒杀服务写日志的文件放在临时文件系统中。相比直接写磁盘,在临时文件系统中写日志的性能至少能提升 100 倍,每当日志文件达到 20MB 的时候,就将日志文件转移到磁盘上,并将临时文件系统中的日志文件清空;
  2. 可以参考内存池设计,将给logger分配缓冲区,每一次的新写可以复用Logger对象;
  3. 参考kafka的缓冲池设计,当缓冲区达到大小和间隔时长临界值时,调用Flush函数,减少丢失的风险;


  • 池化技术

image.png

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
3月前
|
存储 Android开发 开发者
探索安卓开发之旅:从新手到专家的必经之路
【9月更文挑战第3天】在这篇文章中,我们将踏上一场激动人心的旅程,深入探索安卓开发的广阔天地。无论你是初涉编程世界的新手,还是期望提升技能的开发者,这里都有你需要的知识与技巧。我们将从基础概念讲起,逐步引导你了解安卓应用的核心组件,并分享实用的开发建议。准备好了吗?让我们一起开启这段成长之旅吧!
|
3月前
|
存储 Java Swift
移动应用开发之旅:从新手到专家的演进之路
【9月更文挑战第26天】在这篇文章中,我们将通过一个开发者的视角,探索移动应用开发的旅程。从最初的好奇心驱使下的尝试,到不断学习和挑战自我,最终成为一名能够独立设计和实现复杂移动应用的专家。本文将不包含代码示例,而是聚焦于开发者成长过程中的思考、策略以及心态调整。
49 4
|
6月前
|
Web App开发 人工智能 Java
技术经验分享:affineCipherandafineHacker
技术经验分享:affineCipherandafineHacker
38 2
|
2月前
|
测试技术 Android开发 开发者
移动应用开发之旅:从概念到上线的全栈探索
【9月更文挑战第36天】在这个数字时代,移动应用已经成为我们生活中不可或缺的一部分。本文将带领读者踏上一场精彩的旅程,从最初的构想到最终的应用上线,深入探讨移动应用开发的各个环节。我们将一起揭开移动操作系统的神秘面纱,了解它们如何支撑起整个移动生态系统。通过具体的代码示例和实操建议,本文旨在为初学者提供一份实用的指南,同时给予有经验的开发者一些新的启示。让我们一起构建更智能、更互联的世界吧!
|
2月前
|
存储 前端开发 JavaScript
前端技术深度探索:从基础到现代框架的实践之旅
前端技术深度探索:从基础到现代框架的实践之旅
38 2
|
4月前
|
存储 开发工具 Android开发
移动应用开发之旅:从零到精通
【8月更文挑战第30天】本文将带领读者踏上移动应用开发的奇妙旅程,从最初的构想到最终的实现。我们将探索不同的移动操作系统,理解它们的特点,并学习如何为这些平台开发应用程序。无论你是初学者还是有经验的开发者,这篇文章都会为你提供宝贵的见解和实用的技巧。让我们开始吧!
41 3
|
5月前
|
存储 缓存 物联网
个人项目中技术落地的基础入门(3)
个人项目中技术落地的基础入门
|
5月前
|
缓存 NoSQL Java
个人项目中技术落地的基础入门(1)
个人项目中技术落地的基础入门
108 6
|
6月前
|
存储 C# 索引
技术经验分享:C#入门详解(8)
技术经验分享:C#入门详解(8)
30 0
|
6月前
|
存储 缓存 NoSQL
技术经验分享:braum的使用
技术经验分享:braum的使用
28 0