一、缓存辅助模式:根据需要将数据从数据存储加载到缓存中,以提高读取性能和响应速度。

1、查缓存,不存在则查库,并更新缓存:应用程序首先尝试从缓存中获取所需数据,如果缓存中不存在,则从数据库中获取并更新缓存。这种方式可以提高读取性能和命中率。

2、直接维护一块全量数据,与数据库同步:应用程序在内存中直接维护一份全量数据的副本,并尽量与数据库保持同步。这种方式适用于数据变动较小的情况,并且可以实现几乎100%的命中率。如果数据量较小且可以容纳在进程内,还能避免跨进程通信的开销。

在性能优化方面,可以更细致地考虑以下问题:

– 在内存中维护复杂数据结构的优化:由于内存中的数据结构是直接维护的,而不是通过查询数据库获得的,因此搜索效率可以比数据库快几百倍。这对于数据量不大但关系复杂的数据非常有益。

– 数据更新策略的优化:在应用程序启动时,将数据完全加载到内存中,并根据一些策略进行数据更新。更新策略可以包括定时更新和异步更新等方式,不同的数据可以设定不同的更新频率。

– 数据过期和更新的优化:可以为不同的数据设置不同的过期时间,在数据过期后,可以通过请求触发主动更新或使用回调方式被动更新。这样可以确保数据的及时更新,同时避免不必要的数据处理开销。

– 数据修改时的同步更新:在进行数据修改后,需要及时同步更新缓存和数据库中的数据,以保持数据的一致性。这种同步机制需要谨慎设计,确保数据的正确性和可靠性。

二、命令和查询责任分离模式:通过使用不同的接口来分离读取数据和更新数据的操作,从而提高系统的可维护性和灵活性。

CQRS(Command Query Responsibility Segregation)是一种架构模式,其核心思想是将读取和写入操作分离为两个不同的数据模型。这种方式可以让读取和写入具有完全不同的数据结构,减少彼此之间的干扰,并降低权限控制的复杂度。同时,CQRS也可以在程序内部实现两套命令模型,分别处理读取和写入操作,进行针对性的优化和定制。总之,CQRS模式通过将读取和写入操作分离,提供了一种灵活和可优化的架构方式,使系统在性能、可维护性和权限控制等方面都得到了改善。

现在一般的做法是类似于上图的做法,为读写配置两套独立的数据源,并且和事件溯源的方式结合起来做。我们来说说读写两套模型在存储上分离这个事情,在《相辅相成的存储五件套》一文中我们的架构图其实就有这方面的意思。对于读写这两个事情,我们完全可以不用一套数据源,为读建立专门的物化视图,可以针对读进行优化,避免在读的时候做很多Join的工作,可以把性能做到极致(后面会有物化视图模式的介绍)。事件溯源+CQRS+物化视图三者一般会结合起来使用。

三、事件溯源模式:使用只追加的存储方式记录对领域中数据所做操作的完整系列事件,以便可以重现和审计系统状态的变化。

事件溯源(Event Sourcing)是一种软件开发模式,它通过记录和存储事件序列来代替传统的数据状态存储方式。在传统的CRUD(Create, Read, Update, Delete)模式中,我们会直接对数据进行更新操作,但这样做可能会导致并发性和数据丢失等问题。而通过事件溯源模式,我们将重点关注数据的变动事件,而不是当前状态,具有以下特点:

1. 事件不可变性:事件一旦被创建就不能更改,只能追加新的事件。这确保了数据的完整性和准确性。

2. 事件追踪和审计:通过记录完整的事件序列,我们可以追踪和审计数据的变化历史。这对于故障排查、审计要求以及法规合规性非常重要。

3. 高性能和可伸缩性:事件溯源模式的数据写入操作只是简单地将新事件追加到事件流中,这通常比传统的数据更新操作更高效。此外,由于事件可以并行处理,它具有较好的可伸缩性。

4. 事件驱动的开发:事件溯源模式使得系统可以基于事件进行外部处理,通过订阅和处理事件来达到不同的业务目的。这使得系统具有较低的耦合度和更好的灵活性。

5. 保留原始信息:事件溯源记录了每个事件的完整信息,没有信息损耗。这使得我们可以随时访问历史数据,并支持回滚操作。

事件溯源模式在某些业务场景下比传统的数据状态存储更适用,特别是当业务更加关注数据的意图、审计、回滚和历史功能,并且希望避免数据更新冲突、具有较高的性能以及接受数据的最终一致性时。此外,在事件驱动的系统中使用事件溯源模式更加自然,因为它反映了现实世界中物体之间通过事件相互影响的方式。

四、物化视图模式:根据需要生成预填充的视图,以优化某些查询操作,当数据在一个或多个数据存储中的格式不适合时。

用数据存储的时候往往会更多考虑存储而不是读取。我们使用各种数据库范式来设计数据库,在读取数据的时候我们需要做大量的关联查询以输出符合需要的查询结果。这个时候性能往往会成为瓶颈,物化视图是一种空间换时间的做法。与其在查询的时候做关联,倒不如提前保存一份面向于查询和输出的数据格式。因此,物化视图适合下面的场景:

经过复杂的计算才能查询出数据

背后存储可能会有不稳定的情况

需要连接多个不同类型的存储才能查询到结果

但是因为需要考虑到物化视图计算保存的开销,所以也不太适合于数据变化太频繁的情况,因为数据加工需要时间,所以不适合需要数据强一致性的场景。实现上一般是基于消息监听做额外维护一套物化视图的数据源和主流程解耦。惠普的Vertica是一款高性能的列式分析数据库,它的一个特性就是物化视图,通过事先提供SQL语句直接缓存面向于统计的查询结果,极大程度提高了性能,也是空间换时间的思想。

