【Sofice小司笔记】4 Redis,包含nosql,redis架构,8中数据类型,事务,持久化,配置文件详解,发布订阅,集群管理,缓存穿透和雪崩

NoSQL

关系型数据库存在的问题

  • 网站的用户并发性非常高,往往达到每秒上万次读写请求,对于传统关系型数据库来说,硬盘 I/O 是一个很大的瓶颈
  • 网站每天产生的数据量是巨大的,对于关系型数据库来说,在一张包含海量数据的表中查询,效率是非常低的。因此,关系型数据不适合持久存储海量数据
  • 很难进行横向扩展(增加服务器),也就是说想要提高数据处理能力,要使用性能更好的计算机(纵向扩展
  • 性能欠佳:导致关系型数据库性能欠佳的最主要原因就是多表的关联查询,为了保证数据库的ACID特性,必须尽量按照范式要求设计数据库,关系数据库中的表存储的往往是一个固定的、格式化的数据结构

特点

  1. 高可扩:数据之间没有联系
  2. 高性能:nosql是细粒度的缓存
  3. 高可用:CAP和BASE(异地多活)
  4. 数据类型多样:键值对,列,文档,图
  5. 没有固定查询语言
  6. 最终一致性
  7. 不遵循 ACID 特性(不提供对事务的处理)

NoSQL 的四大分类

KV 键值对

  • 新浪:Redis
  • 美团:Redis + Tair
  • 阿里、百度:Redis + memecache

文档型数据库(bson 格式和 json一样)

  • MongoDB (一般必须要掌握)

    • MongoDB 是一个基于分布式文件存储的数据库,C++ 编写,主要用来处理大量的文档!
    • MongoDB 是一个介于关系型数据库和非关系型数据中中间的产品!MongoDB 是非关系型数
      据库中功能最丰富,最像关系型数据库的!
  • ConthDB

列存储数据库

  • HBase
  • 分布式文件系统

图形 (Graph) 数据库

  • 他不是存图形,放的是关系,比如:朋友圈社交网络,广告推荐
  • Neo4j,InfoGrid;

🍹 四者对比:

img

Redis 入门

🎉 免费和开源!是当下最热门的 NoSQL 技术之一,也被人们称之为结构化数据库

🏠 官网 http://www.redis.cn/documentation.html

Redis(Remote Dictionary Server ),即远程字典服务:是一个开源的使用 ANSI C 语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的API。与传统数据库不同的是 Redis 的数据是存在内存中的 ,也就是它是内存数据库,所以读写速度非常快,因此 Redis 被广泛应用于缓存方向

端口:6379

🆚 Redis 与其他 key - value 缓存产品相比有以下三个特点

  • Redis 支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
  • Redis 不仅仅支持简单的 key-value 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储。
  • Redis支持数据的备份,即 master-slave 主从模式的数据备份。

👍 Redis 优势

  • 性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。
  • 丰富的数据类型 – Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
  • 原子 – Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。Redis单条命令是具有原子性的,但是事务并不保证原子性。
  • 丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性。

Redis 是很快的(10w+QPS),官方表示,Redis 是基于内存操作,CPU 不是 Redis 的性能瓶颈,Redis 的瓶颈取决于机器的内存和网络带宽,所以 Redis 是基于单线程的。Redis 将所有的数据全部放在内存中,如果使用多线程,那么 CPU 的上下文切换是一个耗时的操作。对于内存系统来说,如果没有上下文切换,多次读写都是在一个CPU上的,那么效率就是最高的。Redis 便基于此。

❓ Redis 相比 memcached 有哪些优势?

  • memcached 所有的值均是简单的字符串, redis 作为其替代者,支持更为丰富的数据类型
  • redis 的速度比 memcached 快很多
  • redis 可以持久化其数据

Redis使用场景

  • 会话缓存( Session Cache)
    最常用的一种使用 Redis 的情景是会话缓存( session cache)。用 Redis 缓存会话比其他存储(如 Memcached)的优势在于: Redis 提供持久化。
  • 全页缓存( FPC)
    除基本的会话 token 之外, Redis 还提供很简便的 FPC 平台。回到一致性问题,即使重启了Redis 实例,因为有磁盘的持久化,用户也不会看到页面加载速度的下降,这是一个极大改进,类似 PHP 本地 FPC。再次以 Magento 为例, Magento 提供一个插件来使用 Redis 作为全页缓存后端。此外,对 WordPress 的用户来说, Pantheon 有一个非常好的插件 wp-redis,这个插件能帮助你以最快速度加载你曾浏览过的页面。
  • 队列
    Reids 在内存存储引擎领域的一大优点是提供 list 和 set 操作,这使得 Redis 能作为一个很好的消息队列平台来使用。 Redis 作为队列使用的操作,就类似于本地程序语言(如 Python)对 list 的 push/pop 操作。
  • 排行榜/计数器
    Redis 在内存中对数字进行递增或递减的操作实现的非常好。集合( Set)和有序集合(Zset)也使得我们在执行这些操作的时候变的非常简单。
  • 发布/订阅
    最后(但肯定不是最不重要的)是 Redis 的发布/订阅功能。发布/订阅的使用场景确实非常多。我已看见人们在社交网络连接中使用,还可作为基于发布/订阅的脚本触发器,甚至用Redis 的发布/订阅功能来建立聊天系统 。

启动

注意!连接远程服务器的redis服务protected-mode nodaemonize yes,注释bind,更改端口(不然会被扫描挖矿),设置安全组

# 服务器
redis-server redis.conf
# 客户端
redis-cli

Redis系统命令

Redis 不区分大小写命令

redis 默认有 16 个数据库,默认使用的是第0个

# 选择数据库
select 3
# 查看数据库大小
dbsize
# 清除当前数据库
flushdb
# 清除所有数据库
flushall
# 清屏
clear
# 查看信息
# Server,Clients,Memory,Persistence,Stats,Replication,CPU,Cluster,Keyspace
info [section]
# 设置密码
config set requirepass 123456
# 授权密码
auth 123456

Redis架构

底层数据结构

① SDS 字符串(String)

在Redis中,C语言的字符串只会用于一些无需对字符串修改的地方,如日志打印等。而Redis默认的字符串实现是简单动态字符串(simple dynamic string,SDS)。

如set命令中的key底层即是一个SDS,而value如果是一个字符串类型,则底层也是SDS,如果value是列表,则列表里的每个元素底层都是SDS。除了set命令外,SDS还被用作缓冲区:AOF模块中的AOF缓冲区,客户端状态中的输入缓冲区等都是SDS实现的。

SDS的定义在Redis源码的src目录下的sds.h和sds.c文件中,定义如下:

typedef struct sdshdr{
     //记录buf数组中已使用字节的数量
     //等于 SDS 保存字符串的长度
     unsigned int len;
     //记录 buf 数组中未使用字节的数量
     unsigned int free;
     //字节数组,用于保存字符串
     char buf[];
}
image-20220116195535997

【优点】:封装了 len 属性,直接获取长度;增加时动态扩张

sds 扩容

  • 当字符串进行初始化的时候,buf=len+1,就是加上’\0’作为长度
  • 当修改后的 SDS 长度 len 小于 1MB,len=len*2,即buf加倍
  • 当修改后的 SDS 长度 len 大于 1MB,len=len*2+1MB

SDS 缩短

并不会立即使用内存重分配来回收多出来的字节,而是使用 free 属性将这些字节的数量记录下来,等待将来使用。 通过此策略,可以避免内存重分配,同时将来增长操作也有空间。 同时 SDS 也有相应的 API ,用来真正释放未使用空间,不用担心内存的浪费。

二进制存储

SDS 由于有 len 属性的存在,使用 len 来判断字符串是否结束,而不是空字符。这样就避免了二进制数据的问题,可以用来保存图片,音频,视频等文件的二进制数据。

② list 链表(List)

C 语言中没有内置链表的数据结构,Redis 实现了自己的链表结构。Redis 中列表的底层实现之一就是链表。

由于在list的结构中定义了头尾指针和长度,可以让push/pop、或者是求长度的操作复杂度只有o(1)。使用了void*的操作实现了多态,可以保存不同的类型的数据。

typedef struct list{
     //表头节点
     listNode *head;
     //表尾节点
     listNode *tail;
     //链表所包含的节点数量
     unsigned long len;
     //节点值复制函数
     void (*free) (void *ptr);
     //节点值释放函数
     void (*free) (void *ptr);
     //节点值对比函数
     int (*match) (void *ptr,void *key);
}list;

typedef  struct listNode{
       //前置节点
       struct listNode *prev;
       //后置节点
       struct listNode *next;
       //节点的值
       void *value;  
}listNode
image-20220116204730362

③ ziplist 压缩链表(List,Hash,Zset)

压缩列表是列表键和哈希键的底层实现之一。当一个列表键只包含少量列表项并且每个都是小整数值或者长度比较短的字符串时,Redis 就采用压缩列表做底层实现。当一个哈希键只包含少量键值对,并且每个键值对的键和值也是小整数值或者长度比较短的字符串时,Redis 就采用压缩列表做底层实现。

压缩列表是 Redis 为了节约内存而实现的,是一系列特殊编码的连续内存块组成的顺序型数据结构。

image-20220116205401185

zlbytes :4 字节。ziplist的总长度(内存字节数),在内存重分配或者计算 zlend 的位置时使用。
zltail :4 字节。表尾节点相对表头的偏移地址,可以直接确定表尾节点的地址,无需遍历。
zllen :2 字节。节点数量,由于只有 2 字节大小,那么小于 65535 时,表示节点数量;等于 65535 时,需要遍历得到总数。
entry :列表节点,长度不定,由内容决定。
zlend :1 字节,特殊值 0xFF ,用于标记压缩列表的结束。

每个压缩列表节点的结构如图:

image-20220116205900475

prelen是前一个实体的长度,encode是指当前信息的编码信息,content是指编码过的信息

④ hashtable 字典(Hash,Set)

字典在 Redis 中应用很广泛,Redis 底层数据库就是用字典来实现的。任意一个键值对,无论是什么类型,都是存放在数据库的字典中的。另外,字典还是哈希对象的底层实现之一。

typedef struct dictht{
     //哈希表数组
     dictEntry **table;
     //哈希表大小
     unsigned long size;
     //哈希表大小掩码,用于计算索引值
     unsigned long sizemask;
     //该哈希表已有节点的数量
     unsigned long used;
 
}dictht;
typedef struct dictEntry{
    void *key;
    union{
        void *val;
        uint64_tu64;
        int64_ts64;
    }v;
    struct dictEntry *next;
}dictEntry
image-20220119235003834

新增时,先根据键值对的键计算出哈希值,然后根据 sizemask 属性和哈希值,计算索引值——即落入数组中的哪个位置。之后如果有一个位置多个键值对要存入时,组成单向链表即可。
这里和 HashMap 的不同之处在于,链表添加时总是添加在表头位置。因为 dictEntry 节点组成的链表没有指向链表表尾的指针,为了速度考虑,总是将新节点加在链表的表头位置。

扩容和缩容

负载因子=哈希表中已有元素和哈希桶数的比值

  • 负载因子小于1一定不扩容

  • 负载因子大于5一定扩容

  • 负载因子如果在1-5之间,redis没有进行save/rewrite的操作就会扩容

  • 负载因子如果是0.1,那么会进行缩容

扩容会变成原来的2倍,缩容会变成原来的1/2。

渐进式 rehash

如果键值对量巨大时,一次性全部 rehash 必然造成一段时间的停止服务。所以要分多次、渐进式的将键值对从 ht[0] 慢慢的 rehash 到 ht[1] 中。

具体过程:

  1. 为 ht[1] 分配空间,同时有 ht[0] 和 ht[1] 两个哈希表。
  2. 在字典中维持一个索引计数器变量 rehashindex ,并将其置为 0 ,表示 rehash 正式开始。
  3. 在 rehash 期间,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定的操作之外,还会顺便将 ht[0] 哈希表在 rehashindex 索引上的所有键值对 rehash 到 ht[1] 上,当 rehash 工作完成之后,程序将 rehashindex 的值加一。
  4. 随着字典操作的不断进行,最终在某个时间点,ht[0] 的所有键值对都被 rehash 到 ht[1] ,这时程序将 rehashindex 的值置为 -1 ,表示 rehash 工作完成。
    渐进式 rehash 的过程中,更新删除查找等都会在两个哈希表上进行,比如查找,先在 ht[0] 中查找,如果没找到,就去 ht[1] 中查找。而新增操作,直接新增在 ht[1] 中,ht[0] 不会进行任何的新增操作。保证 ht[0] 的数量只减不增,最终变为空表。

⑤ intset 整数集合(Set)

当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis 就会使用整数集合作为集合键的底层实现。

默认其中存放的数据是从小到大有序的。

int8_t不保存对应的值,真正的类型由encoding决定,可以保存 int16_t ,int32_t ,int64_t 的整数值

typedef struct intset{
     //编码方式
     uint32_t encoding;
     //集合包含的元素数量
     uint32_t length;
     //保存元素的数组
     int8_t contents[];
}intset

插入的时候先二分查找到插入的位置(O(log(N))),然后在对插入位置后面所有的元素往后移动一个位置。复杂度(O(N))

当插入的一个数据比原有的数据的字节都大,那么整个数据中的所占用的字节都会进行升级。

⑥ skiplist 跳跃表(Zset,集群节点)

有序链表只能逐一查找元素,导致操作起来非常缓慢,于是就出现了跳表。具体来说,跳表在链表的基础上,增加了多级索引,通过索引位置的几个跳转,实现数据的快速定位。

image-20220120012950765

跳表有很多层组成。每一层都是有序的,除了最后一层都只包含部分数据。最底层的包含所有的数据

typedef struct zskiplistNode {
    // 每一层都有前进指针和跨度,从头到尾遍历时,访问会沿着层的前进指针进行。
    struct zskiplistLevel{
        struct zskiplistNode *forward;
        unsigned int span;
    }level[];
	// 后退指针,指向前一个节点,从尾到头遍历时使用。
    struct zskiplistNode *backward;
    // 分值,跳跃表中的分值按从小到大排列。
    double score;
    // 成员对象,各个节点保存有各个成员对象。
    robj *obj;
    
} zskiplistNode;
typedef struct zskiplist{
     //表头节点和表尾节点
     struct skiplistNode *header, *tail;
     //表中节点的数量
     unsigned long length;
     //表中层数最大的节点的层数
     int level;
 
}zskiplist;
image-20220120010918153

搜索:从最上层开始搜索,如果发现当前层的最大值小于搜索的值。那么就去下一层寻找,往复如此的操作,直到找到最下面的一层。复杂度(O(Log(N)))

数据淘汰策略

  • noeviction:返回错误当内存限制达到并且客户端尝试执行会让更多内存被使用的命令(大部分的写入指令,但 DEL 和几个例外)
  • allkeys-lru:尝试回收最少使用的键(LRU),使得新添加的数据有空间存放。
  • volatile-lru:尝试回收最少使用的键(LRU),但仅限于在过期集合的键,使得新添加的数据有空间存放。
  • allkeys-random:回收随机的键使得新添加的数据有空间存放。
  • volatile-random:回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键。
  • volatile-ttl:回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放。

分区

分区可以让 Redis 管理更大的内存, Redis 将可以使用所有机器的内存。如果没有分区,你最多只能使用一台机器的内存。分区使 Redis 的计算能力通过简单地增加计算机得到成倍提升,Redis 的网络带宽也会随着计算机和网卡的增加而成倍增长。

分区实现方案

  • **客户端分区:**就是在客户端就已经决定数据会被存储到哪个redis节点或者从哪个redis节点读取。大多数客户端已经实现了客户端分区。

  • **代理分区:**意味着客户端将请求发送给代理,然后代理决定去哪个节点写数据或者读数据。代理根据分区规则决定请求哪些 Redis 实例,然后根据 Redis 的响应结果返回给客户端。 redis和 memcached 的一种代理实现就是 Twemproxy

  • **查询路由(Query routing) :**客户端随机地请求任意一个 redis 实例,然后由 Redis 将请求转发给正确的 Redis 节点。 Redis Cluster 实现了一种混合形式的查询路由,但并不是直接将请求从一个 redis 节点转发到另一个 redis 节点,而是在客户端的帮助下直接 redirected 到正确的 redis 节点。

性能测试

redis-benchmark 是一个官方自带的压力(性能)测试工具。

redis 性能测试工具可选参数如下:

我们来简单测试下:

# 测试:100个并发连接 100000请求
redis-benchmark -h localhost -p 6379 -c 100 -n 100000

img

5️⃣ 五大数据类型

Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库缓存消息中间件MQ。 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。

Redis 五大基本数据类型

  • String
  • List
  • Set
  • Zset
  • Hash

Redis 键 (key) 命令

Redis 键命令用于管理 redis 的键。

# 存
set key value
# 取
get key
# 查看所有key
keys *
# 判断当前的key是否存在
EXISTS key
# 移动key到其它数据库
move name 2
# 设置key的过期时间,单位是秒
EXPIRE key second
# 查看当前key的剩余时间
ttl key
# 查看当前key的类型
type key

1. String 字符串

string的数据使用string

value可以是字符串或数字,最大512M

统计(浏览量,粉丝数)

# 追加字符串,如果当前key不存在,就相当于set key
APPEND key "hello"
# 获取字符串的长度
STRLEN key

# 自增/自减1
incr key
decr key
# 可以设置步长,指定增量
incrby key 10
decrby key 10

# 截取字符串 [0,3]
GETRANGE key 0 3 
# 获取全部的字符串(从0开始,到倒数第1个)
GETRANGE key 0 -1
# 替换从指定位置 1 开始的字符串
SETRANGE key 1 val

# 设置过期时间
setex key 30
# 如果key不存在,创建key(分布式锁常用)
setnx key val

# 同时设置多个值
mset k1 v1 k2 v2 k3 v3 
# 同时获取多个值
mget k1 k2 k3
# msetnx 是一个原子性的操作,要么一起成功,要么一起失败
msetnx k1 v1 k4 v4
# 设置一个user:1 对象 值为 json字符来保存一个对象
set user:1 {name:zhangsan,age:3}
# 这里的key是一个巧妙的设计: user:{id}:{filed}
mset user:1:name zhangsan user:1:age 2
mget user:1:name user:1:age

# getset 先get然后在set
# 如果不存在值,则返回 nil,并设置新的值; 如果存在值,获取原来的值,并设置新的值
getset db redis

2. List 列表

list内部使用list和ziplist,当数量比较小的时候会使用ziplist来减少内存使用,否则使用linkedlist

所有的 list命令都是用 l 开头的

实现栈,队列,阻塞队列

# 头部插入 (左)
LPUSH list val
# 尾部插入 (右)
Rpush list val
# 移除list的第一个元素
Lpop list
# 移除list的最后一个元素
Rpop list 

# 通过区间获取具体的值
LRANGE list 0 1 
# 通过下标获得 list 中的某一个值
lindex list index
# 返回列表的长度
Llen list 

# 移除list集合中指定个数的value,精确匹配
lrem list 1 val 

# 通过下标截取指定的长度,这个list只剩下被截取的元素
ltrim list 1 2 

# 移除列表的最后一个元素,将他移动到新的列表中
rpoplpush list list2

# 将下标0的值替换为val,如果该列表不存在或下标越界,更新就会报错
lset list 0 val 

# 将某个具体的value插入到列表中某个元素的前面或者后面
linsert list before val val2
linsert list after val val2

3. Set 集合

set在数据都是整数类型时,使用intset,否则使用hashtable

set 中的值是不能重复的。

共同关注,共同爱好,推荐好友

# 添加
sadd set val
# 移除set集合中的指定元素
srem set val

# 查看指定set的所有值
smembers set
# 判断某一个值是不是在set集合中
sismember set val
# 获取set集合中的内容元素个数
scard set 

# 随机抽选出(指定个数的)元素
SRANDMEMBER set
SRANDMEMBER set 2

# 随机删除一些set集合中的元素
spop set

# 将一个指定的值,移动到另外一个set集合
smove set set2 val 

# 计算差集,交集,并集
sdiff set1 set2 # set1 - set2
sinter set1 set2
sunion set1 set2
# 并存储
sdiffstore set3 set1 set2
sinterstore set3 set1 set2
sunionstore set3 set1 set2

4. Zset 有序集合

Zset使用ziplist和skiplist+hashtable,当数据量小的时候使用ziplist,否则使用skiplist+hashtable

Zset 在 set 的基础上,增加了一个值得分: k1 score1 v1

排行榜,消息等级

# 添加
zadd set score val

# 区间获取,默认升序
zrange set 0 -1 	# 升序
zrevrange set 0 -1 	# 降序
# 按score获取
# WITHSCORES作用是也输出分数
zrangebyscore set -inf +inf WITHSCORES

# 移除有序集合中的指定元素
zrem set val
# 获取有序集合中的个数
zcard set

# 获取指定 score 区间的成员数量
zcount set 101 103

5. Hash 哈希

Hash使用hashtable和ziplist,当数据量比较小的时候使用ziplist,否则使用hashtable

对象的存储

# get,set
hset hash key val
hget hash key
# 获取全部的数据
hgetall hash
# 只获得所有field
hkeys hash
# 只获得所有value
hvals hash

# 删除hash指定key字段
hdel hash key

# 获取hash表的字段数量
hlen hash 
# 判断hash中指定字段是否存在
hexists hash key

# 指定增量
hincrby hash key 1

# 如果字段不存在则可以设置
hsetnx hash key val

3️⃣ 三种特殊数据类型

1. Gerspatial 地理位置

GEO底层的实现原理其实就是 Zset,我们可以使用 Zset命令来操作 geo

ZRANGE china:city 0 -1 # 查看地图中全部的元素
zrem china:city beijing # 移除指定元素

# 添加地理位置
# 两级无法直接添加,我们一般会下载城市数据,直接通过 java 程序一次性导入
# 有效的经度从-180度到180度。有效的纬度从-85.05112878度到85.05112878度。
# 当坐标位置超出上述指定范围时,该命令将会返回一个错误。
geoadd china:city 116.40 39.90 beijing

# 获取指定的城市的经度和纬度
geopos china:city beijing

# 获取两地之间的距离
# 单位有 m米,km千米,mi英里,ft英尺
geodist china:city beijing shanghai km # 查看上海到北京的直线距离

# 以给定的经纬度为中心, 找出某一半径内的元素
georadius china:city 110 30 1000 km # 以110,30 这个经纬度为中心,寻找方圆1000km内的城市
withdist # 显示到中间距离的位置
withcoord # 显示他人的定位信息
count 1 #筛选出指定数量的结果

# 找出位于指定元素周围的其他元素
georadiusbymember china:city beijing 1000 km

# 将二维的经纬度转换为一维11个字符的Geohash字符串字符串。如果两个字符串越接近,那么则距离越近
geohash china:city beijing chongqin

2. Hyperloglog 基数统计

基数 & 基数估计:

比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为{1, 3, 5 ,7, 8}, 基数(不重复元素)为 5。 基数估计就是在误差可接受的范围内,快速计算基数。(也就是说 Hyperloglog 是有一定的错误率的,很小)

Redis 在 2.8.9 版本添加了 HyperLogLog结构,用来做基数统计

优点

计算基数所需的空间总是固定的、并且是很小的,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。

缺点

  • 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog不能像集合那样,返回输入的各个元素。
  • 有一定的错误率的(0.81)

对于统计网页的 UV (一个人访问一个网站多次,但是还是算作一个人)来说 ,传统的方式是利用 set保存用户的 id,然后就统计 set中的元素数量。这个方式如果保存大量的用户 id,就会比较麻烦。我们的目的是为了计数,而不是保存用户 id。

🚩 如果允许容错,那么一定可以使用 Hyperloglog,如果不允许容错,就使用 set或者自定义数据类型。

# 创建一组元素
PFadd key a b c d e f g h i j

# 统计基数数量
PFCOUNT key 

# 合并两组 key1 key2 => key3 并集
PFmerge key3 key1 key2 

3. Bitmap 位图

Bitmap就是通过一个 bit 位来表示某个元素对应的值或者状态, 其中的 key 就是对应元素本身,可以极大的节省储存空间。

Redis 从2.2.0版本开始新增了setbit,getbit,bitcount等几个 bitmap 相关命令。虽然是新命令,但是并没有新增新的数据类型,因为setbit等命令只不过是在set上的扩展。

用户签到(状态:签到,未签到),统计活跃用户(状态:活跃,不活跃)等只有 0 和 1 两个状态的都可以使用 Bitmap

# 设置sign的第3个位为1
setbit sign 3 1
# 获取
getbit sign 3
# 统计1的个数
bitcount sign

事务 Transaction

严格意义来讲,Redis的事务和我们理解的传统数据库(如mysql)的事务是不一样的。

Redis 事务本质:一组命令的集合。一个事务中的所有命令都会被序列化,在事务的执行过程中,按照顺序执行。

Redis事务没有没有隔离级别的概念。

Redis 事务可以一次执行多个命令, 并且带有以下三个重要的保证:

  • 批量操作在发送 EXEC命令前被放入队列缓存。(所有的命令在事务中,并没有直接被执行,只有发起执行命令 EXEC的时候才会执行)

  • 收到 EXEC命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行。(Redis单条命令是具有原子性的,但是事务并不保证原子性

  • 在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中。

一个事务从开始到执行会经历以下三个阶段:

# 开始事务
MULTI
# 命令入队
SET book-name "Mastering C++ in 21 days"
GET book-name
SADD tag "C++" "Programming" "Mastering Series"
SMEMBERS tag
# 执行事务
EXEC

# 取消事务 
DISCARD
# 监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
WATCH key [key ...]
# 取消 WATCH 命令对所有 key 的监视。
UNWATCH 

Redis WATCH 监控事务

在 Redis 中使用 watch命令可以决定事务是执行还是回滚。可以在 multi命令之前使用 watch命令监控某些键值对。

当 Redis 使用 exec命令执行事务的时候,它首先会去比对被 watch命令所监控的键值对,如果没有发生变化,那么它会执行事务队列中的命令,提交事务;如果发生变化,那么它不会执行任何事务中的命令,而去事务回滚。无论事务是否回滚,Redis 都会取消执行事务前的 watch 命令,这个过程如下图所示:

🚨 execdiscardunwatch 命令都会清除所有监视。

Redis 参考了多线程中使用的 CAS 去执行的。

事务正常运行,没有被其他线程修改:

127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set out 0
OK
127.0.0.1:6379> watch money # 监视 money 对象
OK
127.0.0.1:6379> multi 
OK
127.0.0.1:6379> DECRBY money 20
QUEUED
127.0.0.1:6379> INCRBY out 20
QUEUED
127.0.0.1:6379> exec # 事务正常结束,数据期间没有发生变动,这个时候就正常执行成功
1) (integer) 80
2) (integer) 20

