现在后端开发会用到各种中间件,一不留神项目可能在哪天就要用到一个我们之前可能听过但是从来没接触过的中间件,这个时候对于开发人员来说,如果你不知道这个中间件的设计逻辑和使用方法,那在后面的开发和维护工作中可能就会比较吃力,所以我想根据我的经验,把后端开发中常用的中间件最简单的一层帮大家总结一下,包括它是干什么的,优势是什么,如何使用这三大部分,让大家对一些常用的中间件有一些基本的了解,不至于在工作中的项目中用到了这个中间件但却对它没有一点了解。
什么是redis
(来自维基百科)Redis(Remote Dictionary Server)是一个使用ANSI C编写的支持网络、基于内存、分布式、可选持久性的键值对存储数据库。根据月度排行网站DB-Engines.com的数据,Redis是最流行的键值对存储数据库。
Redis为什么叫Redis
Redis的全称是REmote DIctionary Service,即远程字典服务
Redis默认端口为什么是6379
6379其实对应手机9宫格键盘上的4个按键,来源是MERZ这个词:
- Redis内部原理
Redis一般用来干什么
Redis最常见的用途就是用作缓存,当你的系统需要缓存的组件时,Redis基本上就是不二的选择。除了缓存以外,Redis有时还被用来实现分布式锁,比如OpenStack的tooz就是用redis实现的分布式锁。另外在分布式系统中,Redis可以用来共享会话信息。
Redis的三种模式
Redis的三种模式主要包括主从模式(Master-Slave Replication)、哨兵模式(Sentinel)和集群模式(Cluster)。
主从模式
主从模式是一种数据备份和读写分离的模式。在这种模式下,有一个主节点(Master)负责处理所有的写操作,一个或多个从节点(Slave)负责处理读操作。从节点通过复制主节点的数据来保持与主节点的数据一致性。
- 优点:
- 实现数据备份,提高数据安全性。
- 读写分离,提高系统读取性能。
- 缺点:
- 无法自动切换主从节点,主节点故障时需要手动切换。
- 写操作仍然在主节点上进行,可能成为性能瓶颈。
哨兵模式
哨兵模式是在主从模式的基础上,增加了故障转移和监控的功能。哨兵节点(Sentinel)负责监控主从节点的状态,并在主节点故障时自动将从节点提升为主节点。
- 优点:
- 实现自动故障转移,提高系统可用性。
- 监控主从节点的状态,及时发现和解决问题。
- 缺点:
- 哨兵节点需要额外的资源和维护成本。
- 主从切换时可能会有短暂的服务中断。
集群模式
集群模式是一种分布式解决方案,它将数据分散存储在多个节点上,每个节点负责一部分数据的读写。集群模式通过哈希槽技术实现数据的分片和管理。
- 优点:
- 实现数据的水平扩展,提高系统存储容量和性能。
- 自动进行故障转移和负载均衡。
- 支持动态添加或删除节点,易于扩展和维护。
- 缺点:
- 配置和维护相对复杂。
- 不支持跨节点的事务和某些复杂操作。
Redis持久化方式
Redis的持久机制有两种,第一种是快照,第二种是AOF日志。
快照是一次全量备份,AOF日志是连续的增量备份。快照是内存数据的二进制序列化形式,在存储上非常紧凑,而AOF日志记录的是内存数据修改的指令记录文本。AOF日志在长期的运行过程中会变得无比庞大,数据库重启时需要加载AOF日志进行指令重放,这个时间就会无比漫长,所以需要定期进行AOF重写,给AOF日志进行瘦身。
快照
Redis使用操作系统的多进程COW(Copy on Write)机制来实现快照持久化。
Redis在持久化时会调用glibc的函数fork产生一个子进程,快照持久化完全交给子进程来处理,父进程继续处理客户端请求。子进程刚刚产生时,它和父进程共享内存里面的代码段和数据段。这时你可以把父子进程想象成一个连体婴儿,它们在共享身体。这是Linux操作系统的机制,为了节约内存资源,所以尽可能让他们共享起来。在进程分离的一瞬间,内存的增长几乎没有明显变化。
子进程做数据持久化,不会修改现有的内存数据结构,它只是对数据结构进行遍历读取,然后序列化写到磁盘中。但是父进程不一样,它必须持续服务客户端请求,然后对内存数据结构进行不间断的修改。
这个时候就会使用操作系统的COW机制来尽心数据段页面的分离。数据段是由很多操作系统的页面(Page)组合而成,当父进程对其中一个页面的数据进行修改时,会将被共享的页面复制一份分离出来,然后对这个复制的页面进行修改。这是子进程相应的页面是没有变化的,还是进程产生时那一瞬间的数据。
随着父进程修改操作的持续进行,越来越多的共享页面被分离出来,内存就会持续增长,但是也不会超过原有数据内存的2倍大小。另外,Redis实例里冷数据占的比例往往是比较高的,所以很少会出现所有的页面都被分离的情况,被分离的往往只有其中一部分农业面。每个页面的大小只有4KB,一个Redis实例里面一般都会有成千上万个页面。
子进程因为数据没有变化,它能看到的内存里的数据在进程产生的一瞬间就凝固了,再也不会改变,这也是为什么Redis的持久化叫“快照”的原因。接下来子进程就可以非常安心地遍历数据,进行序列化写磁盘了。
AOF
AOF日志存储的是Redis服务器的顺序指令序列,AOF日志只记录对内存进行修改的指令记录。
Redis会在接收到客户端修改指令后,进行参数校验、逻辑处理,如果没问题,就立即将该指令文本存储到AOF日志中,也就是说,先执行指令才将日志存盘。这点不同于leveldb、hbase等存储引擎,它们都是先存储日志再做逻辑处理。
Redis在长期运行的过程中,AOF的日志会越来越长。如果实例宕机重启,重放整个AOF日志会非常耗时,导致Redis长时间无法对外提供服务,所以需要对AOF日志瘦身。
Redis提供了bgrewriteaof指令用于对AOF日志进行瘦身,其原理就是开辟一个子进程对内存进行遍历,转换成一系列Redis的操作指令,序列化到一个新的AOF日志文件中。序列化完毕后再将操作期间发生的增量AOF日志追加到这个新的AOF日志文件中,追加完毕后就立即替代旧的AOF日志文件了,瘦身工作就完成了。
Linux的glibc提供了fsync(int fd)函数可以将制定文件的内容强制从内核缓存刷到磁盘。只要Redis进程实时调用fsync函数就可以保证AOF日志不丢失。但是fsync是一个磁盘IO操作,它很慢!如果Redis执行一条指令就要fsync一次,那么Redis高新更能的地位就不保了。
所以在生产环境的服务器中,Redis通常是每隔1s左右执行一次fsync操作,这个1s的周期是可以配置的。这是在数据安全性和性能之间做的一个折中,在保持高新更能的同时,尽可能使数据少丢失。
Redis清除过期key的方式
Redis会将每个设置了过期时间的key放入一个独立的字典中,以后会定时遍历这个字典来删除到期的key。除了定时遍历之外,它还会使用惰性策略来删除过期的key。所谓惰性策略就是在客户端访问这个key的时候,Redis对key的过期时间进行检查,如果过期了就立即删除。如果说定时删除是集中处理,那么惰性删除就是零散处理。
定期删除
Redis默认每秒进行10次过期扫描,过期扫描不会遍历过期字典中所有的key,而是采用了一种简单的贪心策略,步骤如下:
- 从过期字典中随机选出20个key
- 删除这20个key中已经过期的key
- 如果过期的key的比例超过1/4,那就重复步骤1
同时,为了保证过期扫描不会出现循环过度,导致线程卡死的现象,算法还增加了扫描时间的上限,默认不会超过25ms。
惰性删除
在获取key时,先判断key是否过期,如果过期则删除。这种方式存在一个缺点:如果这个key一直未被使用,那么它一直在内存中,其实它已经过期了,会浪费大量的空间。
这两种策略结合起来以后,定时删除策略发生了一些改变,不再是每次扫描全部的key了,而是随机抽取一部分key进行检查,这样就降低了对CPU资源的损耗。
Redis string的设计
Redis的字符串叫“SDS”,也就是Simple Dynamic String。它的结构是一个带长度信息的字节数组。
struct SDS<T> {
T capacity; // 数组容量
T len; // 数组长度
byte flags; // 特殊标志位,不用理睬它
byte[] content; // 数组内容
}
上面的SDS结构使用了泛型T。为什么不直接用int呢?因为当字符串比较短时,len和capacity可以使用byte和short来表示,Redis为了对内存做极致的优化,不同长度的字符串使用不同的结构体来表示。
Redis的字符串有两种存储方式,在长度特别短时,使用embstr形式存储,而当长度超过44字节时,使用raw形式存储。
为了解释这种现象,我们首先来了解一下Redis对象头结构,所有的Redis对象都有下面的这个头结构。
struct RedisObject {
int4 type; // 4 bits
int4 encoding; // 4 bits
int24 lru; // 24 bits
int32 refcount; // 4 bytes
void *ptr; // 8 bytes, 64-bit system
}
不同的对象具有不同的类型type(4bit)。同一个类型的type会有不同的存储形式encoding(4bit)。为了记录对象的LRU信息,使用了24个bit来记录LRU信息。每个对象都有个引用计数,当引用计数为0时,对象就会被销毁,内存被回收。ptr指针将指向对象内容(body)的具体存储位置。这样一个RedisObject对象头结构需要占据16字节的存储空间。
接着我们再看SDS结构体的大小,在字符串比较小时,SDS对象头结构的大小是capacity+3
,至少是3字节。意味着分配一个字符串的最小空间占用为19(即16+3)字节。
struct SDS {
int8 capacity; // 1 byte
int8 len; // 1 byte
int8 flags; // 1 byte
byte[] content; // 内联数组,长度为capacity
}
embstr将RedisObject对象头结构和SDS对象连续存在一起,使用malloc方法一次分配,而raw存储形式不一样,它需要两次malloc方法,两个对象头在内存地址上一般是不连续的。
内存分配器jemalloc、tcmalloc等分配内存大小的单位都是2/4/8/16/32/64字节等,为了能容纳一个完整的embstr对象,jemalloc最少会分配32字节的空间,如果字符串再稍微长一点,那就是64自己的空间。如果字符串总体超出了64字节,Redis认为它是一个大字符串,不再适合使用embstr存储,而该使用raw形式。
当内存分配了64字节空间时,那这个字符串长度最大可以是多少呢?这个长度就是44字节。
为什么是44字节呢?64字节中,除了RedisObject的16字节和SDS的3字节,留给content的长度最多只有45(即64 - 19)字节了。字符串又是以NULL结尾,所以embstr形式最大能容纳的字符串长度就是44字节。
Redis如何使用
Reids主要通过Redis Cli进行操作,连接方式为
redis-cli -h {host} -p {port}
然后就是在命令行内使用各种get和set命令去操作数据了,具体就不在这里展示了,可以等真正要用的时候去现搜索一下。