如何设计一个高性能秒杀系统?

系统架构

  1. 秒杀系统是一种用于处理短时间内大量用户抢购限量商品的高并发系统,常见于电商促销活动(如“双11”“618”等)。其核心目标是解决瞬时超高流量、避免系统崩溃、保证公平性和防止超卖。
  2. 从高维度出发,通过整体思考问题,秒杀系统无外乎解决两个核心问题,一是并发读,一是并发写,对应到架构设计,就是高性能、一致性和高可用的要求。

高性能

秒杀系统涉及高读和高写的支持,如何支撑高并发,如何抵抗高IOPS(每秒的输入输出操作次数)?核心优化理念其实是类似的:高读就尽量"少读"或"读少",高写就数据拆分。

动静分离

  1. 说得动静分离是一种常见的高性能优化方案,一是需要实时请求的数据要尽量少,以便减少没必要的请求,二是可以被缓存的数据请求路径要尽量短,以便提高单次请求的效率。

  2. 大部分秒杀系统在等待秒杀过程中不需要刷新整个页面的,只有时间在不停跳动。这是因为一般都会对大流量的秒杀系统做系统的静态化改造,即数据意义上的动静分离。

    1. 静态数据:秒杀商品的基础信息(如名称、描述、图片等),在秒杀活动期间几乎不变,可缓存或预加载。
    2. 动态数据:库存数量、用户抢购记录等高频变更数据,需实时强一致性处理。
  3. 分离出静态数据后,该怎么缓存呢?有一个很直接的方法,那就是直接使用URL作为key,缓存整个HTTP响应。如此一来,Web代理服务器根据请求 URL,可以直接取出对应的响应体然后直接返回,响应过程无需解析 HTTP 请求头,性能较高。这个方法需要对URL作唯一化处理,但对于秒杀系统来说,URL天然就可以作为商品的唯一ID。

  4. 那这些静态数据该缓存在哪里呢?有三种方式:

    1. 浏览器:速度快,可以大幅减少服务器负载。但用户的浏览器是不可控的,主要体现在如果用户不主动刷新,系统很难主动地把消息推送给用户,如此可能会导致用户端在很长一段时间内看到的信息都是错误的。这里需要理清楚,静态数据仅仅是一段时间相对不变,而不是永远不变的数据。对于秒杀系统,保证缓存可以在秒级时间内失效是不可或缺的。
    2. 服务端:服务端主要还是负责逻辑处理与计算,对高并发缓存的优化有限。另外,静态数据下沉至服务端也会拉长数据的请求路径,导致用户体验不佳。
    3. CDN:CDN本身更擅长处理大并发的静态文件请求,既可以做到主动失效,又离用户尽可能近,是相对来说最好的选择。
  5. CDN缓存静态数据需要注意的问题:

    1. 失效问题:任何一个缓存都应该是有时效的,尤其对于一个秒杀场景。所以,系统需要保证全国各地的 CDN 在秒级时间内失效掉缓存信息,这实际对 CDN 的失效系统要求是很高的。
    2. 命中率问题:用户请求会被调度到最近的CDN节点,导致相同数据的请求分散到不同节点(如上海用户访问上海节点,北京用户访问北京节点)。如果每个节点的缓存独立,那么同一数据的重复缓存会增多,整体命中率下降。又或者,如果单个CDN节点的请求量不足,缓存可能因低频访问被提前淘汰,导致频繁回源查询,增加服务器负载。
  6. 因此,通常来说选择作为缓存的CDN节点需要满足靠近访问量集中的地区、并且用户与CDN的网络质量要比较良好。基于以上因素,选择 CDN 的二级缓存比较合适。相比遍布全国的边缘节点(一级缓存),二级缓存节点数量更少,但单个节点的存储容量和计算能力更强,适合集中缓存热点数据。

  7. 数据整合:分离出动静态数据之后,前端如何组织数据页就是一个新的问题,主要在于动态数据的加载处理,通常有两种方案:ESI(Edge Side Includes)方案和 CSI(Client Side Include)方案。

    1. ESI 方案:在CDN节点或Web反向代理服务器上请求动态数据,并将动态数据插入到静态页面中,用户看到页面时已经是一个完整的页面。这种方式对服务端性能要求高,但用户体验较好。
    2. CSI 方案:CDN节点或Web反向代理服务器上只返回静态页面,前端单独发起一个异步 JS 请求动态数据。这种方式对服务端性能友好,但用户用户可能先看到不完整的页面,直到动态内容被加载出来,对用户体验稍差。