Jedis—Java 使用 Redis

Jedis 是 Redis 官方推荐的 Java 连接开发工具, 使用 Java 操作 Redis 的中间件。

首先需要导入 Jedis 的 maven 依赖:

<!--导入jedis的包-->
<dependencies>
    <!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>3.2.0</version>
    </dependency>
    <!--fastjson-->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.62</version>
    </dependency>
</dependencies>

接下来进行测试是否能够正确访问到 Redis 服务:

import redis.clients.jedis.Jedis;
public class TestPing {
    public static void main(String[] args) {
        // 1、 new Jedis 对象即可
        Jedis jedis = new Jedis("127.0.0.1",6379);
        // jedis 所有的命令就是我们之前学习的所有指令!所以之前的指令学习很重要
        System.out.println(jedis.ping());
    }
}

① String(字符串) 实例

public class RedisStringJava {
    public static void main(String[] args) {
        //连接本地的 Redis 服务
        Jedis jedis = new Jedis("localhost");
        //设置 redis 字符串数据
        jedis.set("name","smallbeef");
        // 获取存储的数据并输出
        System.out.println("redis 存储的字符串为:" + jedis.get("name"));
    }
}

② List(列表) 实例

public class RedisListJava {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("127.0.0.1",6379);
        // 存入列表
        jedis.lpush("mylist","one");
        jedis.lpush("mylist","two");
        jedis.lpush("mylist","three");
        // 获取数据
        List<String> mylist = jedis.lrange("mylist", 0, 2);
        for(int i=0; i<mylist.size(); i++) {
            System.out.println("列表项为: "+mylist.get(i));
    }
}

