都说“程序等于数据结构加算法”,在软件的运行时刻,其实是数据加进程,进程离数据越“近”,越能得到高速的读写性能。
Cpu的计算速度通常不是软件执行的瓶颈,io的速度会更大地影响软件执行的速度,从“近”到“远”,一般有以下几个场景:
- 访问内存
- 访问磁盘
- 访问网络
软件设计的性能提升,都是想办法使进程更靠近数据,减少数据在慢速介质中的传输。
传统软件的client/server结构,就是基于这个思路,让计算在服务端完成,只把计算结果传输到客户端,这样可以减少数据的传输的耗时。反过来,当然也可以把数据传输到客户端,由客户端计算,来节省服务器的cpu消耗,但一般情况下,很少有只需要传送少量数据而需要大量计算的场景,而采用后一种方案。
传统的数据库软件,都会提供存储过程和触发器这样的编程方法,使计算更“靠近”数据,也是提高性能的良方。现代的海量数据系统,比如hadoop,数据分布在许多节点上,在执行计算时,也是把代码发布到数据节点上,在每个节点分别计算,最后汇总结果,这个过程称为mapreduce,执行计算时,都是访问节点本地的数据,汇总时才使用到网络传输。
回到我们的应用软件,因为访问内存比访问磁盘快,所以我们使用redis来做缓存,redis是共享内存的存储,在实际使用中,通常和应用服务器不在同一台物理机器上,也就是说,应用系统访问redis,还是要经过网络传输。
- 所以应用程序访问数据库的延时是:网络延时+磁盘延时
- 应用程序访问redis缓存的延时是:网络延时+内存延时
所以,即使是访问缓存,也要求在完成一个请求(当然,今天我们讨论的是web应用)时,访问缓存的次数尽量地少。根据实际的情况,如果是一个需要返回结果给调用方结果的同步请求,需要访问缓存6、7次,会大大降低性能,这时,说明你的进程离数据不够“近”。
这时我们有两个选择,要么给redis开发插件,使redis支持“存储过程”,使计算在redis的进程内完成。要么自己实现缓存功能,也就是在你的应用进程内,实现redis的功能。在大多数情况下,后者更容易实现,通常只需要在内存中构造一个dictionary对象,支持按key访问value就可以了。这样,我们其实构造了一个local cache,我们构造local cashe的目的非常明确,当应用的计算,需要访问多次缓存数据时(这时使用缓存的性能优势已经不明显),我们在应用中构造自己的缓存,使得访问外部缓存系统变成访问本地内存,来提高性能。
在实践中,构造本地缓存,需要考虑到以下几个方面
- 使用集群,每个集群内的缓存数据,都是相同的,外部请求不管落到哪个服务器,都是访问本机的内存来获得数据
- 数据同步,集群内的每个服务器,数据同步是独立完成的,数据源的变化会通知到每一台服务器。在某个时间点,不同的服务器内数据允许有不同,但最终数据一致
- 读写分离的api实现,读操作,可以多线程并发执行。写操作,由专门的线程执行,也就是使用actor模型,由某个actor执行所有的数据修改操作
- 冷启动,因为需要加载某些表的所有记录到内存中,通常会需要几十秒到数分钟,需要有 拉出集群 – 加载数据 – 拉进集群 的发布流程
- 数据过期策略,如果有数据过期的需求,需要设计一个后台线程,定期检查所有的缓存元素,把过期的数据清理掉