热点优化

  1. 热点优化主要是针对数据的访问量进行了数据拆分,以便进行针对性地处理。

  2. 热点操作:零点刷新、零点下单、零点添加购物车等都属于热点操作。热点操作是用户的行为,不好改变,但可以做一些限制保护,比如用户频繁刷新页面时进行提示阻断。

  3. 热点识别:对于热点数据来说,第一步肯定是识别出热点数据,其分为静态热点和动态热点:

    1. 静态热点:能够提前预测的数据。比如促销前夕,可以根据行业特点、活动商家等纬度信息分析出热点商品,或者通过卖家报名的方式提前筛选;另外,还可以通过一些技术手段,例如对买家每天访问的商品进行大数据计算,然后统计出 TOP N 的商品,即可视为热点商品。
    2. 动态热点:无法提前预测的数据。冷热数据往往是随实际业务场景发生交替变化的,尤其是如今直播卖货模式的兴起——带货商临时做一个广告,就有可能导致一件商品在短时间内被大量购买,这类商品非常容易导致缓存击穿。
  4. 秒杀系统需要对热点具有动态发现的能力,一个常见的实现思路是:

    1. 采集交易产生时各个环节的热点Key信息,如 Nginx可以采集访问URL,提前识别潜在的热点数据。
    2. 聚合分析热点数据,达到一定规则的热点数据,通过订阅分发推送到各个中间件,然后它们根据自身需求决定如何处理热点数据。
    3. 热点数据采集最好采用异步方式,一方面不会影响业务核心,一方面可以保证采集方式的通用性。
    4. 热点发现最好做到秒级实时,这样动态发现才有意义,实际上也是对核心节点的数据采集和分析能力提出了较高的要求。
  5. 热点隔离:热点数据识别出来之后,第一原则就是将热点数据隔离出来,不要让 1% 影响到另外的 99%,可以基于以下几个层次实现热点隔离:

    1. 业务隔离:秒杀作为一种营销活动,卖家需要单独报名,从技术上来说,系统可以提前对已知热点做缓存预热
    2. 系统隔离:系统隔离是运行时隔离,通过分组部署和另外 99% 进行分离,另外秒杀也可以申请单独的域名,入口层就让请求落到不同的集群中
    3. 数据隔离:秒杀数据作为热点数据,可以启用单独的缓存集群或者DB服务组,从而更好的实现横向或纵向能力扩展。
  6. 热点优化:热点数据隔离之后,也就方便对这 1% 的请求做针对性的优化,方式无外乎两种

    1. 缓存:热点缓存是最为有效的办法。如果热点数据做了动静分离,那么可以长期缓存静态数据
    2. 限流:流量限制更多是一种保护机制,开发者需要时刻关注什么时候触发了限流,然后后续进行优化。

系统优化

  1. 系统优化针对的是整个秒杀系统,我们需要定期检查应用基线来进行优化。比如性能基线(何时性能突然下降)、成本基线(去年大促用了多少机器)、链路基线(核心流程发生了哪些变化),通过基线持续关注系统性能,提高代码质量、去除不合理调用、在架构层面不断优化改进。
  2. 减少序列化:减少序列化操作可以很好的提升系统性能。序列化大部分是在 RPC 阶段发生,因此应该尽量减少 RPC 调用,一种可行的方案是将多个关联性较强的应用进行 “合并部署”,从而减少不同应用之间的 RPC 调用。
  3. 直接输出流数据:只要涉及字符串的I/O操作,无论是磁盘 I/O 还是网络 I/O,都比较耗费 CPU 资源,因为字符需要转换成字节,而这个转换又必须查表编码。所以对于常用数据,比如静态字符串,推荐提前编码成字节并缓存。
  4. 裁剪日志异常堆栈:无论是外部系统异常还是应用本身异常,都会有堆栈打出,超大流量下,频繁的输出完整堆栈,只会加剧系统当前负载。可以通过日志配置文件控制异常堆栈输出的深度。
  5. 去组件框架:极致优化要求下,可以去掉一些组件框架,比如去掉传统的 MVC 框架,让客户端直接与服务对接,绕过一大堆复杂且用处不大的处理逻辑,节省时间。

一致性