③ keys 实例

public class RedisKeysJava {
    public static void main(String[] args) {
		Jedis jedis = new Jedis("127.0.0.1",6379);
        Set<String> keys = jedis.keys("*");
        Iterator<String> iterator = keys.iterator();
        while(iterator.hasNext()){
            String key = iterator.next();
            System.out.println(key); // name mylist
        }
    }
}

④ 事务实例

public class TestTX {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("127.0.0.1",6379);
        jedis.flushDB();

        JSONObject jsonObject = new JSONObject();
        jsonObject.put("hello","world");
        jsonObject.put("name","smallbeef");
        // 开启事务
        Transaction multi = jedis.multi();
        String s = jsonObject.toJSONString();
        // jedis watch
        try{
            multi.set("user1", s);
            multi.set("user2", s);
            int i = 1/0 ; // 抛出异常,执行失败
            multi.exec();
        } catch (Exception e){
            multi.discard(); // 放弃事务
            e.printStackTrace();
        } finally {
            System.out.println(jedis.get("user1")); // null
            System.out.println(jedis.get("user2")); // null
            jedis.close(); // 关闭连接
        }
    }
}

📑 Redis 配置文件详解

centos中通过yum安装的默认的配置文件在 /etc/redis.conf

通过配置启动:redis-server /etc/redis.conf

