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

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云原生内存数据库 Tair,内存型 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
相关文章
|
8月前
|
运维 Java 应用服务中间件
九五从零开始的运维之路(其十九)
Tomcat 服务器是一个免费的开放源代码的Web 应用服务器,属于轻量级应用服务器,在中小型系统和并发访问用户不是很多的场合下被普遍使用,是开发和调试JSP 程序的首选。 Tomcat虚拟主机是通过linux或windows操作系统下进行独立运行的一个网站发布容器,他是一种在单一主机或主机群上,实现多网域服务的方法,可以运行多个网站或服务的技术。
67 0
|
20天前
|
前端开发 Android开发 开发者
探索移动应用开发之旅:从概念到上线
【7月更文挑战第31天】本文旨在引导读者走进移动应用开发的奇妙世界,从最初的构想到最终的应用上线。我们将探讨移动操作系统的基础知识,介绍跨平台开发工具,并深入理解如何通过代码示例实现一个基本的功能模块。文章不仅涉及技术细节,还关注用户体验和市场趋势,以帮助开发者构建既美观又实用的移动应用。
29 4
|
1月前
|
缓存 NoSQL Java
个人项目中技术落地的基础入门(1)
个人项目中技术落地的基础入门
|
1月前
|
存储 缓存 物联网
个人项目中技术落地的基础入门(3)
个人项目中技术落地的基础入门
|
2月前
|
数据采集 前端开发 数据可视化
程序员必知:基于氚云平台的应用开发学习(一)
程序员必知:基于氚云平台的应用开发学习(一)
94 0
|
8月前
|
Web App开发 存储 运维
九五从零开始的运维之路(其十一)
(一)power on开机 (二)POST开机自检,如果有问题会发出蜂鸣声,没有问题就加载bios程序 (三)bios是基本输入输出系统,通过BIOS加载引导程序 (四)boot启动顺序检查,启动第一启动顺序
35 1
|
8月前
|
存储 运维 Linux
九五从零开始的运维之路(其三十一)
计划任务是在指定的时间间隔内自动执行的任务。在Linux系统中,常用的计划任务工具是crond(cron daemon)。用户可以通过创建计划任务来定期执行指定的命令或脚本。
49 0
|
8月前
|
运维 安全 网络协议
九五从零开始的运维之路(其二十一)
本篇将简述的内容:Linux系统下的文件共享服务器
45 0
|
8月前
|
存储 运维 网络协议
九五从零开始的运维之路(其二十五)
iscsi全称:Internet Small Computer System Interface——互联网小型计算机接口 通过网络获取磁盘设备在本地进行存储使用。
53 0
|
8月前
|
运维 应用服务中间件 网络安全
九五从零开始的运维之路(其十八)
本篇将简述的内容:Linux系统下的Nginx基本部署
31 0