秒杀系统的一致性问题核心在于:在高并发抢购场景下,如何确保商品库存的准确扣减,既不超卖(卖多了)也不少卖(卖少了)

减库存

  1. 电商场景下的购买过程一般分为两步:下单和付款。“提交订单”即为下单,“支付订单”即为付款。

  2. 下单减库存:买家下单后,扣减商品库存。

    1. 优点:用户体验最好。下单减库存是最简单的减库存方式,也是控制最精确的一种。下单时可以直接通过数据库事务机制控制商品库存,所以一定不会出现已下单却不能付款的情况
    2. 缺点:当卖家参加某个促销活动时,竞争对手通过恶意下单的方式将该商品全部下单,然后又不付款,导致库存清零,那么这就不能正常售卖。
  3. 付款减库存:买家下单后,并不立即扣减库存,而是等到付款后才真正扣减库存。

    1. 优点:“下单减库存” 可能导致恶意下单,从而影响卖家的商品销售, “付款减库存” 由于需要付出真金白银,可以有效避免。
    2. 缺点:用户体验较差,可能导致很多买家下单成功后却不能付款。用户下单后,不一定会实际付款,假设有 100 件商品,就可能出现 200 人下单成功的情况,因为下单时不会减库存,所以也就可能出现下单成功数远远超过真正库存数的情况,这尤其会发生在大促的热门商品上
  4. 预扣库存:这种方式相对复杂一些,买家下单后,库存为其保留一定的时间(如 15 分钟),超过这段时间,库存自动释放,释放后其他买家可以购买

    1. 优点:缓解了以上两种方式的问题。预扣库存实际就是“下单减库存”和 “付款减库存”两种方式的结合,将两次操作进行了前后关联,下单时预扣库存,付款时释放库存
    2. 缺点:没有彻底解决以上问题。比如针对恶意下单的场景,虽然可以把有效付款时间设置为 10 分钟,但恶意买家完全可以在 10 分钟之后再次下单。
  5. 业界最为常见的是预扣库存,一般采用以下几个方案解决少卖和超卖的问题:

    1. 少卖:可以结合安全和反作弊措施来制止。比如,识别频繁下单不付款的买家并进行打标,这样可以在打标买家下单时不减库存;再比如为大促商品设置单人最大购买件数,一人最多只能买 N 件商品;又或者对重复下单不付款的行为进行次数限制阻断等。
    2. 超卖:解决超卖需要保证数据库的库存字段不能为负:一个方法是通过事务来判断,即保证减后库存不能为负,否则就回滚;还有就是直接设置数据库字段类型为无符号整数,这样一旦库存为负就会在执行 SQL 时报错。
  6. 另外有些场景下,超卖可能不是个很严重的问题,对于普通商品,秒杀只是一种大促手段,即使库存超卖,商家也可以通过补货来解决。

性能优化

  1. 库存是一个动态数据,也是一个热点数据,需要支持高并发读与高并发写的操作,这些都是秒杀系统中最为核心的问题。

  2. 高并发读:

    1. 读数据时可以事先进行不影响性能的分层校验,来提前过滤一些无效的读请求。如用户是否具有秒杀资格、商品状态是否正常、用户答题是否正确、秒杀是否已经结束、是否非法请求等,而不做一致性校验等容易引发瓶颈的检查操作。
    2. 因此,在分层校验的基础上,可以对库存进行缓存,允许用户读取脏数据,比如显示"有库存"但最终可能扣减失败,等到真正写数据时再保证最终一致性。
  3. 高并发写的一个解决思路——更改数据库选型:

    1. 如果减库存逻辑非常单一的话,可以将库存直接放到缓存中,也就是直接在一个带有持久化功能的缓存中进行减库存操作,比如 Redis,提高读写性能。
    2. 但如果有比较复杂的减库存逻辑,比如有总库存和具体库存(像手机就有具体的颜色、规格等类型,而减库存时需要同时减去具体规格手机的库存和总库存),或者需要使用到事务,那就必须在数据库中完成减库存操作。
  4. 高并发写的另一个解决思路——优化数据库性能:

    1. 某个库存在数据库中实际就是一行记录,如果此时大量请求来到数据库,比如MySQL,就会产生多个线程争用行锁的现象。并发越高,等待线程就会越多,吞吐量会受到严重影响。
    2. 应用层排队:通过分布式锁来控制集群对数据库同一行记录进行操作的并发度,同时也能控制单个商品占用数据库连接的数量,防止热点商品占用过多的数据库连接。
    3. 数据层排队:可以对数据库系统本身进行优化,比如消息队列,让并发的请求变成单一的排队队列(与锁竞争有所区别,排队比锁竞争性能更高)。或者也可以实现事务不需要等待实时提交,而是在数据执行完最后一条 SQL 后,直接根据 TARGET_AFFECT_ROW 的结果进行提交或回滚。