1. 网络相关

bind 127.0.0.1 # 绑定的主机地址,如果需要允许外网访问,需要将此行注释,或者改为 bind 0.0.0.0

protected-mode yes # 保护模式

port 6379 # 指定 Redis 监听端口,默认端口为 6379

timeout 300	# 当客户端闲置多长秒后关闭连接,如果指定为 0 ,表示关闭该功能

2. 守护进程

daemonize yes # 以守护进程的方式运行,默认是 no,我们需要自己开启为 yes

# 当 Redis 以守护进程方式运行时,Redis 默认会把 pid 写入 /var/run/redis.pid 文件,可以通过 pidfile 指定
pidfile /var/run/redis.pid

3. 日志

# 指定日志记录级别,Redis 总共支持四个级别:debug、verbose、notice、warning,默认为 notice
loglevel notice

logfile "" # 日志的文件位置名

# 日志记录方式,默认为标准输出,如果配置 Redis 为守护进程方式运行,而这里又配置为日志记录方式为标准输出,则日志将会发送给 /dev/null
logfile stdout

4. RDB 配置

持久化,在规定的时间内,执行了多少次操作,则会持久化到文件 .rdb. aof

# 如果900s内,如果至少有一个1 key进行了修改,就进行持久化操作
save 900 1

# 持久化如果出错,是否还需要继续工作
# 当启用了RDB且最后一次后台保存数据失败,Redis是否停止接收数据。这会让用户意识到数据没有正确持久化到磁盘上,否则没有人会注意到灾难(disaster)发生了。如果Redis重启了,那么又可以重新开始接收数据了
stop-writes-on-bgsave-error yes 

