redis中存储的数据在内存中,取内存数据速度比去磁盘取数据快多了,能够减小数据库压力。内存存储开销比较大所以redis一般存一些比较常用的数据。
redis速度很快,官方数据每秒10w+QPS。(采用单线程,多路I/O复用模型)
持久化
redis数据存储在内存中,如果不进行持久化,重启系统后数据就丢失了
1. rdb
每隔一段时间对数据进行快照存储
redis先fork一个子进程,子进程将数据集写入临时rdb文件中,等全部写完以后用这个rdb文件替换掉之前的rdb文件并删除旧的rdb。
2. aof
AOF持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以redis协议追加保存每次写的操作到文件末尾。
两者比较
RDB 方式可以保存过去一段时间内的数据,并且保存结果是一个单一的文件,可以将文件备份到其他服务器,并且在回复大量数据的时候,RDB 方式的速度会比 AOF 方式的回复速度要快。
AOF 方式默认每秒钟备份1次,频率很高,它的操作方式是以追加的方式记录日志而不是数据,并且它的重写过程是按顺序进行追加,所以它的文件内容非常容易读懂。可以在某些需要的时候打开 AOF 文件对其编辑,增加或删除某些记录,最后再执行恢复操作。保存的数据比较完整。
主从复制
单节点就算进行了持久化但是遇到了磁盘故障也会导致丢失。主从复制可以避免单节点故障问题(全部节点都坏概率太小了),又可以Master/Slave读写分离,缓解Master读的压力。不过由于所有的写操作都是在 Master 节点上操作,然后同步到 Slave 节点,那么同步就会有一定的延时。
原理
同步和命令传播
- 从服务器发出SLAVEOF命令后,从服务器会向主节点发送SYNC命令。(新的命令叫PSYNC,可以解决断线重复制的问题)
- 主服务器执行 BGSAVE 命令,在后台生成一个 RDB 文件,并使用一个缓冲区记录从开始执行的所有写命令。
- 主服务器的 BGSAVE 命令执行完毕后,主服务器会将 BGSAVE 命令生成的 RDB 文件发送给从服务器,从服务器接收此 RDB 文件,并将服务器状态更新为RDB文件记录的状态。
- 主服务器把缓冲区的写命令写发给从服务器,从服务器执行这些命令。
- 同步完成后,主服务器遇到新的写命令后,把这些写的命令传播给从服务器。然后从服务器执行后就能保持一致了。
哨兵
如果master服务器挂了那么整个系统就无法用了。哨兵模式监控redis系统的运行状态,当master挂了以后可以主动挑选出一个从节点担当主节点(自动通过投票机制),并建立和其他节点的关系。
redis集群
可以在多个 Redis 节点之间进行数据共享
集群+主从复制实现了redis高可用,又分布式服务又不容易挂掉。
redis集群不支持事务,rename(因为涉及了多个key)
Redis Cluster | 相关博客
去中心化,每个节点都是平等的并实现了redis的分布式存储
Redis 集群没有并使用传统的一致性哈希来分配数据,而是采用另外一种叫做哈希槽 (hash slot)的方式来分配的。redis cluster 默认分配了 16384 个slot,当我们set一个key 时,会用CRC16算法来取模得到所属的slot,然后将这个key 分到哈希槽区间的节点上,具体算法就是:CRC16(key) % 16384。
所以,我们假设现在有3个节点已经组成了集群,分别是:A, B, C 三个节点,它们可以是一台机器上的三个端口,也可以是三台不同的服务器。那么,采用哈希槽 (hash slot)的方式来分配16384个slot 的话,它们三个节点分别承担的slot 区间是:
节点A覆盖0-5460;
节点B覆盖5461-10922;
节点C覆盖10923-16383.
这种哈希槽的分配方式有好也有坏,好处就是很清晰,比如我想新增一个节点D,redis cluster的这种做法是从各个节点的前面各拿取一部分slot到D上。
一致性哈希|相关博客
hash(服务器的IP地址) % 2^32
2^32 无符号整形的最大值
通过上述公式算出的结果一定是一个0到2^32-1之间的一个整数,我们就用算出的这个整数,代表服务器A,既然这个整数肯定处于0到2^32-1之间,那么,上图中的hash环上必定有一个点与这个整数对应,而我们刚才已经说明,使用这个整数代表服务器A,那么,服务器A就可以映射到这个环上。
接下来使用如下算法定位数据访问到相应服务器: 将数据key使用相同的函数Hash计算出哈希值,并确定此数据在环上的位置,从此位置沿环顺时针“行走”,第一台遇到的服务器就是其应该定位到的服务器!
redis缓存雪崩/缓存穿透
一致性哈希博客里面如果不使用一致性哈希,使用普通哈希函数,如果其中有一台服务器宕机了,哈希函数会大概率定位错服务器导致大多数缓存失效,这就是缓存雪崩。
正常使用缓存的流程是拿到key后先去redis查询,没有的话再去数据库查询,如果查到数据以后保存key和value在缓存。恶意攻击就会故意拿一些没有的key去查询,这会对数据库造成压力,这就是缓存穿透。(可以把key查不到的值稍微缓存个一分钟解决。)
redis的事务
redis事务是通过MULTI,EXEC,DISCARD和WATCH四个原语实现的。
先以 MULTI 开始一个事务, 然后将多个命令入队到事务中, 最后由 EXEC 命令触发事务, 一并执行事务中的所有命令
不保证原子性:redis同一个事务中如果有一条命令执行失败(参考事务中的2类错误,执行失败是指第二类错误),其后的命令仍然会被执行,没有回滚,这也就是:Redis部分支持事务。
redis的数据类型(String、List、Set、Hash、ZSet)
redis的内部整体的存储结构就是一个大的hashmap
Redis中的一个对象的结构体包含:类型(上面五种),编码方式(底层实现),指向对象的值(*ptr)等。
typedef struct redisObject {
// 类型
unsigned type:4;
// 编码
unsigned encoding:4;
// 对象最后一次被访问的时间
unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
// 引用计数
int refcount;
// 指向实际值的指针
void *ptr;
} robj;
底层数据结构
1.String 相关博客
string针对不同的字符串会相应转换成三种不同的底层实现:
- 整数,存储字符串长度小于21且能够转化为整数的字符串。(int)
- EMBSTR,存储字符串长度小于等于44(以前是39)的字符串(REDIS_ENCODING_EMBSTR_SIZE_LIMIT)。
- 剩余情况使用sds进行存储。(raw)
为什么以44为界限分配:上面redisObject对象头占据了16字节,然后sds对象头capacity,len,flags各占一个字节,字符串结尾\0还要占一个字节,redis的内存分配器jemalloc给embstr最多64字节,减去上面的就变成最长44字节了。
embstr和sds区别:embstr的创建只需要分配一次内存,而sds需要两次。同样释放内存也一样
不过embstr是只读的,如果需要修改他的值需要转换成raw后才能进行修改
embstr 存储形式
是将 RedisObject 对象头和 SDS 对象连续存在一起,使用 malloc 方法一次分配。而 raw 存储形式不一样,它需要两次 malloc,两个对象头在内存地址上一般是不连续的
SDS
struct sdshdr {
// buf 中已占用空间的长度
int len;
// buf 中剩余可用空间的长度
int free;
// 数据空间
char buf[];
};
SDS本质上就是char *,不过多了sdshdr结构的存在,获取字符串长度的时间复杂度为O(1),直接读取len就可以了。有free的存在,可以修改字符串长度时候杜绝缓冲区溢出,并减少修改字符串时带来的内存重分配次数(C 字符串每次增长或者缩短, 都要对保存这个 C 字符串的数组进行一次内存重分配操作)。
2. List
压缩链表
相关博客1
相关博客2
列表元素少而且里面元素长度也短的时候用压缩链表比较好
压缩链表将数据按照一定规则编码在一块连续的内存区域,目的是节省内存。
其实redis底层存储不是简单的链表而是快速链表quicksort,是由双链表+压缩链表组合而成。将多个压缩链表用双指针串起来。既满足了快读插入删除的性能,又不会出现太大的空间冗余
双向链表
// list 节点
typedef struct listNode {
// 前驱节点
struct listNode *prev;
// 后继节点
struct listNode *next;
// 节点值
void *value;
} listNode;
// redis 双链表实现
typedef struct list {
listNode *head; // 表头指针
listNode *tail; // 表尾指针
void *(*dup)(void *ptr); // 节点值复制函数
void (*free)(void *ptr); // 节点值释放函数(函数指针)
int (*match)(void *ptr, void *key); // 节点值对比函数
unsigned long len; // 链表包含的节点数量
} list;
当每增加一个listNode的时候,就需要重新malloc一块内存
节点包含前驱指针和后继指针,可以使用迭代轻松遍历
header和tail,快速定位头部和尾部。对于在链表的头部或尾部进行插入节点的时间复杂度全部为O(1)
len:获取链表长度的时间复杂度为O(1)。
3.Hash
当对象数目不多且内容不大的时候也可以用压缩链表,哈希对象是按照key1,value1,key2,value2这样的顺序存放来存储的。
redis hashtable 解决哈希冲突使用链地址法(python的dict用的是开放寻址法)
typedef struct dictht {
dictEntry **table; // 哈希表数组
unsigned long size; // 哈希表数组的大小
unsigned long sizemask; // 用于映射位置的掩码,值永远等于(size-1)
unsigned long used; // 哈希表已有节点的数量
}dictht;
Redis的字典的值只能是字符串,另外它rehash用的是渐进式rehash策略(一次性rehash过于耗时)。
渐进式rehash在rehash的同时,保存新旧两个hash结构,查询时会同时查询两个hash结构,然后在后续的定时任务以及hash操作指令中,循序渐进地将旧hash内容迁移到新hash结构中。迁移完成后,就会使用新的hash结构和取而代之。
4.Set
集合对象的编码可以是intset或者hashtable。
intset是一个整数集合,里面存的为某种同一类型的整数
5.ZSet
类似于java中sortedset和hashmap的结合体,一方面它是一个set保证了value的唯一性,另一方面它可以为每个value赋值一个score,代表value的排序权重。它的内部实现:跳跃链表。
普通链表按照score进行排序。意味着如果新元素要进来时需要遍历链表才能找到特定位置的插入点。所以跳跃链表使用的是层级索引(最多32层)
python中redis的基本操作
import redis
# StrictRedis默认参数host='localhost', port=6379, db=0
# python3 redis 默认get字符串的时候 返回的是byte,需要设置decode_responses为True
r = redis.StrictRedis(password='your password',decode_responses=True)
1.string
r.set('name', 'alan', ex=20) #ex为过期时间单位秒
r.mset(name1='peter', name2='ben') #批量设置
r.get('name1') #获取值,没有的话会返回None
2.hash
r.hset('student', 'alan', 12) # student = {'alan':12}
r.hget('student', 'alan') # 返回12 字符串类型
r.hgetall('student') # 返回{'alan': '12'} dict类型
dic={"ben":10, "peter":22}
r.hmset("student",dic) #批量设置
r.hlen('student') # 获取键值对个数
r.hkeys('student') # 获取所有keys 返回一个数组
r.hvals('student') # 获取所有vals
r.hexists('student', 'alan') #返回True
r.hdel('student_age', 'alan') #删除指定key的键值对
3.list
blpop/brpop阻塞读 消息延迟几乎为0。阻塞读在队列没有数据的时候会进入休眠状态,一旦数据来临就会醒过来。
r.lpush('l_name', 1) #添加元素至列表最左边
r.lpush('l_name', 2, 3) # [3, 2, 1]
r.rpush('l_name', 4) # 添加元素至列表最右边
r.llen('l_name') # 查看列表元素个数
r.lpop('l_name') # 弹出列表最左边元素
r.lrange('l',0, -1) # 获取范围内所有元素都是闭区间,-1代表最后一个元素
4.set
r.sadd('s_name', 'alan')
r.smembers('s_name') # 获取所有元素
r.scard('s_name') # 元素个数
r.sadd('s2_name','peter')
r.sdiff('s_name','s2_name') #获取在s_name但不在s2_name中的元素
r.sismember('s_name', '12') #检查元素是否在set中
r.spop('s_name') # 移除最右边的元素
5.zset
ZADD key score member [score member ...]
redis分布式锁
分布式锁的实现目标就是要在redis里面占坑,当别的进程也要来占坑时,发现坑已经被人占了,只能放弃或稍后再来占。
占坑一般使用setnx(set if not exists)指令,只允许一个客户端来占坑。先来先占,在调用del指令释放坑。
为了防止死锁我们要拿到坑位以后先设置个过期时间,保证锁自动释放。
redis2.8以后加入了set指令的扩展参数,使set和expire可以一起执行。
> set lock true ex 5 nx
...
> del lock