高可用

对于秒杀系统来说,秒杀请求高度集中于某一特定的时间点。这样一来就会造成一个特别高的零点峰值,而对资源的消耗也几乎是瞬时的。所以秒杀系统的可用性保护是不可或缺的。

流量削峰

  1. 对于秒杀的目标场景,最终能够抢到商品的人数是固定的,无论 100 人和 10000 人参加结果都是一样的,即有效请求额度是有限的。并发度越高,无效请求也就越多。但秒杀作为一种商业营销手段,活动开始之前是希望有更多的人来刷页面,只是真正开始后,秒杀请求不是越多越好。因此系统可以设计一些规则,人为的延缓秒杀请求,甚至可以过滤掉一些无效请求。

  2. 答题:答题是现在比较普遍的延缓和过滤方式。

    1. 防止秒杀器作弊,如果答题时间小于1~2s,人为的可能性会很小,可以以此判断为秒杀器。
    2. 人为拉长峰值下单的时长,比如之前是1秒内有大量请求,现在则变成了10秒内有大量请求,这些请求被平摊到了10秒,大大减轻高峰期并发压力。另外,由于请求具有先后顺序,后面的请求到来时可能已经没有库存了,因此根本无法下单,此阶段落到数据层真正的写也就非常有限了。
  3. 排队:消息队列也是一种场常见的延缓方式,它可以将同步的直接调用转换成异步的间接推送,减少瞬时流量。但是消息队列也有一些缺点,一个是请求容易积压,如果超过负载,请求仍然会被丢弃。二是用户体验肯定比同步调用要差,可能出现请求先发后至的情况。

  4. 过滤:过滤的核心结构在于分层,通过在不同层次过滤掉无效请求,达到数据读写的精准触发。

    1. 读限流:对读请求做限流保护,将超出系统承载能力的请求过滤掉。
    2. 读缓存:对读请求做数据缓存,将重复的请求过滤掉。
    3. 写限流:对写请求做限流保护,将超出系统承载能力的请求过滤掉。
    4. 写校验:对写请求做一致性校验,只保留最终的有效数据
  5. 分流:在业务层也有很多手段进行流量削峰,比如在零点同时发起抽奖和发放优惠券活动,将流量引导到其他系统上。

备选方案

  1. 当一个系统面临持续的高峰流量时,其实是很难单靠自身调整来恢复状态的,日常运维没有人能够预估所有情况,意外总是无法避免。尤其在秒杀这一场景下,为了保证系统的高可用,必须设计一个备选方案来进行兜底
  2. 预防:建立常态压测体系,定期对服务进行单点压测以及全链路压测。
  3. 管控:做好线上运行的降级、限流和熔断保护。需要注意的是,无论是限流、降级还是熔断,对业务都是有损的,所以在进行操作前,一定要和上下游业务确认好再进行。就拿限流来说,哪些业务可以限、什么情况下限、限流时间多长、什么情况下进行恢复,都要和业务方反复确认
  4. 监控:建立性能基线,记录性能的变化趋势;建立报警体系,发现问题及时预警
  5. 恢复:遇到故障能够及时止损,并提供快速的数据订正工具,不一定要好,但一定要有。

其他问题

预扣的库存超时后如何进行回补?

  1. 定时任务扫描超时订单,比如每隔1分钟扫描数据库中的超时订单,批量回补库存。优点简单直接,缺点是延迟高,数据库压力过大。
  2. 下单时同时设置redis过期键,通过redis监听过期键状态,触发回补。优点是精准,数据库压力较小。缺点是需要保证redis和数据库的连接稳定性和一致性。
  3. 下单时发送延迟消息到消息队列中,消费者收到消息后检查订单状态,未支付则回补。优点是支持大规模并发,缺点是消息队列本身需要支持延迟消息,比如Kafka。