# 指定存储至本地数据库时是否压缩数据,默认为 yes,Redis 采用 LZF 压缩,如果为了节省 CPU 时间,可以关闭该选项,但会导致数据库文件变的巨大
rdbcompression yes 

# 保存rdb文件的时候,进行错误的检查校验
rdbchecksum yes 

# 快照文件名
dbfilename dump.rdb

# 快照文件的存放路径
dir /var/lib/redis 

5. 安全

可以在这里设置redis的密码,默认是没有密码

# 获取redis的密码
config get requirepass 
# 设置redis的密码
config set requirepass "123456" 
# 使用密码进行登录
auth 123456

设置密码后可使用如下命令进行登录:

redis-cli -a 密码

6. 限制

databases 16 # 数据库的数量,默认是 16 个数据库

# 设置同一时间最大客户端连接数,默认无限制,Redis 可以同时打开的客户端连接数为 Redis 进程可以打开的最大文件描述符数,如果设置 maxclients 0,表示不作限制。当客户端连接数到达限制时,Redis 会关闭新的连接并向客户端返回 max number of clients reached 错误信息
maxclients 10000 

# 指定 Redis 最大内存限制,Redis 在启动时会把数据加载到内存中,达到最大内存后,Redis 会先尝试清除已到期或即将到期的 Key,当此方法处理 后,仍然到达最大内存设置,将无法再进行写入操作,但仍然可以进行读取操作。Redis 新的 vm 机制,会把 Key 存放内存,Value 会存放在 swap 区
maxmemory <bytes> 

7. AOF 配置

# 指定是否在每次更新操作后进行日志记录,Redis 在默认情况下是异步的把数据写入磁盘,如果不开启,可能会在断电时导致一段时间内的数据丢失。因为 redis 本身同步数据文件是按上面保存条件来同步的,所以有的数据会在一段时间内只存在于内存中。
# 默认为 no,即默认使用rdb方式持久化的,在大部分所有的情况下,rdb完全够用
appendonly no 

# 指定更新日志文件名(持久化的文件的名字),默认为 appendonly.aof
appendfilename "appendonly.aof" 

# 指定更新日志条件,共有 3 个可选值:
 - no:不执行 sync,表示等操作系统进行数据缓存同步到磁盘(快)
 - always:表示每次更新操作后手动调用 fsync() 将数据写到磁盘(慢,安全)
 - everysec:表示每秒同步一次(折中,默认值),可能会丢失这1s的数据