五、基于队列的负载均衡模式:使用队列作为任务和服务之间的缓冲区,以平滑处理间歇性的重负载,确保系统的稳定性和可扩展性。

引入消息队列并不会直接提高处理能力,但它可以降低系统的耦合性,使得每个组件都能独立具有弹性。对于无法立即承受负荷的部分,可以利用消息队列进行缓冲。然而,需要注意的是,消息队列并不意味着无限制的存储能力,它也有一定的容量限制。

在使用消息队列时,需要考虑处理速度和入队速度之间的比例关系。一般来说,我们需要事先评估系统的需求,并确保处理能力(即每秒处理的事务数 TPS)至少是最高峰值入队能力(即每秒入队的事务数 TPS)的两倍以上。这样做可以确保系统保持一定的余量,即使业务逻辑有所修改,处理能力下降了30%,系统仍能够承受一定的压力。

换句话说,消息队列可以作为一个缓冲层,通过调节处理和入队速度的比例来确保系统的稳定性和弹性。但在设计和配置消息队列时,需要根据实际情况进行评估和调整,以满足系统的需求。

六、优先级队列模式:确定发送到服务的请求的优先级,使具有较高优先级的请求可以更快地被接收和处理,确保高优先级任务的及时完成。

优先级队列与FIFO队列不同,它允许消息具有不同的处理优先级。在实际实现中,有两种方式可以实现优先级队列:消息优先级方式和不同处理池方式。

在消息优先级方式中,队列会实时根据消息的优先级进行位置重排,始终将优先级较高的消息优先处理。而在不同处理池方式中,我们可以针对不同优先级的消息配置专门的处理池,以提供更多的处理资源和优质的硬件设备。这样做可以确保高优先级消息具有更高的处理能力。在选择方案和实施时,需要考虑是否需要绝对按照优先级处理消息,或者只是相对优先处理即可。如果需要绝对优先,除了消息位置重排外,还需要实现抢占处理的机制。

另外,如果采用第二种多池方式来处理消息,有可能发生低优先级消息的处理时间比高优先级消息更快的情况(如果两者的业务逻辑完全不同)。在实现中,RabbitMQ 3.5以上版本支持了消息优先级,使用的是第一种方式,在消息堆积缓冲时进行消息重排,消费端可以优先处理优先级高的消息。但在消费速度大于生产速度的情况下,无法实现高优先级消息的优先处理。

此外,对于队列中的消息,还有一种情况需要特别考虑,即长时间停留在队列中的消息应被视为低优先级或死信消息来处理。最好是有专门的消费者来处理这类消息,以避免影响整个队列的处理。我们也应注意到,在实践中会遇到由于被废弃消息阻塞而导致完全失去处理能力的事故。

七、限流模式:控制应用程序、个人租户或整个服务实例所消耗的资源量,以防止过度负载和资源的浪费,确保系统的平稳运行和公平分配资源。

在进行压力测试时,我们会观察到系统吞吐量随着压力增加而增加,同时响应时间保持在可控范围内(1秒以内)。然而,当压力突破一定边界后,响应时间会突然变得不可控,系统吞吐量下降,并最终导致系统崩溃。每个系统都有其负载的边界,超过这个边界,系统将无法满足服务级别协议(SLA),从而导致用户无 ** 常使用该服务。

由于系统扩展通常不是短时间内能够实现的,所以最快的手段是限流,通过限制流量来保护当前系统,防止其突破边界并彻底崩溃。在处理大量业务的系统中,对于关键服务甚至入口级别进行限流是必要的,没有其他选择。例如,在淘宝双11的凌晨0点,我们也会发现一定比例的下单请求被限流。

常见的限流算法包括以下几种:

1. 计数器算法:最简单的算法,对资源使用进行计数,达到一定计数后拒绝服务。

2. 令牌桶算法:以固定速率往一个桶中放入令牌,桶中最多存放n个令牌,当桶满时丢弃多余的令牌。在处理请求时需要获取令牌,无法获取则拒绝请求。

3. 漏桶算法:一个固定容量的漏洞,按照一定的速率流出水滴(任务),可以以任意速度流入水滴(任务),漏桶满了则溢出丢弃。

令牌桶算法限制平均流入速率,并允许一定的突发请求;而漏桶算法限制常量的流出速率,用于平滑流入的速度。在实现上,常用的开源类库中都有相关的实现,例如Google的Guava库提供了RateLimiter,它就是基于令牌桶算法实现的。

在进行限流时,需要快速执行,任何超过流量控制的请求都不能被放行,否则就失去了意义。同时,限流应该提前执行,在系统能力达到80%时最好开始限流,这样可以减小风险。可以向客户端返回特定的限流控制错误代码,让用户知道这不是错误,而是限流,可以稍后再尝试。此外,在监控图上,我们应该敏感地观察限流曲线,限流后的曲线会突然失去增长的梯度,变得平稳。如果监控图的时间范围太短,可能会误判这是正常请求量。

限流可以在边缘节点上进行。以秒杀场景为例,如果每秒有100万个请求,将这100万个请求全部发送到应用服务器毫无意义。我们可以在边缘节点(CDN)甚至客户端上进行简单的边缘计算,让这100万个请求按命中注定的方式随机放弃其中99.9%,只留下1000个请求进入我们的业务服务。这样,1000个每秒的TPS一般来说是可处理的。因此,在参与秒杀等活动时,系统会在极短的时间内告知您活动已结束,说明您已被选中,无法进入后端系统参与秒杀。

关注公众号:领取架构师面试资料