第三方支付如何保证减库存和付款时的状态一致性?

  1. 预扣库存+冻结库存+支付回调:最简单广泛的方式,适用于大部分电商系统和秒杀系统。
    1. 下单时先预扣库存,并增加冻结库存,此时商品并未真正卖出,只是暂时锁定,避免超卖。
    2. 用户完成支付后,支付宝/微信会回调系统,系统再扣减冻结库存,并修改订单状态为已支付。
    3. 如果支付回调重复(比如网络问题),系统会检查订单状态(只有未支付才执行扣库存操作),避免重复扣减。即实现了幂等性,即使同一操作(回调)执行多次,结果也跟只执行一次一样。
    4. 如果支付失败或超时,系统自动把冻结的库存释放回可用库存,不影响其他用户购买导致少卖。
  2. 消息队列异步处理:在高并发处理上会更好,支付和库存服务仅通过MQ通信,避免直接依赖,并且MQ还可以缓冲客户流量,即使短暂不一致(如支付回调延迟),MQ也会确保最终库存正确,适用于高性能秒杀系统。
    1. 用户下单后,系统发一条消息到MQ:“订单A待支付,冻结商品B库存1”。库存服务监听MQ,先冻结库存。
    2. 支付成功时,再发一条消息:“订单A已支付,扣减商品B库存1”。库存服务收到后,正式扣减。
    3. 如果超时未支付,系统自动发消息释放冻结库存。
  3. 对账系统:即使上述方案做了防护,仍可能有极端情况(如服务器宕机、网络彻底中断),所以需要对账系统兜底。对账系统每天负责定时对比支付系统的交易记录和库存系统的扣减记录

如何设计一个高性能的分布式日志系统?

轻量级日志收集客户端

  1. 多语言SDK:提供Go/Java/Python/C++等主流语言的SDK,核心部分用C++实现以获得最佳性能

  2. 内存管理

    1. 采用内存池技术预分配内存,减少内存分配开销
    2. 使用双缓冲机制,前台缓冲接收新日志,后台缓冲异步发送。前台缓冲如果达到阈值(80%)则交换两个缓冲区的指针(确保原子性)。若后台发送未完成而前台已满,启用临时第三缓冲接收新日志,避免数据丢失,并记录警告与降级(例如丢弃DEBUG日志,优先保证ERROR日志),等待后台IO恢复后,再释放第三缓冲。
    3. 使用动态环形缓冲区,支持按日志级别划分不同区域,默认16MB,可根据内存压力自动扩容和缩小。
  3. 网络传输

    1. 同时支持多个协议传输日志,包括UDP、TCP、HTTP/JSON、gRPC/proto、Unix Domain Socket(UDS)。
    2. 根据场景使用不同协议:如果是本机日志记录,则使用UDS发送日志,其使用了零拷贝技术,具有极高吞吐量。如果是网络传输日志,则使用gRPC,吞吐量与延迟都要比传统HTTP要好。HTTP仅用来调试接口,TCP/UDP用于兼容不同系统。
  4. 智能采样

    1. 在高负载和高流量的情况下,系统不可能记录所有日志,这样会很容易导致系统到达瓶颈,因此需要动态调整日志的采样率,来使系统正常运行。
    2. 按错误级别采样:错误日志100%,警告日志50%,普通日志10%,Debug日志1%。
    3. 按服务等级采样:核心服务100%,基础服务50%,辅助服务10%。
  5. 故障恢复

    1. 定期检查点:每5分钟持久化缓冲区状态到磁盘
    2. 网络恢复后:优先发送未确认的日志
    3. 磁盘溢出保护:当磁盘使用>90%时,丢弃DEBUG/INFO级别日志

高性能消息队列缓冲层

  1. 多节点高配置kafka集群,确保单节点可处理每秒数十万条日志,并防止单点故障,即使2-3台宕机也不影响服务。
  2. 数据分区:将数据分散到不同分区,多个生产者可并行写入,多个消费者可并行读取。
  3. 数据备份:每个分区有多个副本,即使服务器宕机,数据也不丢失,防止单点故障。
  4. 特殊处理:对于关键日志,需要更多的副本和更大的最小副本同步数才算写入成功,确保可靠性,但会导致写入速度变慢,因为需要更多的副本确认才可以。