appendfsync everysec 

持久化

Redis是一个内存数据库,为了避免内存中数据丢失,Redis提供了对持久化的支持,我们可以选择不同的方式将数据从内存中保存到硬盘当中,使数据可以持久化保存。

持久化流程

既然redis的数据可以保存在磁盘上,那么这个流程是什么样的呢?

要有下面五个过程:

  • 客户端向服务端发送写操作(数据在客户端的内存中)。

  • 数据库服务端接收到写请求的数据(数据在服务端的内存中)。

  • 服务端调用write这个系统调用,将数据往磁盘上写(数据在系统内存的缓冲区中)。

  • 操作系统将缓冲区中的数据转移到磁盘控制器上(数据在磁盘缓存中)。

  • 磁盘控制器将数据写到磁盘的物理介质中(数据真正落到磁盘上)。

这5个过程是在理想条件下一个正常的保存流程,但是在大多数情况下,我们的机器等等都会有各种各样的故障,这里划分了两种情况:

  • Redis数据库发生故障,只要在上面的第三步执行完毕,那么就可以持久化保存,剩下的两步由操作系统替我们完成。

  • 操作系统发生故障,必须上面5步都完成才可以。

这里它提供了两种策略机制。

RDB(Redis DataBase)

RDB其实就是把数据以快照(snapshot)的形式保存在磁盘上。在指定的时间间隔内将内存中的数据集快照写入磁盘。也是默认的持久化方式,这种方式是就是将内存中数据以快照的方式写入到二进制文件中,默认的文件名为 dump.rdb。

优点

  • RDB文件紧凑,全量备份,非常适合用于进行备份和灾难恢复。
  • 生成RDB文件的时候,redis主进程会fork()一个子进程来处理所有保存工作,主进程不需要进行任何磁盘IO操作。
  • RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快

缺点

  • 需要一定的时间间隔进程操作!如果 redis 意外宕机了,这个最后一次修改数据就没有了
  • fork 进程的时候,会占用一定的内容空间

触发机制

① save 触发方式

时间复杂度: O(N), N 为要保存到数据库中的 key 的数量。

使用 save命令执行一个同步保存操作,将当前 Redis 实例的所有数据快照 以 RDB 文件的形式保存到硬盘:

该命令会阻塞当前Redis服务器,具体流程如下:

一般来说,在生产环境很少执行 SAVE操作,因为它会阻塞所有客户端,这种方式显然不可取。

保存数据库的任务通常由 BGSAVE命令异步地执行。然而,如果负责保存数据的后台子进程不幸出现问题时, SAVE可以作为保存数据的最后手段来使用。

② bgsave 触发方式

时间复杂度: O(N), N 为要保存到数据库中的 key 的数量。

在后台异步保存当前数据库的数据到磁盘。

bgsave 命令执行之后立即返回 OK ,然后 Redis fork 出一个新子进程,原来的 Redis 进程(父进程)继续处理客户端请求,而子进程则负责将数据保存到磁盘,然后退出。

具体操作是Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短。基本上 Redis 内部所有的RDB操作都是采用 bgsave命令。

③ 自动化触发方式

自动触发是由我们的配置文件来完成的。

[相关配置项见Redis 配置文件详解](# 📑 Redis 配置文件详解)

AOF(Append Only File)

全量备份总是耗时的,有时候我们提供一种更加高效的方式,即日志记录:redis 会将每一个收到的写命令都通过 write函数追加到文件 appendonly.aof 中,恢复的时候就把这个文件全部在执行一遍。

优点

  • AOF 可以更好的保护数据不丢失,一般AOF会每隔1秒,通过一个后台线程执行一次 fsync操作,最多丢失1秒钟的数据。
  • AOF 日志文件没有任何磁盘寻址的开销,写入性能非常高,文件不容易破损。
  • AOF 日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写。
  • AOF 日志文件的命令通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复。比如某人不小心用 flushall命令清空了所有数据,只要这个时候后台 rewrite 还没有发生,那么就可以立即拷贝AOF文件,将最后一条 flushall命令给删了,然后再将该AOF文件放回去,就可以通过恢复机制,自动恢复所有数据

缺点

  • 相对于数据文件来说,AOF 远远大于 RDB,修复的速度也比 RDB慢
  • AOF 运行效率也要比 RDB慢,所以 Redis 默认的配置就是 RDB持久化
# 默认是不开启的,我们需要手动进行配置, 我们只需要将 appendonly 改为 yes就开启了
appendonly yes

重写

AOF 提供重写机制保证文件不会越来越大。

如果 aof 文件大于 64m, redis 提供了 bgrewriteaof命令,将内存中的数据以命令的方式保存到临时文件中,同时会 fork 出一条新进程来将文件重写。重写 AOF 文件的操作,并没有读取旧的 AOF 文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的 AOF 文件,这点和快照有点类似。

触发机制

  • 每次修改同步 always:同步持久化,每次更新操作后调用 fsync() 将数据写到磁盘,性能较差但数据完整性比较好

  • 每秒同步 everysec:异步操作,每秒记录。可能会丢失这1s的数据

  • 不同步 no:从不同步,等操作系统进行数据缓存同步到磁盘

# 指定更新日志条件,共有 3 个可选值
appendfsync everysec 

修复aof文件

redis-check-aof --fix appendonly.aof

使用建议

  1. 只做缓存时,不需要持久化
  2. 同时开启两种持久化方式时,重启时会优先载入AOF,因为AOF通常要比RDB数据完整
  3. 建议:
    • RDB更适合备份,因为不需要持续IO,不会持续变化,快速重启,不会有潜在Bug
    • 只在Slave上备份RDB,只保留save 900 1
    • AOF重写大小可以设到5G以上,尽量减少AOF rewrite 频率
    • 如果不Enable AOF,只靠 Master-Slave Replication 也可实现高可用,省掉一大笔IO,代价是Master/Slave同时挂,会丢失十几分钟数据,启动脚本也需要比较他们的RDB文件,载入较新的

发布订阅

Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。

👨‍💼 三大角色:消息发布者;频道 channel;消息订阅者

Redis 客户端可以订阅任意数量的频道。

广泛用于构建即时通信应用,比如网络聊天室和实时广播、实时提醒等。

命令

命令描述
PSUBSCRIBE pattern [pattern ...]订阅一个或多个符合给定模式的频道。
PUNSUBSCRIBE [pattern [pattern ...]]退订所有给定模式的频道。
SUBSCRIBE channel channel [...]订阅给定的一个或多个频道的信息。
UNSUBSCRIBE [channel [channel ...]]退订给定的频道。
PUBLISH channel message将信息发送到指定的频道。
PUBSUB subcommand [argument [argument ...]]查看订阅与发布系统状态。

实例

以下实例演示了发布订阅是如何工作的。在我们实例中我们创建了订阅频道名为 redisChat

订阅端:

redis 127.0.0.1:6379> SUBSCRIBE redisChat

Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "redisChat"
3) (integer) 1
# 等待读取推送的信息

发布端:

redis 127.0.0.1:6379> PUBLISH redisChat "Redis is a great caching technique"

(integer) 1

redis 127.0.0.1:6379> PUBLISH redisChat "Learn redis"

(integer) 1

# 订阅者的客户端会显示如下消息
1) "message" # 消息
2) "redisChat" # 哪个频道的消息
3) "Redis is a great caching technique" # 消息的具体内容
1) "message"
2) "redisChat"
3) "Learn redis"

原理

Redis 底层是使用 C 实现的,通过分析 Redis 源码里的 pubsub.c 文件,了解发布和订阅机制的底层实现,加深对 Redis 的理解。

Redis 提供两种信息机制:

① 频道

每个 Redis 服务器进程都维持着一个表示服务器状态的 redis.h/redisServer 结构, 结构的 pubsub_channels 属性是一个字典, 这个字典就用于保存订阅频道的信息

struct redisServer {
    // ...
    dict *pubsub_channels;
    // ...
};

⭐ 其中,字典的键为正在被订阅的频道, 而字典的值则是一个链表, 链表中保存了所有订阅这个频道的客户端

比如说,在下图展示的这个 pubsub_channels 示例中, client2client5client1 就订阅了 channel1 , 而其他频道也分别被别的客户端所订阅:

当客户端调用 SUBSCRIBE 命令时, 程序就将客户端和要订阅的频道在 pubsub_channels 字典中关联起来。

举个例子,如果客户端 client10086 执行命令 SUBSCRIBE channel1 channel2 channel3 ,那么前面展示的 pubsub_channels 将变成下面这个样子:

通过 pubsub_channels 字典, 程序只要检查某个频道是否为字典的键, 就可以知道该频道是否正在被客户端订阅; 只要取出某个键的值, 就可以得到所有订阅该频道的客户端的信息。

② 模式

redisServer.pubsub_patterns 属性是一个链表,链表中保存着所有和模式相关的信息:

struct redisServer {
    // ...
    list *pubsub_patterns;
    // ...
};

链表中的每个节点都包含一个 redis.h/pubsubPattern 结构:

typedef struct pubsubPattern {
    redisClient *client; // 订阅模式的客户端
    robj *pattern;		// 被订阅的模式
} pubsubPattern;

每当调用 PSUBSCRIBE 命令订阅一个模式时, 程序就创建一个包含客户端信息和被订阅模式的 pubsubPattern 结构, 并将该结构添加到 redisServer.pubsub_patterns 链表中。

作为例子,下图展示了一个包含两个模式的 pubsub_patterns 链表, 其中 client123client256 都正在订阅 tweet.shop.* 模式:

如果这时客户端 client10086 执行 PSUBSCRIBE broadcast.live.* , 那么 pubsub_patterns 链表将被更新成这样:

通过遍历整个 pubsub_patterns 链表,程序可以检查所有正在被订阅的模式,以及订阅这些模式的客户端。

当使用 PUBLISH命令发送信息到某个频道时, 不仅所有订阅该频道的客户端会收到信息, 如果有某个/某些模式和这个频道匹配的话, 那么所有订阅这个/这些频道的客户端也同样会收到信息。

下图展示了一个带有频道和模式的例子, 其中 tweet.shop.* 模式匹配了 tweet.shop.kindle 频道和 tweet.shop.ipad 频道, 并且有不同的客户端分别订阅它们三个。当有信息发送到 tweet.shop.kindle 频道时, 信息除了发送给 clientXclientY 之外, 还会发送给订阅 tweet.shop.* 模式的 client123client256

集群

主从模式

主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master/leader),后者称为从节点(slave/follower)。Master以写为主,Slave 以读为主。

**数据的复制是单向的,只能由主节点到从节点。**主机中的所有信息和数据,都会自动被从机保存。

默认情况下,每台 Redis 服务器都是主节点;且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。

🚩 主从复制的作用主要包括:

  • 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
  • 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
  • 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
  • 高可用(集群)基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是 Redis 高可用的基础。

一般来说,要将Redis运用于工程项目中,只使用一台Redis是万万不能的(宕机),原因如下:

  • 从结构上,单个Redis服务器会发生单点故障,并且一台服务器需要处理所有的请求负载,压力较大

  • 从容量上,单个Redis服务器内存容量有限,就算一台Redis服务器内存容量为256G,也不能将所有内存用作Redis存储内存,一般来说,单台Redis最大使用内存不应该超过20G。

电商网站上的商品,一般都是一次上传,无数次浏览的,说专业点也就是"多读少写"。对于这种场景,我们可以使用如下架构:

主从复制,读写分离。其实在 80% 的情况下都是在进行读操作,一般使用一主二从来减缓服务器的压力。

环境配置

配置从节点:端口,pid名字,log文件名,rdb文件名

查看主节点的信息:

info replication # 查看当前库的信息

复制原理

Slave 启动成功连接到 master 后会发送一个 sync同步命令,Master 接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集的命令,在后台进程执行完毕之后,master 将传送整个数据文件到 slave,并完成一次完全同步。

全量复制:slave 服务在接收到数据库文件数据后,将其存盘并加载到内存中。

增量复制:Master 继续将新的所有收集到的修改命令依次传给 slave,完成同步

注意:只要是重新连接 master,将自动执行全量复制。

薪火相传

上一个 Slave (记为 A)可以是下一个 slave 的 Master(注意 A 在它的主节点面前仍然是从节点),A 同样可以接收其他 slaves 的连接和同步请求,那么 A 作为了链条中下一个的 master, 可以有效减轻 master 的写压力。

使用slaveof命令将中间从机升级为主机,但还是无法写入,原主机slave-1。如果源主机断开,则升级为真正的主机。

反客为主

从节点可以使用 slaveof no one 升级成为主节点。

哨兵模式 sentinel

主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。Redis 从 2.8开始正式提供了 Sentinel(哨兵) 架构来解决这个问题。它是反客为主的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库

哨兵模式是一种特殊的模式,首先 Redis 提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待 Redis 服务器响应,从而监控运行的多个 Redis 实例。

这里的哨兵有两个作用:

  • 通过发送命令,让 Redis 服务器返回监控其运行状态,包括主服务器和从服务器。
  • 当哨兵监测到 master 宕机,会自动将 slave 切换成 master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机

然而一个哨兵进程对 Redis 服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会互相进行监控,这样就形成了多哨兵模式

假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行 failover(故障转移) 过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行 failover 操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线

优点

  • 哨兵集群,基于主从复制模式,所有的主从复制优点,它全有

  • 主从可以切换,故障可以转移,系统的可用性就会更好

  • 哨兵模式就是主从模式的升级,手动到自动,更加健壮

缺点

  • Redis 不易于在线扩容,集群容量一旦到达上限,在线扩容就十分麻烦
  • 实现哨兵模式的配置其实是很麻烦的,里面有很多选择(多哨兵需要配置哨兵端口等信息)