日志实时处理与分析层

  1. 日志解析:使用正则表达式匹配和JSON解析。
  2. 字段提取:自动识别IP、URL、时间戳等信息,并对敏感数据进行脱敏处理,如手机号和身份证。
  3. 规则匹配:支持自定义业务规则匹配,并进行热加载,无需重启。
  4. 冷热分离:3天内的热数据会被发送到NVMe存储,7天内的温数据会被发送到SSD存储,30天内的冷数据会被发送到HDD存储。

日志高可用存储层

  1. 存储分层:

    1. 对于热数据,我们需要使用高成本的NVMe存储,确保访问延迟足够低,一般用于高频查询。
    2. 对于温数据,我们需要使用较为平衡的SSD存储,以平衡存储成本和访问性能,主要用于批量分析。
    3. 对于冷数据,我们需要使用较为低价的HDD存储,高延迟大容量,这类数据主要用于归档备份。
  2. 存储格式:

    1. 原始日志:基于行的二进制格式,支持多种压缩算法,追加写入效率高,适合流式日志收集,读取效率低,不支持列裁剪,主要用于冷数据。
    2. 列存储:兼容多种计算引擎,压缩比较低,写入速度一般,但读取速度快,支持列裁剪,主要用于温数据的批量分析。
    3. 索引数据:一种特殊的列存储,具有最高的压缩效率,写入速度最慢,查询速度最快,支持索引和列裁剪,适用于热数据的高频查找和范围查找。

日志查询与可视化层

该层主要面向开发者,需要进行一些查询功能的优化,比如自动补全和查询缓存等,尽可能对开发者友好。

监控指标体系

在日志系统使用的过程中,我们需要时刻为系统进行性能监控与分析,然后来动态调整系统,其指标一般如下:

  1. 收集层:日志生成与发送成功的延迟
  2. 队列层:日志堆积的数据量大小
  3. 处理层:处理日志需要的延迟
  4. 存储层:磁盘空间使用情况
  5. 查询层:查询

如何在海量数据中筛选出数据?

如果是找出最大/最小的几个数,则考虑用堆解决,比如从中数十亿的数据中筛选出最大的K个数据:

  1. 先建立一个小顶堆,并从原数据集中放入K个数。
  2. 逐个添加原数据集中的剩余元素,如果当前元素小于堆顶则跳过,大于堆顶则加入堆,并弹出堆顶元素。
  3. 遍历完成后,堆中剩余的K个数就是最大的K个数。
  4. 在内存受限的情况下,可以使用将原数据集划分为n个子数据集,然后用不同线程去建堆筛选,最后由一个线程将结果合并。
  5. 如果原数据集的重复元素过多,可以使用hash法或位示图法去重后再进行筛选。

如何对海量数据进行排序?

  1. 当数据量远大于可用内存时,采用外部排序算法,将数据分割成子数据集装入内存中,使用高效的排序算法排序,然后将结果写入临时文件,最后再归并所有临时文件,完成排序。
  2. 当数据量可以完全写入内存时,就需要考虑排序算法的使用,通常使用快速排序即可。如果需要确保稳定性,则使用归并排序。如果需要节省空间,则使用堆排序进行原地排序。如果数据的范围。

如何对海量数据进行统计?

在实际业务场景中,我们常常需要对海量数据进行元信息统计,比如显示用户某个月的签到次数和首次签到实际,或是统计7天内连续签到的用户总数。对于这类只有两种状态(有签到和没签到)的海量数据,我们可以使用位图来进行统计:

  1. 判断用户是否在线:创建一个表示用户登陆状态的位图,用户ID作为下标,在线值就设置为1,下线值就设置为0。
  2. 判断用户的签到情况:针对单个用户创建一个表示签到情况的位图,这样一年的签到也仅需365个bit位。而且Redis也提供了可以快速检索位图中第一个不为0的命令,这样就可以快速获取用户的首次打卡时间。
  3. 判断用户的连续签到情况:创建表示用户是否打卡的位图,用户ID作为下标,如果打卡则设置为1,没有打卡则设置为0,并且该位图要创建多张,每张表示不同的打卡日期。假设要统计7天连续打卡情况,则将7个位图进行与操作,然后在其中检索哪些位为1就可以得知连续签到情况了。