测试

首先需要在主机 Redis 的目录下新建 sentinel.conf 文件,文件内容如下

#                监控名称   host     port 得票数阈值
sentinel monitor host6379 127.0.0.1 6379 1

后面的这个数字 1,代表主机挂了后 slave 进行投票看让谁接替成为主机,得票数多少后(此处为 1 票)就会成为主机

启动哨兵:

redis-server sentinel.conf --sentinel

关闭主机服务后,哨兵将自动选取新的主机,哨兵日志如下:

可以看到,哨兵选择了 6380 为新的主机:

🚩 如果此时主机重新开启了,会归并到新的主机下,当做从机

Cluster模式

sentinel模式基本可以满足一般生产的需求,具备高可用性。但是当数据量过大到一台服务器存放不下的情况时,主从模式或sentinel模式就不能满足需求了,这个时候需要对存储的数据进行分片,将数据存储到多个Redis实例中。cluster模式的出现就是为了解决单机Redis容量有限的问题,将Redis的数据根据一定的规则分配到多台机器。

cluster集群特点:

  • 多个redis节点网络互联,数据共享,去中心化,连接哪个节点都可以获取和设置数据。
  • 所有的节点都是一主一从(也可以是一主多从),其中从不提供服务,仅作为备用
  • 不支持同时处理多个key(如MSET/MGET),因为redis需要把key均匀分布在各个节点上, 并发量很高的情况下同时创建key-value会降低性能并导致不可预测的行为
  • 支持在线增加、删除节点
  • 客户端可以连接任何一个主节点进行读写

**哈希槽:**Redis 集群没有使用一致性 hash,而是引入了哈希槽的概念, Redis 集群有 16384 个哈希槽,每个 key 通过 CRC16 校验后对 16384 取模来决定放置哪个槽,集群的每个节点负责一部分hash 槽。

环境搭建

三台机器,分别开启两个redis服务(端口)

192.168.30.128              端口:7001,7002
192.168.30.129              端口:7003,7004
192.168.30.130              端口:7005,7006

修改配置文件:

192.168.30.128

mkdir /usr/local/redis/cluster
cp /usr/local/redis/redis.conf /usr/local/redis/cluster/redis_7001.conf
cp /usr/local/redis/redis.conf /usr/local/redis/cluster/redis_7002.conf
chown -R redis:redis /usr/local/redis
mkdir -p /data/redis/cluster/{redis_7001,redis_7002} && chown -R redis:redis /data/redis
# vim /usr/local/redis/cluster/redis_7001.conf

bind 192.168.30.128
port 7001
daemonize yes
pidfile "/var/run/redis_7001.pid"
logfile "/usr/local/redis/cluster/redis_7001.log"
dir "/data/redis/cluster/redis_7001"
#replicaof 192.168.30.129 6379
masterauth 123456
requirepass 123456
appendonly yes
cluster-enabled yes
cluster-config-file nodes_7001.conf
cluster-node-timeout 15000
# vim /usr/local/redis/cluster/redis_7002.conf

bind 192.168.30.128
port 7002
daemonize yes
pidfile "/var/run/redis_7002.pid"
logfile "/usr/local/redis/cluster/redis_7002.log"
dir "/data/redis/cluster/redis_7002"
#replicaof 192.168.30.129 6379
masterauth "123456"
requirepass "123456"
appendonly yes
cluster-enabled yes
cluster-config-file nodes_7002.conf
cluster-node-timeout 15000

其它两台机器配置与192.168.30.128一致

启动redis服务:

redis-server /usr/local/redis/cluster/redis_7001.conf
tail -f /usr/local/redis/cluster/redis_7001.log
redis-server /usr/local/redis/cluster/redis_7002.conf
tail -f /usr/local/redis/cluster/redis_7002.log

redis-cli -a 123456 --cluster create 192.168.30.128:7001 192.168.30.128:7002 192.168.30.129:7003 192.168.30.129:7004 192.168.30.130:7005 192.168.30.130:7006 --cluster-replicas 1

登录集群:

redis-cli -c -h 192.168.30.128 -p 7001 -a 123456                  # -c,使用集群方式登录

集群操作

# 查看集群信息
CLUSTER INFO                
# 集群中增加节点
CLUSTER MEET 192.168.30.129 7007
# 删除节点
CLUSTER FORGET 1a1c7f02fce87530bd5abdfc98df1cffce4f1767
# 更换节点身份
# 将新增的192.168.30.130:7008节点身份改为192.168.30.129:7007的slave
redis-cli -c -h 192.168.30.130 -p 7008 -a 123456 cluster replicate e51ab166bc0f33026887bcf8eba0dff3d5b0bf14
# 保存配置,将节点配置信息保存到硬盘
CLUSTER SAVECONFIG

🌁 缓存穿透和雪崩(高可用)

Redis缓存的使用,极大的提升了应用程序的性能和效率,特别是数据查询方面。

但同时,它也带来了一些问题。其中,最要害的问题,就是数据的一致性问题,从严格意义上讲,这个问题无解。如果对数据的一致性要求很高,那么就不能使用缓存。

另外的一些典型问题就是,缓存穿透、缓存雪崩和缓存击穿。

缓存穿透(查不到)

当用户很多的时候,缓存都没有命中,于是都去请求了持久层数据库。这会给持久层数据库造成很大的压力,这时候就出现了缓存穿透。

解决方案

Ⅰ 布隆过滤器

布隆过滤器是一种数据结构,对所有可能查询的参数以 hash 形式存储,在控制层先进行校验,不符合则丢弃,从而避免了对底层存储系统的查询压力

Ⅱ 缓存空对象

当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源

但是这种方法会存在两个问题:

  • 如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键;

  • 即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。

缓存击穿(查的太多)

缓存击穿,是指一个 key 非常热点,在不停的扛着大并发,大并发集中。对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,导使数据库瞬间压力过大。

解决方案

Ⅰ 设置热点数据永不过期

从缓存层面来看,不设置过期时间,就不会出现热点 key 过期后产生的问题。

Ⅱ 加互斥锁

使用分布式锁,保证对于每个 key 同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可。这种方式将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大。

缓存雪崩

缓存雪崩,是指在某一个时间段,缓存集中过期失效,Redis 宕机。

其实缓存集中过期,倒不是非常致命,比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网。因为自然形成的缓存雪崩,一定是在某个时间段集中创建缓存,这个时候,数据库也是可以顶住压力的。无非就是对数据库产生周期性的压力而已。而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知的,很有可能瞬间就把数据库压垮。

解决方案

Ⅰ Redis 高可用

多增设几台 redis,搭建redis集群(异地多活)。

Ⅱ 限流降级

在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个 key 只允许一个线程查询数据和写缓存,其他线程等待。

Ⅲ 数据预热

数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值