如何制作一个排行榜?

  1. 如果只是简单的排行榜,可以使用redis中的zset(有序集合)去实现,将得分设置为score,用户ID设置为value,利用zset的排序功能,可以按照分数从高到低排序。
  2. 如果分数相同,按照默认的排序规则会按照value值排序,但目前希望按照时间顺序排序,就是分数相同的情况下先上榜的排在前面。一个可行的方案是将分数score设置为一个浮点数,其中整数部分为得分,小数部分为时间戳,即 score= 分数 + 1 - 时间戳/1e13。此时在分数相同的情况下,时间早的时间戳小,除以一个很大的数后得到的数字就小,被1减去以后得到的差就会大,也就实现了分数相同的情况下先上榜的排在前面。
  3. 大体上排行榜由两部分组成,排序和信息汇总,排序可以让用户看到当前排行靠前的人有谁,而信息汇总则是点击某个用户的头像可以看到具体的用户信息。对于并发量不大的情况,可以直接对接数据库,如果是并发量大的情况,则考虑使用缓存、消息队列等机制。

如何设计一个预约系统?

预约系统的核心目标是允许用户预约特定的时间段,并确保这些时间段不会与其他预约发生冲突。换句话说,系统需要管理一组时间资源,并确保在任何给定的时间段内,同一资源不会被多次预约。

  1. 资源定义:明确哪些资源可以被预约,例如,会议室、医生、服务人员等,然后创建一个数据库表描述这些资源。
  2. 时间粒度:确定预约的最小时间单位(如15分钟、30分钟、1小时等),然后创建一个数据库表描述具体的预约,比如用户预约了哪个资源(对应资源表中的主键),预约开始时间和结束时间,以及预约状态(是否预约)。
  3. 冲突检测:在创建或更新预约时,需要检查该资源在请求的时间段内是否已有其他预约。
  4. 用户界面:用户可以查看可用时间并进行预约。
  5. 并发控制:对于如何处理多个用户同时尝试预约同一时间段的情况,可以使用悲观锁和乐观锁去解决。对于高并发来说,同样也是可以使用缓存和消息队列去解决。
  6. 高级功能:对于一些高级功能,比如通知系统,则可以使用合适的微服务去解决。

如何去计算一篇文章的热度?

  1. 影响因素:对于文章热度来说,有很多需要考虑的因素,如浏览量、评论数、转发量、点赞数、收藏数、阅读时长。
  2. 权重分配:不同平台对热度的定义可能不同。例如,新闻平台可能更看重转发和评论,而博客平台可能更看重阅读时长和收藏。
  3. 归一化处理:不同因素的数值范围可能差异很大。例如,浏览量可能是数千,而评论数可能是几十。为了公平比较,需要进行归一化。
  4. 时间衰减:热度通常会随时间下降。可以引入时间衰减因子。
  5. 动态调整:不同时期用户可能更倾向于评论而非转发(如节日话题易引发讨论)、平台可能阶段性鼓励评论(如推出"优质评论奖励"活动)、以及发现刷量行为时自动降低对应指标的权重。这些原因导致了文章热度的权重分配需要进行动态调整,这种调整通常基于时间或基于业务等。

如何设计一个评论系统?

  1. 评论对于文章来说是非常常见的一种系统,通常需要支持快速查看某篇文章的评论数、支持楼中楼回复、查看某个评论有多少条回复,不一次性加载所有评论等特性。
  2. 为了满足这些要求,可以设计三个表:文章),评论索引表和评论内容表。
    1. 文章表包含文章ID、标题和文章的根评论数
    2. 评论索引表包含评论ID、所属文章ID、父评论ID(顶级评论为0)、用户ID、回复数量和点赞数量。
    3. 评论内容表存储评论ID(关联评论索引表的评论ID的外键)和评论内容。
  3. 这种设计方式通过将评论内容和评论索引分开存储,提高了查询性能,同时也便于数据维护,并且通过文章表记录文章的根评论数,方便快速统计。并且可以设计触发器用于在更改评论时的多表级联更新。
  4. 添加评论:
    1. 前端发送评论到服务端。
    2. 服务端收到评论消息之后,将之发给MQ去处理,然后直接返回评论成功的结果。
    3. 消费者从MQ上消费消息,向数据添加评论。
    4. 添加完成后,将添加成功的消息通过websocket推送给客户端。也就是说实际添加评论到数据库这一动作是异步的,用于应对高并发。
  5. 查询评论:
    1. 添加缓存
    2. 主从复制和读写分离(评论系统倾向于读多写少)

参考资料

高性能秒杀系统的设计思考,超详细!