redis笔记(入门到精通)

Redis

Redis文档中心 – Redis中国用户组(CRUG)

Redis英文官方

0、准备工作

0.1安装redis

0.1.1 centos下安装redis
1. yum install wget
2  ce ~
3   mkdir soft
4   cd soft
5   wget   http://download.redis.io/releases/reis-5.0.5.tar.gz
6.  tar xf redis...tar.gz
7.  cd reis-src
8.  看READEM.md
9.  make     #编译源码
	yum install gcc   # 安装c编译器   
    make  distclean   # 清理编译缓存
10. make
11  cd  src 生成可执行文件
12  cd...  make install PREFIX=/opt/xxx/redis5 #安装到指定位置
13. vi /etc/.profile  
    .... export REDIS_HOME = /opt/xxx/redis5
    .... export PATH = $PATH:$REDIS_HOME/bin
14  cd utils 
16 ./install-server.sh   #安装配置服务,一个物理机可以通过端口安装多个redis实例,可执行程序就1份,但是每个实例的配置文件是互相区别的
17. service redis_6379 start/stop/stauts  #linux 服务  在/etc/init.d/****
0.1.2 ubuntu下安装redis6
#卸载Gcc
sudo apt-get remove gcc
#安装GCC10
sudo add-apt-repository ppa:ubuntu-toolchain-r/ppa
sudo apt update
sudo apt install gcc
#sudo apt install gcc-10 g++-10 
#sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 60 --slave /usr/bin/g++ g++ /usr/bin/g++-10
#安装wget,用于下载redis
sudo apt-get install wget
#去redis官方复制redis6的下载url,  wget ${url}
sudo apt-get install pkg-config
# 指定目录编译安装
make PREFIX=/usr/local/soft/redis6 install
#环境变量配置 vi /etc/profile
export REDIS_HOME=/usr/local/soft/redis6
export PATH=$PATH:$REDIS_HOME/bin
# source /etc/profile
# 安装服务
root@redis-master:~/soft/redis-6.0.6/utils# ./install_server.sh

#失败必备命令
make distclean   //清理编译的临时文件
sudo apt-get remove gcc


#install_server安装目录介绍
/etc/init.d/redis_6379 启动脚本位置
/etc/redis/    配置文件路径

0.2 Value类型

string,list,set,sortedSet,hash.

简单使用

select 8 切换库8

# 看key的类型
redis-master:0>STRLEN k1
"16"
redis-master:0>
0.2.1 字符串

字符串(可以表示字符串、整数、位图)

redis与外界交互的时候,io流是字节流

set k1  ooxx  nx    #不存在时才可以设置,用法分布式锁
set k2  ooxx  xx    #只能更新
已连接。
redis-master:0>set k1 test xx
null
redis-master:0>get k2
null
redis-master:0>mset  k3 a k4 b  #设置多个
"OK"
redis-master:0>get k3 
"a"
redis-master:0>get k4
"b"
redis-master:0>mget k3 k4  # 获取多个值
 1)  "a"
 2)  "b"
redis-master:0>get k1
null
redis-master:0>set k1  ' i love'
"OK"
redis-master:0>get k1
" i love"
redis-master:0>append k1 '  chaina'   #追加字符串
"15"
redis-master:0>get k1
" i love  chaina"
 # 取某个范围,正向索引,反向索引
redis-master:0>get k1
"helloworld"
redis-master:0>GETRANGE k1 3 -2 
"loworl"
# 覆盖字符串
redis-master:0>SETRANGE k1  6 luxiaomeng
"16"
redis-master:0>get k1
"hellowluxiaomeng"
# 获取字符串的长度
redis-master:0>STRLEN k1
"16"
redis-master:0>


# string中面向数值的操作
redis-master:0>type k1
"string"
redis-master:0>set k1 1212312
"OK"
redis-master:0>object encoding k1 #返回给定 key 锁储存的值所使用的内部表示(representation)。
"int"
redis-master:0>type k1
"string"
redis-master:0>


redis-master:0>type k1
"string"
redis-master:0>set k1 1212312
"OK"
redis-master:0>object encoding k1
"int"
redis-master:0>type k1
"string"
redis-master:0>INCR k1
"1212313"
redis-master:0>incrby k1 22
"1212335"
redis-master:0>decr k1
"1212334"
redis-master:0>decrby k1 22
"1212312"
redis-master:0>incrbyfloat k1 0.5
"1212312.5"
redis-master:0>

#bitmap
setbit k1 1 1
bitop or orkey k1 k2   #或操作

0.2.2 list

有序,存储弹出顺序。


#list  可以模拟栈和队列
redis-master:0>FLUSHALL   # 清空所有key
"OK"
redis-master:0>lpush k1 a b c d e f  #从左侧放入
"6"
redis-master:0>rpush k2 a b c d e f #从右侧放入
"6"
redis-master:0>lpop k1   #从左侧弹出
"f"
redis-master:0>lpop k1 
"e"
redis-master:0>lpop k1 
"d"
redis-master:0>rpop k2  #从右侧弹出
"f"
redis-master:0>LRANGE k1  0   -1  #查看list中所有元素
 1)  "c"
 2)  "b"
 3)  "a"
redis-master:0>LINDEX  k1  2   # 根据索引取出元素
"a"
redis-master:0>LINDEX k1 -1      #取出最后一个
"a"
redis-master:0>Lset k1 2 a1   # 根据索引设置值
"OK"
redis-master:0>LRANGE k1 0 -1 
 1)  "c"
 2)  "b"
 3)  "a1"
redis-master:0>
redis-master:0>LRANGE k1 0 -1
 1)  "c"
 2)  "b"
 3)  "a1"
redis-master:0>lrem k1  1 a1  # 移除   linsert 可以指定位置插入
"1"
redis-master:0>LRANGE k1 0 -1
 1)  "c"
 2)  "b"
 redis-master:0>llen k1  # 元素的个数
"2"
redis-master:0>
0.2.3 hash
redis-master:0>hset k1  name  lxm
"1"
redis-master:0>hmset k1 name lxm  age 18 
"OK"
redis-master:0>hget k1 name
"lxm"
redis-master:0>hmget k1   name age
 1)  "lxm"
 2)  "18"
redis-master:0>hkeys k1
 1)  "name"
 2)  "age"
redis-master:0>hvals k1
 1)  "lxm"
 2)  "18"
redis-master:0>
0.2.4 set

去重、无序集合

redis-master:0>FLUSHDB
"OK"
redis-master:0>sadd k1 a1 a2 a3
"3"
redis-master:0>smembers k1
 1)  "a3"
 2)  "a2"
 3)  "a1"
 redis-master:0>srem k1 a1   #移除
"1"
redis-master:0>smembers k1
 1)  "a3"
 2)  "a2"
redis-master:0>
redis-master:0>smembers k1
 1)  "a3"
 2)  "a2"
 
 # 集合交集
redis-master:0>sadd k2 a1 a3 a2 a4
"4"
redis-master:0>sinterstore k3  k1 k2
"2"
redis-master:0>smembers k3
 1)  "a2"
 2)  "a3"
redis-master:0>sinter k1 k2
 1)  "a3"
 2)  "a2"
redis-master:0>

# 并集 sunion sunionstore

redis-master:0>sunion k1 k2
 1)  "a4"
 2)  "a2"
 3)  "a3"
 4)  "a1"
redis-master:0>

# 差集 SDIFF



# 随机事件 SRANDMEMBER
redis-master:0>SRANDMEMBER k1 5
 1)  "a2"
 2)  "a3"
redis-master:0>SRANDMEMBER k1 -5
 1)  "a3"
 2)  "a3"
 3)  "a2"
 4)  "a3"
 5)  "a2"
redis-master:0>smembers k1
 1)  "a3"
 2)  "a2"
redis-master:0>SPOP k1   随机弹出

0.2.5 sorted_set

对元素排序的顺序,物理内存从小到大,也有交并差操作(需考虑分值)

利用场景歌曲排行榜

redis-master:0>FLUSHDB
"OK"
redis-master:0>zadd k1 8 apple 2 banana 3 orange
"3"
redis-master:0>zrange k1 0 -1
 1)  "banana"
 2)  "orange"
 3)  "apple"
redis-master:0>zrange k1 0 -1 withscores
 1)  "banana"
 2)  "2"
 3)  "orange"
 4)  "3"
 5)  "apple"
 6)  "8"
redis-master:0>zrangebyscore k1 3 8
 1)  "orange"
 2)  "apple"
redis-master:0>zrange k1 0 1
 1)  "banana"
 2)  "orange"
redis-master:0>zrevrange k1 0 1
 1)  "apple"
 2)  "orange"
redis-master:0>zrange k1 -2 -1
 1)  "orange"
 2)  "apple"
redis-master:0>zscore k1 apple
"8"
redis-master:0>zrank k1 apple
"2"
redis-master:0>zrange k1 0 -1 withscores
 1)  "banana"
 2)  "2"
 3)  "orange"
 4)  "3"
 5)  "apple"
 6)  "8"
redis-master:0>zincrby k1 2.5 banana
"4.5"
redis-master:0>zrange k1 0 -1 withscores
 1)  "orange"
 2)  "3"
 3)  "banana"
 4)  "4.5"
 5)  "apple"
 6)  "8"
redis-master:0>

#交并差集合案例 ZUNIONSTORE
redis-master:0>zadd k2 4.1 orange 5.1 pear 6.2 watermelon 
"3"
redis-master:0>zrange k1 0 -1 withscores
 1)  "orange"
 2)  "3"
 3)  "banana"
 4)  "4.5"
 5)  "apple"
 6)  "8"
redis-master:0>ZUNIONSTORE unkey   2 k1 k2   weights 1 0.5
"5"
redis-master:0>zrange unkey 0 -1 withscores
 1)  "pear"
 2)  "2.5499999999999998"
 3)  "watermelon"
 4)  "3.1000000000000001"
 5)  "banana"
 6)  "4.5"
 7)  "orange"
 8)  "5.0499999999999998"
 9)  "apple"
 10)  "8"
redis-master:0>zrange unkey 0 -1 withscores
 1)  "orange"
 2)  "4.0999999999999996"
 3)  "banana"
 4)  "4.5"
 5)  "pear"
 6)  "5.0999999999999996"
 7)  "watermelon"
 8)  "6.2000000000000002"
 9)  "apple"
 10)  "8"
redis-master:0>

一、缓存通识

​ 为了协调两者数据传输速度差异的结构,一般称为缓存。

1.1 缓存类型

缓存的类型分为:本地缓存、分布式缓存和多级缓存。

1.1.1本地缓存

​ 就是在本地主机进程的内存中进行缓存,比如JVM堆。实现本地缓存可以使用apache common-collections框架中的LRUMAP,也可以使用Ehcache这样的工具来实现。

​ 本地缓存是内存访问,没有远程交互开销,性能最好,但是受限于单机容量,一般缓存较小且无法扩展。

1.1.2 分布式缓存

​ 分布式缓存可以很好得解决这个问题。分布式缓存一般都具有良好的水平拓展能力,对较大数据量的场景也能应付自如。缺点就是需要进行远程请求,性能不如本地缓存。

1.1.3 多级缓存

​ 为了平衡这种情况,实际业务中一般采用多级缓存,本地缓存只保存访问评率最高的部分热点数据,其他的热点数据放在分布式缓存中。

1.2 淘汰策略

redis 的有效期不会随着时间延长,发生写会剔除过期时间

ttl k1   # 查看有效时间
EXPIRE K1  50   # 设置有效期
EXPIREAT K1 timestamp   # 根据在某一时间点 失效

redis-master:0>set k1 2  ex 2  # 设置值并设置有效期
"OK"
redis-master:0>get  k1
null
redis-master:0>EXISTS k1  # 判断key是否存在
"0"
redis-master:0>set k1   1
"OK"
redis-master:0>exists k1
"1"
redis-master:0>

不管是本地缓存还是分布式缓存,为了保证较高的性能。都是使用内存来保证数据,由于成本和内存限制,当存储的数据超过缓存容量时,需要对缓存的数据进行剔除。

一般的剔除策略有

  • FIFO:淘汰最早数据
  • LRU:剔除最近最少使用
  • LFU:剔除最近使用频率最低的数据。
如何配置Redis淘汰策略
  1. 找到redis.conf文件

设置Redis 内存大小的限制,我们可以设置maxmemory ,当数据达到限定大小后,会选择配置的策略淘汰数据 比如:maxmemory 300mb。

  1. 设置内存淘汰具体使用那种策略

设置Redis的淘汰策略。比如:maxmemory-policy volatile-lru

Redis keys过期有两种方式:被动和主动方式。

当一些客户端尝试访问它时,key会被发现并主动的过期。

当然,这样是不够的,因为有些过期的keys,永远不会访问他们。 无论如何,这些keys应该过期,所以定时随机测试设置keys的过期时间。所有这些过期的keys将会从密钥空间删除。

具体就是Redis每秒10次做的事情:

  1. 测试随机的20个keys进行相关过期检测。
  2. 删除所有已经过期的keys。
  3. 如果有多于25%的keys过期,重复步奏1.

这是一个平凡的概率算法,基本上的假设是,我们的样本是这个密钥控件,并且我们不断重复过期检测,直到过期的keys的百分百低于25%,这意味着,在任何给定的时刻,最多会清除1/4的过期keys。

二、缓存技术

2.1 MemCache

​ memcache是一套分布式的高速缓存系统,由LiveJournal的Brad Fitzpatrick开发,但目前被许多网站使用以提升网站的访问速度,尤其对于一些大型的、需要频繁访问数据库的网站访问速度提升效果十分显著 [1] 。这是一套开放源代码软件,以BSD license授权发布。

特点:

  • MC 处理请求时使用多线程异步 IO 的方式,可以合理利用 CPU 多核的优势,性能非常优秀;
  • MC 功能简单,使用内存存储数据
  • MC 对缓存的数据可以设置失效期,过期后的数据会被清除;
  • 失效的策略采用延迟失效,就是当再次使用数据时检查是否失效;
  • 当容量存满时,会对缓存中的数据进行剔除,剔除时,除了会对过期 key 进行清理,还会按 LRU 策略对数据进行剔除。

MC 有一些限制,这些限制在现在的互联网场景下很致命,成为大家选择Redis、MongoDB的重要原因:

  • key 不能超过 250 个字节;
  • value 不能超过 1M 字节;
  • 只支持 K-V 结构,不提供持久化和主从同步功能。

2.2 Redis

​ redis是一个key-value存储系统。和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)和hash(哈希类型)。这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。在此基础上,redis支持各种不同方式的排序。与memcached一样,为了保证效率,数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。

与MemCache主要区别为:

  • Redis 采用单线程模式处理请求。这样做的原因有 2 个:一个是因为采用了非阻 塞的异步事件处理机制;另一个是缓存数据都是内存操作 IO 时间不会太长,单线程可以避免线程 上下文切换产生的代价。
  • Redis 支持持久化,所以 Redis 不仅仅可以用作缓存,也可以用作 NoSQL 数据库。
  • 相比 MC,Redis 还有一个非常大的优势,就是除了 K-V 之外,还支持多种数据格式,例如 list、 set、sorted set、hash 等。

Redis6.0 引入了多线程

主流使用2.8 3.0 3.2 4.0 5.0

2.2.1 为什么一开始选择单线程模型(单线程的好处)?

(1)IO多路复用

io多路复用

​ 操作系统为你提供了一个功能,当你的某个socket可读或者可写的时候,它可以给你一个通知。这样当配合非阻塞的socket使用时,只有当系统通知我哪个描述符可读了,我才去执行read操作,可以保证每次read都能读到有效数据而不做纯返回-1和EAGAIN的无用功。写操作类似。操作系统的这个功能通过select/poll/epoll/kqueue之类的系统调用函数来使用,这些函数都可以同时监视多个描述符的读写就绪状况,这样,多个描述符的I/O操作都能在一个线程内并发交替地顺序完成,这就叫I/O多路复用,这里的“复用”指的是复用同一个线程。

​ FD是一个文件描述符,意思是表示当前文件处于可读、可写还是异常状态。使用 I/O 多路复用机制同时 监听多个文件描述符的可读和可写状态。你可以理解为具有了多线程的特点。 一旦受到网络请求就会在内存中快速处理,由于绝大多数的操作都是纯内存的,所以处理的速度会非常 地快。也就是说在单线程模式下,即使连接的网络处理很多,因为有IO多路复用,依然可以在高速的内 存处理中得到忽略。

(2)可维护性高

​ 多线程模型虽然在某些方面表现优异,但是它却引入了程序执行顺序的不确定性,带来了并发读写的一系列问题。单线程模式下,可以方便地进行调试和测试。

(3)基于内存,单线程状态下效率依旧高

​ 多线程能够充分利用CPU的资源,但对于Redis来说,由于基于内存速度那是相当的高,能达到在一秒内处理10万个用户请求,如果一秒十万还不能满足,那我们就可以使用Redis分片的技术来交给不同的 Redis服务器。这样的做法避免了在同一个 Redis 服务中引入大量的多线程操作。 而且基于内存,除非是要进行AOF备份,否则基本上不会涉及任何的磁盘 I/O 操作。这些数据的读写由于只发生在内存中,所以处理速度是非常快的;用多线程模型处理全部的外部请求可能不是一个好的方案。

总结成两句话,基于内存而且使用多路复用技术,单线程速度很快,又保证了多线程的特点。因为没有 必要使用多线程。

2.2.2 为什么Redis在6.0之后加入了多线程?
  1. 因为读写网络的read/write系统调用在Redis执行期间占用了大部分CPU时间,如果把网络读写做成多线程的方式对性能会有很大提升。
  2. 用del命令删除一个元素,如果这个元素非常大,可能占据了几十兆或者是几百兆,那么 在短时间内是不能完成的,这样一来就需要多线程的异步支持。

总结:

​ Redis 选择使用单线程模型处理客户端的请求主要还是因为 CPU 不是 Redis 服务器的瓶颈,所以使用多线程模型带来的性能提升并不能抵消它带来的开发成本和维护成本,系统的性能瓶颈也主要在网络 I/O 操作上;而 Redis 引入多线程操作也是出于性能上的考虑,对于一些大键值对的删除操作,通 过多线程非阻塞地释放内存空间也能减少对 Redis 主线程阻塞的时间,提高执行的效率

三、Redis内存模型

1. Redis内存统计

127.0.0.1:6379>  info memory 
#Redis分配的内存总量,包括虚拟内存(字节) 
used_memory:853464 
#占操作系统的内存,不包括虚拟内存(字节) 
used_memory_rss:12247040 
#内存碎片比例 如果小于1说明使用了虚拟内存  used_memory_rss/used_memory    标准1~1.5健康
mem_fragmentation_ratio:15.07       
#Redis使用的内存分配器
mem_allocator:jemalloc-5.1.0

参数说明:

  • used_memory:由Redis内存分配器分配的数据内存和缓冲内存的内存总量(单位是字节),包括使用的虚拟内存(即 swap);
  • used_memory_rss:记录的是由操作系统分配的Redis进程内存和Redis内存中无法再被jemalloc分配的内存碎片(单位是字节)。
  • mem_allocator:Redis使用的内存分配器,在编译时指定;可以是 libc 、jemalloc或者tcmalloc,默认是jemalloc;

2. Redis内存分配

Redis是单进程单线程的,那么它占用的内存中都有哪些东西呢?

内存占用

数据

​ 作为数据库,数据是主要的部分;这部分占用的内存会统计在 used_memory 中。
​ Redis 使用键值对存储数据,其中的值(对象)包括 5 种类型,即字符串、哈希、列表、集合、有序集合。
​ 这 5 种类型是 Redis 对外提供的,实际上,在 Redis 内部,每种类型可能有 2 种或更多的内部编码实现。

进程

​ Redis 主进程本身运行肯定需要占用内存,如代码、常量池等等;这部分内存大约几M,在大多数生产 环境中与 Redis 数据占用的内存相比可以忽略。 这部分内存不是由 jemalloc 分配,因此不会统计在 used_memory 中。
补充说明:除了主进程外,Redis 创建的子进程运行也会占用内存,如 Redis 执行 AOF、RDB 重写时创 建的子进程。
当然,这部分内存不属于 Redis 进程,也不会统计在 used_memory 和 used_memory_rss 中。

缓冲内存

​ 缓冲内存包括客户端缓冲区、复制积压缓冲区、AOF 缓冲区等;其中,客户端缓冲区存储客户端连接的 输入输出缓冲;复制积压缓冲区用于部分复制功能;AOF 缓冲区用于在进行 AOF 重写时,保存近的 写入命令。
在了解相应功能之前,不需要知道这些缓冲的细节;这部分内存由 jemalloc 分配,因此会统计在 used_memory 中。

内存碎片

​ 内存碎片是 Redis 在分配、回收物理内存过程中产生的。例如,如果对数据的更改频繁,而且数据之间 的大小相差很大,可能导致 Redis 释放的空间在物理内存中并没有释放。 但 Redis 又无法有效利用,这就形成了内存碎片,内存碎片不会统计在 used_memory 中。
内存碎片的产生与对数据进行的操作、数据的特点等都有关;此外,与使用的内存分配器也有关系:如 果内存分配器设计合理,可以尽可能的减少内存碎片的产生。如果 Redis 服务器中的内存碎片已经很 大,可以通过安全重启的方式减小内存碎片:因为重启之后,Redis 重新从备份文件中读取数据,在内 存中进行重排,为每个数据重新选择合适的内存单元,减小内存碎片。

3.Redis数据存储的细节

Redi是一个K-V Nosql数据库,五种类型都是针对K-V的V的。

下面是执行set hello world时,所涉及到的数据模型

数据存储的细节

  1. dictEntry:Redis是Key-Value数据库,因此对每个键值对都会有一个dictEntry,里面存储了指向Key和Value的指针;next指向下一个dictEntry,与本Key-Value无关。
  2. Key:图中右上角可见,Key(”hello”)并不是直接以字符串存储,而是存储在SDS结构中。
  3. redisObject:Value(“world”)既不是直接以字符串存储,也不是像Key一样直接存储在SDS中,而 是存储在redisObject中。实际上,不论Value是5种类型的哪一种,都是通过redisObject来存储的;而 redisObject中的type字段指明了Value对象的类型,ptr字段则指向对象所在的地址。不过可以看出,字 符串对象虽然经过了redisObject的包装,但仍然需要通过SDS存储。实际上,redisObject除了type和ptr字段以外,还有其他字段图中没有给出,如用于指定对象内部编码的字段;后面会详细介绍.
  4. jemalloc:无论是DictEntry对象,还是redisObject、SDS对象,都需要内存分配器(如 jemalloc)分配内存进行存储。
Jemalloc

​ jemalloc作为Redis的默认内存分配器,在减小内存碎片方面做的相对比较好。jemalloc在64位系统 中,将内存空间划分为小、大、巨大三个范围;每个范围内又划分了许多小的内存块单位;当Redis存 储数据时,会选择大小最合适的内存块进行存储。

​ jemalloc划分的内存单元如下图所示:

jemalloc

RedisObject

​ Redis对象有5种类型;无论是哪种类型,Redis都不会直接存储,而是通过redisObject对象进行存储。 redisObject对象非常重要,Redis对象的类型、内部编码、内存回收、共享对象等功能,都需要 redisObject支持,下面将通过redisObject的结构来说明它是如何起作用的。 Redis中的每个对象都是由如下结构表示(列出了与保存数据有关的三个属性)

{
	unsigned type:4;//类型 五种对象类型
	unsigned encoding:4;//编码
	void *ptr;//指向底层实现数据结构的指针
	//...
	int refcount;//引用计数
	//...
	unsigned lru:24;//记录最后一次被命令程序访问的时间
	//...
}robj;

(1) type

type字段表示对象的类型,占4个比特;目前包括REDIS_STRING(字符串)、REDIS_LIST (列表)、 REDIS_HASH(哈希)、REDIS_SET(集合)、REDIS_ZSET(有序集合)。

当我们执行type命令时,便是通过读取RedisObject的type字段获得对象的类型;

(2)encoding

​ encoding表示对象的内部编码,占4个比特。 对于Redis支持的每种类型,都有至少两种内部编码,例如对于字符串,有int、embstr、raw三种编码。

​ 通过encoding属性,Redis可以根据不同的使用场景来为对象设置不同的编码,大大提高了Redis 的灵活性和效率。以列表对象为例,有压缩列表和双端链表两种编码方式;如果列表中的元素较少, Redis倾向于使用压缩列表进行存储,因为压缩列表占用内存更少,而且比双端链表可以更快载入;当列表对象元素较多时,压缩列表就会转化为更适合存储大量元素的双端链表。 通过object encoding命令,可以查看对象采用的编码方式

(3) lru

​ lru记录的是对象最后一次被命令程序访问的时间,占据的比特数不同的版本有所不同(2.6版本占22比 特,4.0版本占24比特)。 通过对比lru时间与当前时间,可以计算某个对象的闲置时间;object idletime命令可以显示该闲置时间 (单位是秒)。object idletime命令的一个特殊之处在于它不改变对象的lru值。

​ lru值除了通过object idletime命令打印之外,还与Redis的内存回收有关系:如果Redis打开了 maxmemory选项,且内存回收算法选择的是volatile-lru或allkeys—lru,那么当Redis内存占用 超过maxmemory指定的值时,Redis会优先选择空转时间最长的对象进行释放。

(4) refcount

​ refcount与共享对象。refcount记录的是该对象被引用的次数,类型为整型。refcount的作用,主要在于对象的引用计数和内存回收。

​ 当创建新对象时,refcount初始化为1;当有新程序使用该对象时,refcount加1;当对象不再被 一个新程序使用时,refcount减1;当refcount变为0时,对象占用的内存会被释放。

​ Redis中被多次使用的对象(refcount>1),称为共享对象。Redis为了节省内存,当有一些对象重复出现 时,新的程序不会创建新的对象,而是仍然使用原来的对象。这个被重复使用的对象,就是共享对象。 目前共享对象仅支持整数值的字符串对象。

​ 共享对象的具体实现。 Redis的共享对象目前只支持整数值的字符串对象。之所以如此,实际上是对内存和CPU(时间)的平 衡:共享对象虽然会降低内存消耗,但是判断两个对象是否相等却需要消耗额外的时间。对于整数值, 判断操作复杂度为O(1);对于普通字符串,判断复杂度为O(n);而对于哈希、列表、集合和有序集合, 判断的复杂度为O(n^2)。 虽然共享对象只能是整数值的字符串对象,但是5种类型都可能使用共享对象(如哈希、列表等的元素 可以使用)。

​ 就目前的实现来说,Redis服务器在初始化时,会创建10000个字符串对象,值分别是0~9999的整数 值;当Redis需要使用值为0~9999的字符串对象时,可以直接使用这些共享对象。10000这个数字可以 通过调整参数

​ REDIS_SHARED_INTEGERS(4.0中是OBJ_SHARED_INTEGERS)的值进行改变。 共享对象的引用次数可以通过object refcount命令查看

(5)ptr

​ ptr指针指向具体的数据,如前面的例子中,set hello world,ptr指向包含字符串world的SDS。

(6) 总结

综上所述,redisObject的结构与对象类型、编码、内存回收、共享对象都有关系;一个redisObject对 象的大小为16字节:

4bit+4bit+24bit+4Byte+8Byte=16Byte
SDS

​ Redis没有直接使用C字符串(即以空字符’\0’结尾的字符数组)作为默认的字符串表示,而是使用了 SDS。SDS是简单动态字符串(Simple Dynamic String)的缩写。

redis3.2之前

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

​ 其中,buf表示字节数组,用来存储字符串;len表示buf已使用的长度,free表示buf未使用的长度。下面是两个例子。

SDS

​ 通过SDS的结构可以看出,buf数组的长度=free+len+1(其中1表示字符串结尾的空字符);所以,一 个SDS结构占据的空间为:free所占长度+len所占长度+ buf数组的长度 =4+4+free+len+1=free+len+9。

3.2之后

typedef char *sds;
struct __attribute__ ((__packed__)) sdshdr5 { // 对应的字符串长度小于 1<<5
	unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
	char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 { // 对应的字符串长度小于 1<<8
	uint8_t len; /* used */ //目前字符创的长度 用1字节存储
	uint8_t alloc; //已经分配的总长度 用1字节存储
	unsigned char flags; //flag用3bit来标明类型,类型m后续解释,其余5bit目前没有使用
	char buf[]; //柔性数组,以'\0'结尾
};
struct __attribute__ ((__packed__)) sdshdr16 { // 对应的字符串长度小于 1<<16
	uint32_t len; /*已使用长度,用2字节存储*/
	uint32_t alloc; /* 总长度,用2字节存储*/
	unsigned char flags; /* 3 lsb of type, 5 unused bits */
	char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 { // 对应的字符串长度小于 1<<32
	uint32_t len; /*已使用长度,用4字节存储*/
	uint32_t alloc; /* 总长度,用4字节存储*/
	unsigned char flags;/* 低3位存储类型, 高5位预留 */
	char buf[];/*柔性数组,存放实际内容*/
};
struct __attribute__ ((__packed__)) sdshdr64 { // 对应的字符串长度小于 1<<64
	uint64_t len; /*已使用长度,用8字节存储*/
	uint64_t alloc; /* 总长度,用8字节存储*/
	unsigned char flags; /* 低3位存储类型, 高5位预留 */
	char buf[];/*柔性数组,存放实际内容*/
};

SDS与C字符串的比较

  • 获取字符串长度:SDS是O(1),C字符串是O(n)
  • 缓冲区溢出:使用C字符串的API时,如果字符串长度增加(如strcat操作)而忘记重新分配内存, 很容易造成缓冲区的溢出;而SDS由于记录了长度,相应的API在可能造成缓冲区溢出时会自动重 新分配内存,杜绝了缓冲区溢出。
  • 修改字符串时内存的重分配:对于C字符串,如果要修改字符串,必须要重新分配内存(先释放再 申请),因为如果没有重新分配,字符串长度增大时会造成内存缓冲区溢出,字符串长度减小时会 造成内存泄露。而对于SDS,由于可以记录len和free,因此解除了字符串长度和空间数组长度之 间的关联,可以在此基础上进行优化:空间预分配策略(即分配内存时比实际需要的多)使得字符 串长度增大时重新分配内存的概率大大减小;惰性空间释放策略使得字符串长度减小时重新分配内 存的概率大大减小。
  • 存取二进制数据:SDS可以,C字符串不可以。因为C字符串以空字符作为字符串结束的标识,而 对于一些二进制文件(如图片等),内容可能包括空字符串,因此C字符串无法正确存取;而SDS 以字符串长度len来作为字符串结束标识,因此没有这个问题。

此外,由于SDS中的buf仍然使用了C字符串(即以’\0’结尾),因此SDS可以使用C字符串库中的部分函 数;但是需要注意的是,只有当SDS用来存储文本数据时才可以这样使用,在存储二进制数据时则不行 (’\0’不一定是结尾)。

四、Redis的对象类型和内部编码

​ Redis支持5种对象类型,而每种结构都有至少两种编码;

这样的好处在于:一方面接口与实现分离,当需要增加或改变内部编码时,用户使用不受影响; 另一方面可以根据不同的应用场景切换内部编码,提高效率。

redis内部编码

4.1.字符串(SDS)

4.1.1概况

​ 字符串是最基础的类型,因为所有的键都是字符串类型,且字符串之外的其他几种复杂类型的元素也是字符串。字符串长度不能超过512MB。

4.1.2 内部编码

字符串类型的内部编码有3种,它们的应用场景如下

  • int:8个字节的长整型。字符串值是整型时,这个值使用long整型表示。
  • embstr:<=44字节的字符串。embstr与raw都使用redisObject和sds保存数据,区别在于, embstr的使用只分配一次内存空间(因此redisObject和sds是连续的),而raw需要分配两次内存 空间(分别为redisObject和sds分配空间)。因此与raw相比,embstr的好处在于创建时少分配一 次空间,删除时少释放一次空间,以及对象的所有数据连在一起,寻找方便。而embstr的坏处也 很明显,如果字符串的长度增加需要重新分配内存时,整个redisObject和sds都需要重新分配空 间,因此redis中的embstr实现为只读。
  • raw:大于44个字节的字符串。

3.2之后 embstr和raw进行区分的长度,是44;是因为redisObject的长度是16字节,sds的长度 是4+字符串长度;因此当字符串长度是44时,embstr的长度正好是16+4+44 =64,jemalloc正好 可以分配64字节的内存单元。 3.2 之前embstr和raw进行区分的长度,是39,因为redisObject的长度是16字节,sds的长度是 9+字符串长度;因此当字符串长度是44时,embstr的长度正好是16+9+39 =64,jemalloc正好可 以分配64字节的内存单元。

4.2 列表

4.2.1 概况

​ 列表(list)用来存储多个有序的字符串,每个字符串称为元素; 一个列表可以存储2^32-1个元素。 Redis中的列表支持两端插入和弹出,并可以获得指定位置(或范围)的元素,可以充当数组、队列、 栈等。

4.2.2 内部编码

​ Redis3.0之前列表的内部编码可以是压缩列表(ziplist)或双端链表(linkedlist)。选择的折中方案 是两种数据类型的转换,但是在3.2版本之后 因为转换也是个费时且复杂的操作,引入了一种新的数据 格式,结合了双向列表linkedlist和ziplist的特点,称之为quicklist。所有的节点都用quicklist存储,省去了到临界条件是的格式转换。

4.2.3 压缩列表

​ 压缩列表(ziplist)是列表键和哈希键的底层实现之一。当一个列表只包含少量列表项时,并且每个列表项是小整数值或短字符串,那么Redis会使用压缩列表来做该列表的底层实现。

	压缩列表(ziplist)是Redis为了节省内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构,一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值,放到一个连续内存区。

​ 压缩列表的每个节点构成如下:

压缩列表节点

  • previous_entry_enth: 记录压缩列表前一个字节的长度
  • encoding:节点的encoding保存的是节点的content的内容类型。
  • content:content区域用于保存节点的内容,节点内容类型和长度由encoding决定。
4.2.4 双向链表

双向链表:由一个list结构和多个listNode结构组成

典型结构如下图所示:

双向链表

通过图中可以看出,双端链表同时保存了表头指针和表尾指针,并且每个节点都有指向前和指向后的指针;链表中保存了列表的长度;dup、free和match为节点值设置类型特定函数,所以链表可以用于保 存各种不同类型的值。而链表中每个节点指向的是type为字符串的redisObject。

4.2.5 快速列表

​ 简单的说,我们仍旧可以将其看作一个双向列表,但是列表的每个节点都是一个ziplist,其实就是 linkedlist和ziplist的结合。quicklist中的每个节点ziplist都能够存储多个数据元素。 Redis3.2开始,列表采用quicklist进行编码。

​ 简单的说,我们仍旧可以将其看作一个双向列表,但是列表的每个节点都是一个ziplist,其实就是 linkedlist和ziplist的结合。quicklist中的每个节点ziplist都能够存储多个数据元素。 Redis3.2开始,列表采用quicklist进行编码。

快速列表

//32byte 的空间
typedef struct quicklist {
	// 指向quicklist的头部
	quicklistNode *head;
    // 指向quicklist的尾部
	quicklistNode *tail;
	// 列表中所有数据项的个数总和
	unsigned long count;
	// quicklist节点的个数,即ziplist的个数
	unsigned int len;
	// ziplist大小限定,由list-max-ziplist-size给定
	// 表示不用整个int存储fill,而是只用了其中的16位来存储
	int fill : 16;
	// 节点压缩深度设置,由list-compress-depth给定
	unsigned int compress : 16;
} quicklist;
typedef struct quicklistNode {
	struct quicklistNode *prev; // 指向上一个ziplist节点
	struct quicklistNode *next; // 指向下一个ziplist节点
	unsigned char *zl; // 数据指针,如果没有被压缩,就指向ziplist结构,反之指向quicklistLZF结构
	unsigned int sz; // 表示指向ziplist结构的总长度(内存占用长度)
	unsigned int count : 16; // 表示ziplist中的数据项个数
	unsigned int encoding : 2; // 编码方式,1--ziplist,2--	quicklistLZF
	unsigned int container : 2; // 预留字段,存放数据的方式,1--NONE,2--ziplist
	unsigned int recompress : 1; // 解压标记,当查看一个被压缩的数据时,需要暂时解压,标记此参数为1,之后再重新进行压缩
	unsigned int attempted_compress : 1; // 测试相关
	unsigned int extra : 10; // 扩展字段,暂时没用
} quicklistNode;

4.3 哈希

4.3.1 概况

​ 哈希(作为一种数据结构),不仅是redis对外提供的5种对象类型的一种(与字符串、列表、集合、有 序结合并列),也是Redis作为Key-Value数据库所使用的数据结构。为了说明的方便,后面当使用“内 层的哈希”时,代表的是redis对外提供的5种对象类型的一种;使用“外层的哈希”代指Redis作为KeyValue数据库所使用的数据结构。

4.3.2 内部编码

​ 内层的哈希使用的内部编码可以是压缩列表(ziplist)和哈希表(hashtable)两种;Redis的外层的哈希则只使用了hashtable。

压缩列表

压缩列表前面已介绍。与哈希表相比,压缩列表用于元素个数少、元素长度小的场景;其优势在 于集中存储,节省空间;同时,虽然对于元素的操作复杂度也由O(1)变为了O(n),但由于哈希中 元素数量较少,因此操作的时间并没有明显劣势。

4.3.3 哈希表

hashtable:一个hashtable由1个dict结构、2个dictht结构、1个dictEntry指针数组(称为bucket)和 多个dictEntry结构组成。

正常情况下(即hashtable没有进行rehash时)各部分关系如下图所示:

image-20210112141857756

dict

一般来说,通过使用dictht和dictEntry结构,便可以实现普通哈希表的功能;但是Redis的实现中,在 dictht结构的上层,还有一个dict结构。下面说明dict结构的定义及作用。

dict结构如下:

typedef struct dict{
	dictType *type; // type里面主要记录了一系列的函数,可以说是规定了一系列的接口
	void *privdata; // privdata保存了需要传递给那些类型特定函数的可选参数
//两张哈希表
	dictht ht[2];//便于渐进式rehash
	int trehashidx; //rehash 索引,并没有rehash时,值为 -1
	//目前正在运行的安全迭代器的数量
	int iterators;
} dict;

其中,type属性和privdata属性是为了适应不同类型的键值对,用于创建多态字典。

​ ht属性和trehashidx属性则用于rehash,即当哈希表需要扩展或收缩时使用。ht是一个包含两个项的数组,每项都指向一个dictht结构,这也是Redis的哈希会有1个dict、2个dictht结构的原因。通常情况 下,所有的数据都是存在放dict的ht[0]中,ht[1]只在rehash的时候使用。dict进行rehash操作的时候, 将ht[0]中的所有数据rehash到ht[1]中。然后将ht[1]赋值给ht[0],并清空ht[1]。

​ 因此,Redis中的哈希之所以在dictht和dictEntry结构之外还有一个dict结构,一方面是为了适应 不同类型的键值对,另一方面是为了rehash。

dictht

dictht结构如下:

typedef struct dictht{
	//哈希表数组,每个元素都是一条链表
	dictEntry **table;
	//哈希表大小
	unsigned long size;
	// 哈希表大小掩码,用于计算索引值
	// 总是等于 size - 1
	unsigned long sizemask;
	// 该哈希表已有节点的数量
	unsigned long used;
}dictht;

其中,各个属性的功能说明如下:

  • table属性是一个指针,指向bucket;
  • size属性记录了哈希表的大小,即bucket的大小;
  • used记录了已使用的dictEntry的数量;
  • sizemask属性的值总是为size-1,这个属性和哈希值一起决定一个键在table中存储的位置。

bucket

​ bucket是一个数组,数组的每个元素都是指向dictEntry结构的指针。redis中bucket数组的大小计算规 则如下:大于dictEntry的、最小的2^n;

​ 例如,如果有1000个dictEntry,那么bucket大小为1024;如果有1500个dictEntry,则bucket大 小为2048。

dictEntry

dictEntry结构用于保存键值对,结构定义如下:

// 键
typedef struct dictEntry{
	void *key;
	union{ //值v的类型可以是以下三种类型
	void *val;
	uint64_tu64;
	int64_ts64;
}v;
// 指向下个哈希表节点,形成链表
	struct dictEntry *next;
}dictEntry;

其中,各个属性的功能如下:

  • key:键值对中的键
  • val :键值对中的值,使用union(即共用体)实现,存储的内容既可能是一个指向值的指针,也可能 是64位整型,或无符号64位整型;
  • next:指向下一个dictEntry,用于解决哈希冲突问题

在64位系统中,一个dictEntry对象占24字节(key/val/next各占8字节)。

4.3.4 编码转换

如前所述,Redis中内层的哈希既可能使用哈希表,也可能使用压缩列表。

只有同时满足下面两个条件时,才会使用压缩列表:

  • 哈希中元素数量小于512个;
  • 哈希中所有键值对的键和值字符串长度都小于64字节。

4.4 集合

4.4.1 概况

​ 集合(set)与列表类似,都是用来保存多个字符串,但集合与列表有两点不同:集合中的元素是无序 的,因此不能通过索引来操作元素;集合中的元素不能有重复。

​ 一个集合中最多可以存储2^32-1个元素;除了支持常规的增删改查,Redis还支持多个集合取交集、并集、差集。

4.4.2 内部编码

集合的内部编码可以是整数集合(intset)或哈希表(hashtable)。

哈希表前面已经讲过,这里略过不提;需要注意的是,集合在使用哈希表时,值全部被置为null。

整数集合的结构定义如下:

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

​ 其中,encoding代表contents中存储内容的类型,虽然contents(存储集合中的元素)是int8_t 类型,但实际上其存储的值是int16_t、int32_t或int64_t,具体的类型便是由encoding决定的; length表示元素个数。

​ 整数集合适用于集合所有元素都是整数且集合元素数量较小的时候,与哈希表相比,整数集合的 优势在于集中存储,节省空间;同时,虽然对于元素的操作复杂度也由O(1)变为了O(n),但由于 集合数量较少,因此操作的时间并没有明显劣势。

4.4.3 编码转换

只有同时满足下面两个条件时,集合才会使用整数集合:

  • 集合中元素数量小于512个
  • 集合中所有元素都是整数值。

4.5 有序集合

4.5.1 概况

有序集合与集合一样,元素都不能重复;但与集合不同的是,有序集合中的元素是有顺序的。与列表使 用索引下标作为排序依据不同,有序集合为每个元素设置一个分数(score)作为排序依据。

4.5.2 内部编码

​ 有序集合的内部编码可以是压缩列表(ziplist)或跳跃表(skiplist)。ziplist在列表和哈希中都有使用,前面已经讲过,这里略过不提。

​ 跳跃表是一种有序数据结构,通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点 的目的。除了跳跃表,实现有序数据结构的另一种典型实现是平衡树;大多数情况下,跳跃表的效率可 以和平衡树媲美,且跳跃表实现比平衡树简单很多,因此redis中选用跳跃表代替平衡树。跳跃表支持平 均O(logN)、最坏O(N)的复杂点进行节点查找,并支持顺序操作。

Redis的跳跃表实现由zskiplist和zskiplistNode两个结构组成:前者用于保存跳跃表信息(如头结点、尾节点、长度等),后者用于表示跳跃表节点。

只有同时满足下面两个条件时,才会使用压缩列表:

  • 有序集合中元素数量小于128个;
  • 有序集合中所有 成员长度都不足64字节。

如果有一个条件不满足,则使用跳跃表;且编码只可能由压缩列表转化为跳跃 表,反方向则不可能。

4.5.3 跳跃表

普通单向链表图示:

跳跃表1

跳跃表图示:

跳跃表2

查询案例
查找一个节点时,我们只需从高层到低层,一个个链表查找,每次找到该层链表中小于等于目标节点的 最大节点,直到找到为止。由于高层的链表迭代时会“跳过”低层的部分节点,所以跳跃表会比正常的链 表查找少查部分节点,这也是skiplist名字的由来。

查找46 : 55---21---55--37--55--46

插入案例

	先讨论插入,我们先看理想的跳跃表结构,L2层的元素个数是L1层元素个数的1/2,L3层的元素个数是L2层的元素个数的1/2,以此类推。从这里,我们可以想到,只要在插入时尽量保证上一层的元素个数是下一层元素的1/2,我们的跳跃表就能成为理想的跳跃表。那么怎么样才能在插入时保证上一层元素个数是下一层元素个数的1/2呢?很简单,抛硬币就能解决了!假设元素X要插入跳跃表,很显然,L1层肯定要插入X。那么L2层要不要插入X呢?我们希望上层元素个数是下层元素个数的1/2,所以我们有1/2的概率希望X插入L2层,那么抛一下硬币吧,正面就插入,反面就不插入。那么L3到底要不要插入X呢?相对于L2层,我们还是希望1/2的概率插入,那么继续抛硬币吧!以此类推,元素X插入第n层的概率是(1/2)的n次。这样,我们能在跳跃表中插入一个元素了。

在此还是以上图为例:跳跃表的初试状态如下图,表中没有一个元素:

跳跃表4

如果我们要插入元素2,首先是在底部插入元素2,如下图:

跳跃表5

然后我们抛硬币,结果是正面,那么我们要将2插入到L2层,如下图:

跳跃表6

​ 继续抛硬币,结果是反面,那么元素2的插入操作就停止了,插入后的表结构就是上图所示。接下来,我们插入元素33,跟元素2的插入一样,现在L1层插入33,如下图:

跳跃表7

​ 然后抛硬币,结果是反面,那么元素33的插入操作就结束了,插入后的表结构就是上图所示。接下来,我们插入元素55,首先在L1插入55,插入后如下图:

跳跃表8

​ 然后抛硬币,结果是正面,那么L2层需要插入55,如下图:

跳跃表9

继续抛硬币,结果又是正面,那么L3层需要插入55,如下图:

跳跃表10

​ 以此类推,我们插入剩余的元素。当然因为规模小,结果很可能不是一个理想的跳跃表。但是如果元素个数n的规模很大,学过概率论的同学都知道,最终的表结构肯定非常接近于理想跳跃表。

删除

​ 直接删除元素,然后调整一下删除元素后的指针即可。跟普通的链表删除操作完全一样。

实现源码

typedef struct zskiplistNode {
	//层
	struct zskiplistLevel{
	//前进指针 后边的节点
	struct zskiplistNode *forward;
	//跨度
	unsigned int span;
}level[];
	//后退指针
struct zskiplistNode *backward;
	//分值
	double score;
	//成员对象
	robj *obj;
} zskiplistNode
--链表
typedef struct zskiplist{
	//表头节点和表尾节点
	structz skiplistNode *header, *tail;
	//表中节点的数量
	unsigned long length;
	//表中层数最大的节点的层数
	int level;
}zskiplist;

总结:

  • 搜索:从最高层的链表节点开始,如果比当前节点要大和比当前层的下一个节点要小,那么则往下 找,也就是和当前层的下一层的节点的下一个节点进行比较,以此类推,一直找到最底层的最后一个节 点,如果找到则返回,反之则返回空。
  • 插入:首先确定插入的层数,有一种方法是假设抛一枚硬币,如果是正面就累加,直到遇见反面为 止,最后记录正面的次数作为插入的层数。当确定插入的层数k后,则需要将新元素插入到从底层到k 层。
  • 删除:在各个层中找到包含指定值的节点,然后将节点从链表中删除即可,如果删除以后只剩下头 尾两个节点,则删除这一层。

五、Redis持久化

Redis 是一个内存数据库,为了保证数据的持久性,它提供了三种持久化方案:

  • RDB 方式(默认)
  • AOF 方式
  • 混合持久化模式(4.0增加,5.0默认开启)

5.1 RDB

​ RDB 是 Redis 默认采用的持久化方式。 RDB 方式是通过快照( snapshotting )完成的,当符合一定条件时 Redis 会自动将内存中的数据进行 快照并持久化到硬盘。

​ 8点持久化的数据,这个过程持续了半个小时,8.30持久化的数据,是8点这个时间点的。用到了操作系统fork函数。

5.1.1 触发RDB快照的时机
  1. 符合指定配置的快照规则
  2. 执行sava或bgsave命令
  3. 执行flushall
  4. 执行主从复制操作
5.1.2设置快照规则

在redis.conf中配置,漏斗型

save 多少秒内 数据变了多少

save “” : 不使用RDB存储

save 900 1 : 表示15分钟(900秒钟)内至少1个键被更改则进行快照。

save 300 10 : 表示5分钟(300秒)内至少10个键被更改则进行快照。

save 60 10000 :表示1分钟内至少10000个键被更改则进行快照。

5.1.3 RDB快照的实现原理

快照过程

  1. Redis 调用系统中的 fork 函数复制一份当前进程的副本(子进程)
  2. 父进程继续接收并处理客户端发来的命令,而子进程开始将内存中的数据写入硬盘中的临时文件
  3. . 当子进程写入完所有数据后会用该临时文件替换旧的 RDB 文件,至此,一次快照操作完成。

RDB快照

注意事项

  1. Redis 在进行快照的过程中不会修改 RDB 文件,只有快照结束后才会将旧的文件替换成新的,也就是说任何时候 RDB 文件都是完整的。
  2. 这就使得我们可以通过定时备份 RDB 文件来实现 Redis 数据库的备份, RDB 文件是经过压缩的二进制文件 ,占用的空间会小于内存中的数据,更加利于传输。

fork

image-20210913143449622

父进程其实可以让子进程看到数据,在linux中环境变量,子进程的修改不会破坏父进程。父进程的修改也不会破坏子进程。

fork并不是真正的复制,涉及到了变量的引用。而在对数据操作时,父子进程,才会复制(copy on write)

优缺点

  • 缺点:使用 RDB 方式实现持久化,一旦 Redis 异常退出,就会丢失最后一次快照以后更改的所有数据。这个时候我们就需要根据具体的应用场景,通过组合设置自动快照条件的方式来将可能发生 的数据损失控制在能够接受范围。如果数据相对来说比较重要,希望将损失降到最小,则可以使 用 AOF 方式进行持久化
  • 优点: RDB 可以最大化 Redis 的性能:父进程在保存 RDB 文件时唯一要做的就是 fork 出一个子 进程,然后这个子进程就会处理接下来的所有保存工作,父进程无需执行任何磁盘 I/O 操作。同时这个也是一个缺点,如果数据集比较大的时候, fork 可以能比较耗时,造成服务器在一段时间内停止处理客户端的请求;

5.2 AOF

5.2.1 AOF介绍

​ 默认情况下Redis没有开启AOF(append only file)方式的持久化。

​ 开启AOF持久化后,每执行一条会更改 Redis 中的数据的命令, Redis 就会将该命令写入硬盘中的 AOF 文件,这一过程显然会降低 Redis 的性能,但大部分情况下这个影响是能够接受的,另外使用较快的硬盘可以提高 AOF 的性能。

在redis.conf配置开启AOF

# 可以通过修改redis.conf配置文件中的appendonly参数开启
appendonly yes
# AOF文件的保存位置和RDB文件的位置相同,都是通过dir参数设置的。
dir ./
# 默认的文件名是appendonly.aof,可以通过appendfilename参数修改
appendfilename appendonly.aof
5.2.2 同步磁盘数据

​ Redis 每次更改数据的时候, aof 机制都会将命令记录到 aof 文件,但是实际上由于操作系统的缓存 机制,数据并没有实时写入到硬盘,而是进入硬盘缓存。再通过硬盘缓存机制去刷新到保存到文件。

参数说明

# 每次执行写入都会进行同步, 这个是最安全但是是效率比较低的方式
appendfsync always
# 每一秒执行(redis6默认)
appendfsync everysec
# 不主动进行同步操作,由操作系统去执行,这个是最快但是最不安全的方式,kernel fd8 buffer,也可能会丢失数据
appendfsync no

5.2.3 AOF重写原理

​ Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写。重写后的新 AOF 文 件包含了恢复当前数据集所需的最小命令集合。

案例

set s1 11
set s1 22
set s31 33
没有优化
set s1 11
set s1 22
set s1 33
优化后
set s1 33
lpush list1 1 2 3
lpush list1 4 5 6
优化后 lpush 1 2 3 4 5 6

​ AOF 文件有序地保存了对数据库执行的所有写入操作, 这些写入操作以 Redis 协议(RESP)的格式保存, 因此 AOF 文件的内容非常容易被人读懂, 对文件进行分析( parse )也很轻松。

5.2.4如何选择RDB和AOF

4.0之前还需要考虑以下方案,4.0之后不需要考虑

内存数据库 rdb(redis database)+aof 数据不能丢

缓存服务器 rdb

不建议 只使用 aof (性能差)

恢复时: 先aof再rdb

5.2.5 混合持久化方式

​ 4.0之前恢复时,只用AOF恢复

​ Redis 4.0 之后新增的方式,混合持久化是结合了 RDB 和 AOF 的优点,在写入的时候,先把当前 的数据以 RDB 的形式写入文件的开头,再将后续的操作命令以 AOF 的格式存入文件,这样既能保证 Redis 重启时的速度,又能减低数据丢失的风险。

查询是否开启混合持久化可以使用 config get aof-use-rdb-preamble 命令,执行结果

127.0.0.1:6379> config get aof-use-rdb-preamble
1) "aof-use-rdb-preamble"
2) "yes"

其中 yes 表示已经开启混合持久化,no 表示关闭,Redis 5.0 默认值为 yes。如果是其他版本的 Redis 首先需要检查一下,是否已经开启了混合持久化,如果关闭的情况下,可以通过以下两种方式开启:

  • 通过命令行开启:使用命令 config set aof-use-rdb-preamble yes 命令行设置配置的缺点是重启 Redis 服务之后,设置的配置就会失效
  • 通过修改 Redis 配置文件开启。在 Redis 的根路径下找到 redis.conf 文件,把配置文件中的 aof-use-rdb-preamble no 改为 aof-userdb-preamble yes 配置完成之后,需要重启 Redis 服务器,配置才能生效,但修改配置文件的方式,在每次重启 Redis 服 务之后,配置信息不会丢失。 需要注意的是,在非必须进行持久化的业务中,可以关闭持久化,这样可以有效的提升 Redis 的运行速度,不会出现间歇性卡顿的困扰。

六、Redis主从复制

6.1 什么是主从复制

​ 持久化保证了即使 Redis 服务重启也不会丢失数据,因为 Redis 服务重启后会将硬盘上持久化的数据 恢复到内存中,但是当 Redis 服务器的硬盘损坏了可能会导致数据丢失,不过通过 Redis 的主从复制 机制就可以避免这种单点故障,

如下图:

主从复制

说明

  • 主 Redis 中的数据有两个副本( replication )即从 redis1 和从 redis2 ,即使一台 Redis 服务器宕机其它两台 Redis 服务也可以继续提供服务。
  • 主 Redis 中的数据和从 Redis 上的数据保持实时同步,当主 Redis 写入数据时通过主从复制机制 会复制到两个从 Redis 服务上。
  • 只有一个主 Redis ,可以有多个从 Redis 。
  • 主从复制不会阻塞 master ,在同步数据时, master 可以继续处理 client 请求。
  • 一个 Redis 可以即是主又是从。

6.2 实现原理

  • Redis的主从同步,分为全量同步和增量同步
  • 只有从机第一次连接上主机是全量同步
  • 断线重连有可能触发全量同步也有可能增量同步(master 判断runid是否一致)
  • 除此之外都是增量同步

增量同步

6.2.1 全量同步

Redis的全量同步过程主要分为三个阶段:

  • 同步快照阶段:Master创建并发送快照给Slave,Slave载入并解析快照。Master同时将此阶段所产生的新的写命令存储到缓冲区。
  • 同步写缓冲阶段:Master向Slave同步存储在缓冲区的写操作命令。
  • 同步增量阶段:Master向Slave同步写操作命令

全量同步

6.2.2 增量同步
  • Redis 增量同步主要指 Slave 完成初始化后开始正常工作时, Master 发生的写操作同步到 Slave 的过程
  • 通常情况下, Master 每执行一个写命令就会向 Slave 发送相同的写命令,然后 Slave 接收并执 行。 主从配置

6.3 主从配置

主Redis配置

无需配置

从Redis配置

修改从服务器的redis.conf文件

# replicaof <masterip> <masterport>
# 表示当前【从服务器】对应的【主服务器】的IP是192.168.10.135,端口是6379。
#4.0之前只能slaveof 4.0之后默认replicaof,slaveof楪祈作用
slaveof 192.168.133.154 6379
replicaof 192.168.133.154 6379
replicaof none one  #取消追随

七、哨兵机制

​ Redis 主从复制的缺点:没有办法对 master 进行动态选举,需要使用 Sentinel机制(哨兵机制)完成动态选举。哨兵之间基于发布订阅模式。

简介

  • Sentinel (哨兵)进程是用于监控 Redis 集群中 Master 主服务器工作的状态
  • 在 Master 主服务器发生故障的时候,可以实现 Master 和 Slave 服务器的切换,保证系统的高可 用( HA )
  • 其已经被集成在 redis2.6+ 的版本中, Redis 的哨兵模式到了 2.8 版本之后就稳定了下来。

7.1 为什么要用到哨兵模式

哨兵(Sentinel)主要是为了解决在主从复制架构中出现宕机的情况,主要分为两种情况:

1)从Redis宕机

​ 这个相对而言比较简单,在Redis中从库重新启动后会自动加入到主从架构中,自动完成同步数据。在 Redis2.8版本后,主从断线后恢复的情况下实现增量复制。

2)主Reis宕机

​ 这个相对而言就会复杂一些,需要以下2步才能完成 a. 在从数据库中执行SLAVEOF NO ONE命令,断开主从关系并且提升为主库继续服务 b. 第二步,将主库重新启动后,执行SLAVEOF命令,将其设置为其他库的从库,这时数据就能更新回来

由于这个手动完成恢复的过程其实是比较麻烦的并且容易出错,所以Redis提供的哨兵(sentinel)的功能来解决

7.2 哨兵机制(sentinel)的高可用

​ Sentinel(哨兵)是Redis 的高可用性解决方案:由一个或多个Sentinel 实例 组成的Sentinel 系统可以 监视任意多个主服务器,以及这些主服务器属下的所有从服务器,并在被监视的主服务器进入下线状态 时,自动将下线主服务器属下的某个从服务器升级为新的主服务器。

如图所示

哨兵机制

在Server1 掉线后:

哨兵机制2

升级Server2 为新的主服务器:

哨兵机制3

7.3 哨兵的定时监控

每个哨兵都有三个任务。

任务1:每个哨兵节点每10秒会向主节点和从节点发送info命令获取最拓扑结构图,哨兵配置时只要配置对主节点的监控即可,通过向主节点发送info,获取从节点的信息,并当有新的从节点加入时可以马上感知到。

哨兵机制4

任务2

​ 每个哨兵节点每隔2秒会向redis数据节点的指定频道上发送该哨兵节点对于主节点的判断以及当前哨兵节点的信息,同时每个哨兵节点也会订阅该频道,来了解其它哨兵节点的信息及对主节点的判断,其实就是通过消息publish和subscribe来完成的。

哨兵机制5

任务3

​ 隔1秒每个哨兵会向主节点、从节点及其余哨兵节点发送一次ping命令做一次心跳检测,这个也是哨兵用来判断节点是否正常的重要依据

哨兵机制6

主观下线

​ 所谓主观下线,就是单个sentinel认为某个服务下线(有可能是接收不到订阅,之间的网络 不通等等原因)。sentinel会以每秒一次的频率向所有与其建立了命令连接的实例(master,从服务,其他sentinel)发 ping命令,通过判断ping回复是有效回复,还是无效回复来判断实例时候在线(对该sentinel来说是“主 观在线”)。

​ sentinel配置文件中的down-after-milliseconds设置了判断主观下线的时间长度,如果实例在downafter-milliseconds毫秒内,返回的都是无效回复,那么sentinel回认为该实例已(主观)下线,修改其 flags状态为SRI_S_DOWN。如果多个sentinel监视一个服务,有可能存在多个sentinel的down-aftermilliseconds配置不同,这个在实际生产中要注意。

客观下线

​ 当主观下线的节点是主节点时,此时该哨兵3节点会通过指令sentinel is-masterdown-by-addr寻求其它哨兵节点对主节点的判断,如果其他的哨兵也认为主节点主观线下了,则当认为主观下线的票数超过了quorum(选举)个数,此时哨兵节点则认为该主节点确实有问题,这样就客观下线了,大部分哨兵节点都同意下线操作,也就说是客观下线

7.4 哨兵leader选举流程

​ 如果主节点被判定为客观下线之后,就要选取一个哨兵节点来完成后面的故障转移工作,选举出一个leader的流程如下:

  1. 每个在线的哨兵节点都可以成为领导者,当它确认(比如哨兵3)主节点下线时,会向其它哨兵发is-master-down-by-addr命令,征求判断并要求将自己设置为领导者,由领导者处理故障转移;
  2. 当其它哨兵收到此命令时,可以同意或者拒绝它成为领导者;
  3. 如果哨兵3发现自己在选举的票数大于等于num(sentinels)/2+1时,将成为领导者,如果没有超过,继续选举…………

哨兵机制7

7.4 自动故障转移机制

在从节点中选择新的主节点

​ sentinel状态数据结构中保存了主服务的所有从服务信息,领头sentinel按照如下的规则从从服务列表中挑选出新的主服务。

  1. 过滤掉主观下线的节点
  2. 选择slave-priority最高的节点,如果由则返回没有就继续选择
  3. 选择出复制偏移量最大的系节点,因为复制偏移量越大则数据复制的越完整,如果由就返回了,没有就继续
  4. 选择run_id最小的节点

哨兵机制7

更新主从状态

​ 通过slaveof no one命令,让选出来的从节点成为主节点;并通过slaveof命令让其他节点成为其从节点。

​ 将已下线的主节点设置成新的主节点的从节点,当其回复正常时,复制新的主节点,变成新的主节点的从节点,同理,当已下线的服务重新上线时,sentinel会向其发送slaveof命令,让其成为新主的从

八、集群演变

8.1主从复制(不用)

参考主从复制

8.2Replication+sentinel 高可用

这套架构使用的是社区版本推出的原生高可用解决方案,其架构图如下

集群演变

这里sentinel的作用有三个:

  1. 监控:Sentinel 会不断的检查主服务器和从服务器是否正常运行。
  2. 通知:当被监控的某个Redis服务器出现问题,Sentinel通过API脚本向管理员或者其他的应用程序 发送通知。
  3. 自动故障转移:当主节点不能正常工作时,Sentinel会开始一次自动的故障转移操作,它会将与失 效主节点是主从关系的其中一个从节点升级为新的主节点,并且将其他的从节点指向新的主节点。

工作原理

​ 当Master宕机的时候,Sentinel会选举出新的Master,并根据Sentinel中client-reconfig-script脚本配置的内容,去动态修改VIP(虚拟IP),将VIP(虚拟IP)指向新的Master。我们的客户端就连向指定的VIP即可! 故障发生后的转移情况,可以理解为下图。

集群演变02

缺陷

  1. 主从切换的过程中会丢数据
  2. Redis只能单点写,不能水平扩容

8.3 Proxy+Replication+Sentinel(了解)

这里的Proxy有两种选择:Codis(豌豆荚)和Twemproxy(推特)。

集群演变03

工作原理

  • 前端使用Twemproxy+KeepAlived做代理,将其后端的多台Redis实例分片进行统一管理与分配
  • 每一个分片节点的Slave都是Master的副本且只读
  • Sentinel持续不断的监控每个分片节点的Master,当Master出现故障且不可用状态时,Sentinel 会通知/启动自动故障转移等动作
  • Sentinel 可以在发生故障转移动作后触发相应脚本(通过 client-reconfig-script 参数配置 ),脚 本获取到最新的Master来修改Twemproxy配置

缺陷

  1. 部署结构超级复杂
  2. 可扩展性差,进行扩缩容需要手动干预
  3. 运维不方便

8.4 Redis Cluster 拓展性

​ 如果Redis只用复制功能做主从,那么当数据量巨大的情况下,单机情况下可能已经承受不下一份数据,更不用说是主从都要各自保存一份完整的数据。在这种情况下,数据分片是一个非常好的解决办法。

Redis的Cluster正是用于解决该问题。它主要提供两个功能

  • 自动对数据分片,落到各个节点上
  • 即使集群部分节点失效或者连接不上,依然可以继续处理命令

简单哈希算法

假设有三台机,数据落在哪台机的算法为

c = Hash(key) % 3

例如key A的哈希值为4,4%3=1,则落在第二台机。Key ABC哈希值为11,11%3=2,则落在第三台机上。

利用这样的算法,假设现在数据量太大了,需要增加一台机器。A原本落在第二台上,现在根据算法4%4=0,落到了第一台机器上了,但是第一台机器上根本没有A的值。这样的算法会导致增加机器或减少机器的时候,引起大量的缓存穿透,造成雪崩。

一致性哈希算法

在1997年,麻省理工学院的Karger等人提出了一致性哈希算法,为的就是解决分布式缓存的问题。

一致性哈希算法中,整个哈希空间是一个虚拟圆环

假设有四个节点Node A、B、C、D,经过ip地址的哈希计算,它们的位置如下

一致性hash

有4个存储对象Object A、B、C、D,经过对Key的哈希计算后,它们的位置如下

一致性hash2

一致性哈希算法大概如此,那么它的容错性扩展性如何呢?

假设Node C节点挂掉了,Object C的存储丢失,那么它顺时针找到的最新节点是Node D。也就是说Node C挂掉了,受影响仅仅包括Node B到Node C区间的数据,并且这些数据会转移到Node D进行存储。

同理,假设现在数据量大了,需要增加一台节点Node X。Node X的位置在Node B到Node C直接,那么受到影响的仅仅是Node B到Node X间的数据,它们要重新落到Node X上。

所以一致性哈希算法对于容错性和扩展性有非常好的支持。但一致性哈希算法也有一个严重的问题,就是数据倾斜

如果在分片的集群中,节点太少,并且分布不均,一致性哈希算法就会出现部分节点数据太多,部分节点数据太少。也就是说无法控制节点存储数据的分配。如下图,大部分数据都在A上了,B的数据比较少。

一致性hash03

哈希槽

edis集群(Cluster)并没有选用上面一致性哈希,而是采用了哈希槽(SLOT)的这种概念。主要的原因就是上面所说的,一致性哈希算法对于数据分布、节点位置的控制并不是很友好。

首先哈希槽其实是两个概念,第一个是哈希算法。Redis Cluster的hash算法不是简单的hash(),而是crc16算法,一种校验算法。

另外一个就是槽位的概念,空间分配的规则。其实哈希槽的本质和一致性哈希算法非常相似,不同点就是对于哈希空间的定义。一致性哈希的空间是一个圆环,节点分布是基于圆环的,无法很好的控制数据分布。而Redis Cluster的槽位空间是自定义分配的,类似于Windows盘分区的概念。这种分区是可以自定义大小,自定义位置的。

Redis Cluster包含了16384个哈希槽,每个Key通过计算后都会落在具体一个槽位上,而这个槽位是属于哪个存储节点的,则由用户自己定义分配。例如机器硬盘小的,可以分配少一点槽位,硬盘大的可以分配多一点。如果节点硬盘都差不多则可以平均分配。所以哈希槽这种概念很好地解决了一致性哈希的弊端。

另外在容错性扩展性上,表象与一致性哈希一样,都是对受影响的数据进行转移。而哈希槽本质上是对槽位的转移,把故障节点负责的槽位转移到其他正常的节点上。扩展节点也是一样,把其他节点上的槽位转移到新的节点上。

但一定要注意的是,对于槽位的转移和分派,Redis集群是不会自动进行的,而是需要人工配置的。所以Redis集群的高可用是依赖于节点的主从复制与主从间的自动故障转移。

8.4.1Redis-cluster架构图

image-20210112215907568

架构细节

  1. 所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽.
  2. )节点的fail是通过集群中超过半数的节点检测失效时才生效.
  3. )客户端与redis节点直连,不需要中间proxy层.客户端不需要连接集群所有节点,连接集群中任何一个可 用节点即可
  4. redis-cluster把所有的物理节点映射到[0-16383]slot上,cluster 负责维护node<->slot<->value
8.4.2 Redis-cluster投票:容错

redis-cluster

  1. )节点失效判断:集群中所有master参与投票,如果半数以上master节点与其中一个master节点通信 超过(cluster-node-timeout),认为该master节点挂掉.
  2. )集群失效判断:什么时候整个集群不可(cluster_state:fail)?
  • 如果集群任意master挂掉,且当前master没有slave,则集群进入fail状态。也可以理解成集群的[0- 16383]slot映射不完全时进入fail状态。
  • 如果集群超过半数以上master挂掉,无论是否有slave,集群进入fail状态。
8.4.3 安装RedisCluster

​ Redis集群最少需要三台主服务器,三台从服务器。 端口号分别为:7001~7006

第一步

​ 创建7001实例,并编辑redis.conf文件,修改port为7001。 注意:创建实例,即拷贝单机版安装时,生成的bin目录,为7001目录。

redis-cluster2

第二步:
修改redis.conf配置文件,打开cluster-enable yes

redis-cluster3

第三步
复制7001,创建7002~7006实例,注意端口修改

第四步

​ 创建start.sh,启动所有的实例

cd 7001
./redis-server redis.conf
cd ..
cd 7002
./redis-server redis.conf
cd ..
cd 7003
./redis-server redis.conf
cd ..
cd 7004
./redis-server redis.conf
cd ..
cd 7005
./redis-server redis.conf
cd ..
cd 7006
./redis-server redis.conf
cd ..

chmod u+x start.sh

第五步
创建Redis集群

[root@localhost 7001]# ./redis-cli --cluster create 127.0.0.1:7001
127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 127.0.0.1:7006 -
-cluster-replicas 1
>>> Performing hash slots allocation on 6 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
Adding replica 127.0.0.1:7005 to 127.0.0.1:7001
Adding replica 127.0.0.1:7006 to 127.0.0.1:7002
Adding replica 127.0.0.1:7004 to 127.0.0.1:7003
>>> Trying to optimize slaves allocation for anti-affinity
[WARNING] Some slaves are in the same host as their master
M: af559fc6c82c83dc39d07e2dfe59046d16b6a429 127.0.0.1:7001
slots:[0-5460] (5461 slots) master
M: 068b678923ad0858002e906040b0fef6fff8dda4 127.0.0.1:7002
slots:[5461-10922] (5462 slots) master
M: d277cd2984639747a17ca79428602480b28ef070 127.0.0.1:7003
slots:[10923-16383] (5461 slots) master
S: 51c3ebdd0911dd6564040c7e20b9ae69cabb0425 127.0.0.1:7004
replicates af559fc6c82c83dc39d07e2dfe59046d16b6a429
S: e7b1f1962de2a1ffef2bf1ac5d94574b2e4d67d8 127.0.0.1:7005
replicates 068b678923ad0858002e906040b0fef6fff8dda4
S: 78dfe773eaa817fb69a405a3863f5b8fcf3e172f 127.0.0.1:7006
replicates d277cd2984639747a17ca79428602480b28ef070
Can I set the above configuration? (type 'yes' to accept): yes
>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join
....
>>> Performing Cluster Check (using node 127.0.0.1:7001)
M: af559fc6c82c83dc39d07e2dfe59046d16b6a429 127.0.0.1:7001
slots:[0-5460] (5461 slots) master
1 additional replica(s)
M: d277cd2984639747a17ca79428602480b28ef070 127.0.0.1:7003
slots:[10923-16383] (5461 slots) master
1 additional replica(s)
S: e7b1f1962de2a1ffef2bf1ac5d94574b2e4d67d8 127.0.0.1:7005
slots: (0 slots) slave
replicates 068b678923ad0858002e906040b0fef6fff8dda4
M: 068b678923ad0858002e906040b0fef6fff8dda4 127.0.0.1:7002
slots:[5461-10922] (5462 slots) master
1 additional replica(s)
S: 51c3ebdd0911dd6564040c7e20b9ae69cabb0425 127.0.0.1:7004
slots: (0 slots) slave
replicates af559fc6c82c83dc39d07e2dfe59046d16b6a429
S: 78dfe773eaa817fb69a405a3863f5b8fcf3e172f 127.0.0.1:7006
slots: (0 slots) slave
replicates d277cd2984639747a17ca79428602480b28ef070
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
[root@localhost-0723 redis]#

命令客户端连接集群

./redis-cli -h 127.0.0.1 -p 7001 -c

注意:-c 表示是以redis集群方式进行连接

8.4.5 维护节点
1) 查看集群中的状态
127.0.0.1:7003> cluster info
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:3
cluster_current_epoch:6
cluster_my_epoch:3
cluster_stats_messages_sent:926
cluster_stats_messages_received:926

2)查看集群中的节点
127.0.0.1:7003> cluster nodes
d277cd2984639747a17ca79428602480b28ef070 127.0.0.1:7003@17003 myself,master - 0
1570457306000 3 connected 10923-16383
af559fc6c82c83dc39d07e2dfe59046d16b6a429 127.0.0.1:7001@17001 master - 0
1570457307597 1 connected 0-5460
e7b1f1962de2a1ffef2bf1ac5d94574b2e4d67d8 127.0.0.1:7005@17005 slave
068b678923ad0858002e906040b0fef6fff8dda4 0 1570457308605 5 connected
068b678923ad0858002e906040b0fef6fff8dda4 127.0.0.1:7002@17002 master - 0
1570457309614 2 connected 5461-10922
51c3ebdd0911dd6564040c7e20b9ae69cabb0425 127.0.0.1:7004@17004 slave
af559fc6c82c83dc39d07e2dfe59046d16b6a429 0 1570457307000 4 connected
78dfe773eaa817fb69a405a3863f5b8fcf3e172f 127.0.0.1:7006@17006 slave
d277cd2984639747a17ca79428602480b28ef070 0 1570457309000 6 connected
127.0.0.1:7003>
3)继续添加主节点
[root@localhost 7007]# ./redis-cli --cluster add-node 127.0.0.1:7007
127.0.0.1:7001
>>> Adding node 127.0.0.1:7007 to cluster 127.0.0.1:7001
>>> Performing Cluster Check (using node 127.0.0.1:7001)
M: af559fc6c82c83dc39d07e2dfe59046d16b6a429 127.0.0.1:7001
slots:[0-5460] (5461 slots) master
1 additional replica(s)
M: d277cd2984639747a17ca79428602480b28ef070 127.0.0.1:7003
slots:[10923-16383] (5461 slots) master
1 additional replica(s)
S: e7b1f1962de2a1ffef2bf1ac5d94574b2e4d67d8 127.0.0.1:7005
slots: (0 slots) slave
replicates 068b678923ad0858002e906040b0fef6fff8dda4
M: 068b678923ad0858002e906040b0fef6fff8dda4 127.0.0.1:7002
slots:[5461-10922] (5462 slots) master
1 additional replica(s)
S: 51c3ebdd0911dd6564040c7e20b9ae69cabb0425 127.0.0.1:7004
slots: (0 slots) slave
replicates af559fc6c82c83dc39d07e2dfe59046d16b6a429
S: 78dfe773eaa817fb69a405a3863f5b8fcf3e172f 127.0.0.1:7006
slots: (0 slots) slave
replicates d277cd2984639747a17ca79428602480b28ef070
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
>>> Send CLUSTER MEET to node 127.0.0.1:7007 to make it join the cluster.
[OK] New node added correctly.
4) hash槽重新分配(数据迁移)

​ 添加完主节点需要对主节点进行hash槽分配,这样该主节才可以存储数据。

如何给刚添加的7007节点分配槽?

第一步:连接上集群(连接集群中任意一个可用结点都行)

[root@localhost 7007]# ./redis-cli --cluster reshard 127.0.0.1:7007
>>> Performing Cluster Check (using node 127.0.0.1:7007)
M: 50b073163bc4058e89d285dc5dfc42a0d1a222f2 127.0.0.1:7007
slots: (0 slots) master
S: 51c3ebdd0911dd6564040c7e20b9ae69cabb0425 127.0.0.1:7004
slots: (0 slots) slave
replicates af559fc6c82c83dc39d07e2dfe59046d16b6a429
S: 78dfe773eaa817fb69a405a3863f5b8fcf3e172f 127.0.0.1:7006
slots: (0 slots) slave
replicates d277cd2984639747a17ca79428602480b28ef070
S: e7b1f1962de2a1ffef2bf1ac5d94574b2e4d67d8 127.0.0.1:7005
slots: (0 slots) slave
replicates 068b678923ad0858002e906040b0fef6fff8dda4
M: af559fc6c82c83dc39d07e2dfe59046d16b6a429 127.0.0.1:7001
slots:[0-5460] (5461 slots) master
1 additional replica(s)
M: 068b678923ad0858002e906040b0fef6fff8dda4 127.0.0.1:7002
slots:[5461-10922] (5462 slots) master
1 additional replica(s)
M: d277cd2984639747a17ca79428602480b28ef070 127.0.0.1:7003
slots:[10923-16383] (5461 slots) master
1 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

第二步:输入要分配的槽数量

How many slots do you want to move (from 1 to 16384)? 3000

输入:3000,表示要给目标节点分配3000个槽

第三步:输入接收槽的节点id

What is the receiving node ID?

输入:50b073163bc4058e89d285dc5dfc42a0d1a222f2

PS:这里准备给7007分配槽,通过cluster nodes查看7007结点id为: 50b073163bc4058e89d285dc5dfc42a0d1a222f2

第四步:输入源节点id

Please enter all the source node IDs.
Type 'all' to use all the nodes as source nodes for the hash slots.
Type 'done' once you entered all the source nodes IDs.

输入:all

第五步:输入yes开始移动槽到目标节点id

输入:yes

Moving slot 11899 from 127.0.0.1:7003 to 127.0.0.1:7007:
Moving slot 11900 from 127.0.0.1:7003 to 127.0.0.1:7007:
Moving slot 11901 from 127.0.0.1:7003 to 127.0.0.1:7007:
Moving slot 11902 from 127.0.0.1:7003 to 127.0.0.1:7007:
Moving slot 11903 from 127.0.0.1:7003 to 127.0.0.1:7007:
Moving slot 11904 from 127.0.0.1:7003 to 127.0.0.1:7007:
Moving slot 11905 from 127.0.0.1:7003 to 127.0.0.1:7007:
Moving slot 11906 from 127.0.0.1:7003 to 127.0.0.1:7007:
Moving slot 11907 from 127.0.0.1:7003 to 127.0.0.1:7007:
Moving slot 11908 from 127.0.0.1:7003 to 127.0.0.1:7007:
Moving slot 11909 from 127.0.0.1:7003 to 127.0.0.1:7007:
Moving slot 11910 from 127.0.0.1:7003 to 127.0.0.1:7007:
Moving slot 11911 from 127.0.0.1:7003 to 127.0.0.1:7007:
Moving slot 11912 from 127.0.0.1:7003 to 127.0.0.1:7007:
Moving slot 11913 from 127.0.0.1:7003 to 127.0.0.1:7007:
Moving slot 11914 from 127.0.0.1:7003 to 127.0.0.1:7007:
Moving slot 11915 from 127.0.0.1:7003 to 127.0.0.1:7007:
Moving slot 11916 from 127.0.0.1:7003 to 127.0.0.1:7007:
Moving slot 11917 from 127.0.0.1:7003 to 127.0.0.1:7007:
Moving slot 11918 from 127.0.0.1:7003 to 127.0.0.1:7007:
Moving slot 11919 from 127.0.0.1:7003 to 127.0.0.1:7007:
Moving slot 11920 from 127.0.0.1:7003 to 127.0.0.1:7007:
Moving slot 11921 from 127.0.0.1:7003 to 127.0.0.1:7007:

查看结果

127.0.0.1:7001> cluster nodes
d277cd2984639747a17ca79428602480b28ef070 127.0.0.1:7003@17003 master - 0
1570458285557 3 connected 11922-16383
50b073163bc4058e89d285dc5dfc42a0d1a222f2 127.0.0.1:7007@17007 master - 0
1570458284000 7 connected 0-998 5461-6461 10923-11921
e7b1f1962de2a1ffef2bf1ac5d94574b2e4d67d8 127.0.0.1:7005@17005 slave
068b678923ad0858002e906040b0fef6fff8dda4 0 1570458283000 5 connected
068b678923ad0858002e906040b0fef6fff8dda4 127.0.0.1:7002@17002 master - 0
1570458284546 2 connected 6462-10922
51c3ebdd0911dd6564040c7e20b9ae69cabb0425 127.0.0.1:7004@17004 slave
af559fc6c82c83dc39d07e2dfe59046d16b6a429 0 1570458283538 4 connected
af559fc6c82c83dc39d07e2dfe59046d16b6a429 127.0.0.1:7001@17001 myself,master - 0
1570458283000 1 connected 999-5460
78dfe773eaa817fb69a405a3863f5b8fcf3e172f 127.0.0.1:7006@17006 slave
d277cd2984639747a17ca79428602480b28ef070 0 1570458284000 6 connected

5)添加从节点

添加7008从结点,将7008作为7007的从结点

命令

./redis-cli --cluster add-node 新节点的ip和端口 旧节点ip和端口 --cluster-slave --
cluster-master-id 主节点id

例如

./redis-cli --cluster add-node 127.0.0.1:7008 127.0.0.1:7007 --cluster-slave --
cluster-master-id 50b073163bc4058e89d285dc5dfc42a0d1a222f2

50b073163bc4058e89d285dc5dfc42a0d1a222f2是7007结点的id,可通过cluster nodes查看。

注意:如果原来该结点在集群中的配置信息已经生成到cluster-config-file指定的配置文件中(如果 cluster-config-file没有指定则默认为nodes.conf),这时可能会报错:

[ERR] Node XXXXXX is not empty. Either the node already knows other nodes (check
with CLUSTER NODES) or contains some key in database 0

解决方法
解决方法是删除生成的配置文件nodes.conf,删除后再执行./redis-cli --cluster add-node 指令

6) 删除节点

命令:

./redis-cli --cluster del-node 127.0.0.1:7008
41592e62b83a8455f07f7797f1d5c071cffedb50

删除已经占有hash槽的结点会失败,报错如下:

[ERR] Node 127.0.0.1:7008 is not empty! Reshard data away and try again.

需要将该结点占用的hash槽分配出去(参考hash槽重新分配章节)

8.4.6 Jedis连接集群
1)代码实现
@Test
public void testJedisCluster() throws Exception {
	//创建一连接,JedisCluster对象,在系统中是单例存在
	Set<HostAndPort> nodes = new HashSet<>();
	nodes.add(new HostAndPort("192.168.10.133", 7001));
	nodes.add(new HostAndPort("192.168.10.133", 7002));
	nodes.add(new HostAndPort("192.168.10.133", 7003));
	nodes.add(new HostAndPort("192.168.10.133", 7004));
	nodes.add(new HostAndPort("192.168.10.133", 7005));
	nodes.add(new HostAndPort("192.168.10.133", 7006));
	JedisCluster cluster = new JedisCluster(nodes);
	//执行JedisCluster对象中的方法,方法和redis一一对应。
	cluster.set("cluster-test", "my jedis cluster test");
	String result = cluster.get("cluster-test");
	System.out.println(result);
	//程序结束时需要关闭JedisCluster对象
	cluster.close();
}

2) 使用Spring

配置applicationContext.xml

<!-- 连接池配置 -->
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
<!-- 最大连接数 -->
<property name="maxTotal" value="30" />
<!-- 最大空闲连接数 -->
<property name="maxIdle" value="10" />
<!-- 每次释放连接的最大数目 -->
<property name="numTestsPerEvictionRun" value="1024" />
<!-- 释放连接的扫描间隔(毫秒) -->
<property name="timeBetweenEvictionRunsMillis" value="30000" />
<!-- 连接最小空闲时间 -->
<property name="minEvictableIdleTimeMillis" value="1800000" />
<!-- 连接空闲多久后释放, 当空闲时间>该值 且 空闲连接>最大空闲连接数 时直接释放 -->
<property name="softMinEvictableIdleTimeMillis" value="10000" />
<!-- 获取连接时的最大等待毫秒数,小于零:阻塞不确定的时间,默认-1 -->
<property name="maxWaitMillis" value="1500" />
<!-- 在获取连接的时候检查有效性, 默认false -->
<property name="testOnBorrow" value="true" />
<!-- 在空闲时检查有效性, 默认false -->
<property name="testWhileIdle" value="true" />
<!-- 连接耗尽时是否阻塞, false报异常,ture阻塞直到超时, 默认true -->
<property name="blockWhenExhausted" value="false" />
</bean>
<!-- redis集群 -->
<bean id="jedisCluster" class="redis.clients.jedis.JedisCluster">
<constructor-arg index="0">
<set>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg index="0" value="192.168.101.3"></constructorarg>
<constructor-arg index="1" value="7001"></constructor-arg>
</bean>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg index="0" value="192.168.101.3"></constructorarg>
<constructor-arg index="1" value="7002"></constructor-arg>
</bean>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg index="0" value="192.168.101.3"></constructorarg>
<constructor-arg index="1" value="7003"></constructor-arg>
</bean>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg index="0" value="192.168.101.3"></constructorarg>
<constructor-arg index="1" value="7004"></constructor-arg>
</bean>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg index="0" value="192.168.101.3"></constructorarg>
<constructor-arg index="1" value="7005"></constructor-arg>
</bean>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg index="0" value="192.168.101.3"></constructorarg>
<constructor-arg index="1" value="7006"></constructor-arg>
</bean>
</set>
</constructor-arg>
<constructor-arg index="1" ref="jedisPoolConfig"></constructor-arg>
</bean>

测试代码

private ApplicationContext applicationContext;
@Before
public void init() {
	applicationContext = new ClassPathXmlApplicationContext(
"classpath:applicationContext.xml");
}
// redis集群
@Test
public void testJedisCluster() {
	JedisCluster jedisCluster = (JedisCluster) applicationContext
.getBean("jedisCluster");
	jedisCluster.set("name", "zhangsan");
	String value = jedisCluster.get("name");
	System.out.println(value);
}

九、Redis的特殊数据类型

9.1 BitMap

9.1.1 介绍

​ BitMap 就是通过一个 bit 位来表示某个元素对应的值或者状态, 其中的 key 就是对应元素本身,实际上 底层也是通过对字符串的操作来实现。Redis 从 2.2 版本之后新增了setbit, getbit, bitcount 等几个 bitmap 相关命令。虽然是新命令,但是本身都是对字符串的操作,我们先来看看语法:

 SETBIT key offset value

其中 offset 必须是数字,value 只能是 0 或者 1。

例子

SETBIT userid 4 1

bitmap

127.0.0.1:6379> setbit k1 5 1
(integer) 0
127.0.0.1:6379> getbit k1 5
(integer) 1
127.0.0.1:6379> getbit k1 4
(integer) 0
127.0.0.1:6379> bitcount k1
(integer) 1
127.0.0.1:6379> setbit k1 3 1
(integer) 0
127.0.0.1:6379> bitcount k1
(integer) 2
127.0.0.1:6379> setbit "200522:active" 67 1
(integer) 0
127.0.0.1:6379> setbit "200522:active" 78 1
(integer) 0

9.1.2 使用案例

通过 bitcount可以很快速的统计,比传统的关系型数据库效率高很多

1、比如统计年活跃用户数量 用户的ID作为offset,当用户在一年内访问过网站,就将对应offset的bit值设置为“1”; 通过bitcount 来统计一年内访问过网站的用户数量

2、比如统计三天内活跃用户数量 时间字符串作为key,比如 “200522:active“ ; 用户的ID就可以作为offset,当用户访问过网站,就将对应offset的bit值设置为“1”; 统计三天的活跃用户,通过bitop or 获取一周内访问过的用户数量

3、连续三天访问的用户数量 bitop and

4、三天内没有访问的用户数量 bitop not

5、统计在线人数 设置在线key:“online:active”,当用户登录时,通过setbit设置 bitmap的优势,以统计活跃用户为例 每个用户id占用空间为1bit,消耗内存非常少,存储1亿用户量只需要12.5M

9.2 HyperLogLog

HyperLogLog是Redis的高级数据结构,是统计基数的利器。在数学上,基数或势,即集合中包含的元素的“个数”.

这个数据结构的命令有三个:PFADD、PFCOUNT、PFMERGE 内部编码主要分稀疏型和密集型 用途:记录网站IP注册数,每日访问的IP数,页面实时UV、在线用户人数 局限性:只能统计数量,没有办法看具体信息

127.0.0.1:6379> pfadd h1 b
(integer) 1
127.0.0.1:6379> pfadd h1 a
(integer) 0
127.0.0.1:6379> pfcount h1
(integer) 2
127.0.0.1:6379> pfadd h2 c
(integer) 1
127.0.0.1:6379> pfadd h3 a
(integer) 1
127.0.0.1:6379> pfadd h3 e
(integer) 1
127.0.0.1:6379> pfmerge h3 h1 h2
OK
127.0.0.1:6379> pfcount h3
(integer) 4

9.3 Geospatial (3.2)

​ 可以用来保存地理位置,并作位置距离计算或者根据半径计算位置等。有没有想过用Redis来实现附近 的人?或者计算最优地图路径?Geo本身不是一种数据结构,它本质上还是借助于Sorted Set(ZSET)

GEOADD key 经度 维度 名称

把某个具体的位置信息(经度,纬度,名称)添加到指定的key中,数据将会用一个sorted set存储,以 便稍后能使用GEORADIUS和GEORADIUSBYMEMBER命令来根据半径来查询位置信息。

127.0.0.1:6379> GEOADD cities 116.404269 39.91582 "beijing" 121.478799
31.235456 "shanghai"
(integer) 2
127.0.0.1:6379> ZRANGE cities 0 -1
1) "shanghai"
2) "beijing"
127.0.0.1:6379> ZRANGE cities 0 -1 WITHSCORES
1) "shanghai"
2) "4054803475356102"
3) "beijing"
4) "4069885555377153"
127.0.0.1:6379> GEODIST cities beijing shanghai km
"1068.5677"
127.0.0.1:6379> GEOPOS cities beijing shanghai
1) 1) "116.40426903963088989"
2) "39.91581928642635546"
2) 1) "121.47879928350448608"
2) "31.23545629441388627"
127.0.0.1:6379> GEOADD cities 120.165036 30.278973 hangzhou
(integer) 1
127.0.0.1:6379> GEORADIUS cities 120 30 500 km
1) "hangzhou"
2) "shanghai"
127.0.0.1:6379> GEORADIUSBYMEMBER cities shanghai 200 km
1) "hangzhou"
2) "shanghai"
127.0.0.1:6379> ZRANGE CITIES 0 -1
(empty list or set)
127.0.0.1:6379> ZRANGE cities 0 -1
1) "hangzhou"
2) "shanghai"
3) "beijing"
127.0.0.1:6379>

十、Redis消息模式(了解,现在用Stream)

10.1 队列模式

使用list类型的lpush和rpop实现消息队列

消息模式

注意事项

消息接收方如果不知道队列中是否有消息,会一直发送rpop命令,如果这样的话,会每一次都建立一次连接,这样显然不好。

可以使用brpop命令,它如果从队列中取不出来数据,会一直阻塞,在一定范围内没有取出则返回 null

10.2 发布订阅模式

发布订阅模式

10.2.1 使用案例

订阅后只能接受订阅时间之后的消息

发布

redis-master:0>FLUSHALL
"OK"
redis-master:0>publish ooxx hello
"0"
redis-master:0>publish ooxx hello
"1"
redis-master:0>publish ooxx hello1
"1"
redis-master:0>

订阅

redis-master:0>FLUSHALL

“OK”
redis-master:0>publish ooxx hello

“0”
redis-master:0>publish ooxx hello

“1”
redis-master:0>publish ooxx hello1

“1”
redis-master:0>

十一、Redis Stream(重点)

11.1 基本介绍

​ Redis 5.0 全新的数据类型:streams,官方把它定义为:以更抽象的方式建模日志的数据结构。Redis 的streams主要是一个append only的数据结构,至少在概念上它是一种在内存中表示的抽象数据类 型,只不过它们实现了更强大的操作,以克服日志文件本身的限制。 如果你了解MQ,那么可以把streams当做基于内存的MQ。如果你还了解kafka,那么甚至可以把 streams当做基于内存的kafka。

另外,这个功能有点类似于redis以前的Pub/Sub,但是也有基本的不同:

  • streams支持多个客户端(消费者)等待数据(Linux环境开多个窗口执行XREAD即可模拟),并 且每个客户端得到的是完全相同的数据。
  • Pub/Sub是发送忘记的方式,并且不存储任何数据;而streams模式下,所有消息被无限期追加在 streams中,除非用于显示执行删除(XDEL)。
  • streams的Consumer Groups也是Pub/Sub无法实现的控制方式。

Redis Stream 的结构如下所示,它有一个消息链表,将所有加入的消息都串起来,每个消息都有一个唯一的 ID 和对应的内容:

stream

它主要有消息、生产者、消费者、消费组4组成

streams数据结构本身非常简单,但是streams依然是Redis到目前为止最复杂的类型,其原因是实现的一些额外的功能:一系列的阻塞操作允许消费者等待生产者加入到streams的新数据。另外还有一个称 为Consumer Groups的概念,Consumer Group概念最先由kafka提出,Redis有一个类似实现,和 kafka的Consumer Groups的目的是一样的:允许一组客户端协调消费相同的信息流!

发布消息

使用 XADD 向队列添加消息,如果指定的队列不存在,则创建一个队列,XADD 语法格式:

XADD key ID field value [field value ...]
  • key :队列名称,如果不存在就创建
  • ID :消息 id,我们使用 * 表示由 redis 生成,可以自定义,但是要自己保证递增性。
  • field value : 记录
127.0.0.1:6379> xadd mystream * message apple
"1589994652300-0"
127.0.0.1:6379> xadd mystream * message orange
"1589994679942-0

读取消息

xrange key start end
  • key :队列名
  • start :开始值, - 表示最小值
  • end :结束值, + 表示最大值

127.0.0.1:6379> xrange mystream - +
1) 1) "1589994652300-0"
2) 1) "message"
2) "apple"
2) 1) "1589994679942-0"
2) 1) "message"
2) "orange

阻塞读取

xread block 0 streams mystream $

发布新消息

127.0.0.1:6379> xadd mystream * message strawberry

创建消费者组

127.0.0.1:6379> xgroup create mustream mygroup1 0
OK
127.0.0.1:6379> xgroup create mustream mygroup2 0
OK

通过消费者组读取消息

127.0.0.1:6379> xreadgroup group mugroup1 zange count 2 streams mystream >
1) 1) "mystream"
2) 1) 1) "1589994652300-0"
2) 1) "message"
2) "apple"
2) 1) "1589994679942-0"
2) 1) "message"
2) "orange"
127.0.0.1:6379> xreadgroup group mugroup1 tuge count 2 streams mystream >
1) 1) "mystream"
2) 1) 1) "1589995171242-0"
2) 1) "message"
2) "strawberry"
127.0.0.1:6379> xreadgroup group mugroup2 tuge count 1 streams mystream >
1) 1) "mystream"
2) 1) 1) "1589995171242-0"
2) 1) "message"
2) "apple"

11.2 使用场景

redis使用场景

十二、Redis Pipeline

Pipeline (管道技术) 是客户端提供的一种批处理技术 可以批量执行一组指令,一次性返回全部结果, 可以减少频繁的请求应答

12.1 简单使用

12.1.1 shell方式
root@redis-master:/home/simon# echo -e "set k1 99\nincr k1\n get k2" | nc localhost 6379
+OK
:100
$-1


pipeline

十三、Redis和lua

13.1 什么是lua

​ lua是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用 程序中,从而为应用程序提供灵活的扩展和定制功能。

13.2 Redis中使用lua的好处

  1. . 减少网络开销,在Lua脚本中可以把多个命令放在同一个脚本中运行
  2. 原子操作,redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。换句话说,编写脚 本的过程中无需担心会出现竞态条件
  3. 复用性,客户端发送的脚本会永远存储在redis中,这意味着其他客户端可以复用这一脚本来完成 同样的逻辑

13.3 lua的安装和语法

lua 教程 https://www.runoob.com/lua/lua-tutorial.html

13.4Redis整合lua脚本

从Redis2.6.0版本开始,通过内置的lua编译/解释器,可以使用EVAL命令对lua脚本进行求值。

EVAL命令

在redis客户端中,执行以下命令:

EVAL script numkeys key [key ...] arg [arg ...]

参数介绍

  • script参数:是一段Lua脚本程序,它会被运行在Redis服务器上下文中,这段脚本不必(也不应该)定义为一个Lua函数。
  • numkeys参数:用于指定键名参数的个数。
  • key [key …]参数: 从EVAL的第三个参数开始算起,使用了numkeys个键(key),表示在 脚本中所用到的那些Redis键(key),这些键名参数可以在Lua中通过全局变量KEYS数组,用1 为基址的形式访问( KEYS[1] , KEYS[2] ,以此类推)。
  • arg [arg …]参数:可以在Lua中通过全局变量ARGV数组访问,访问的形式和KEYS变量类似( ARGV[1] 、 ARGV[2] ,诸如此类)。

例子

./redis-cli
> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"

13.5 lua脚本调用Redis命令

13.5.1 redis.call()

返回值就是redis命令执行的返回值 如果出错,返回错误信息,不继续执行

13.5.2 redis.pcall()

返回值就是redis命令执行的返回值 如果出错了 记录错误信息,继续执行

注意事项

  • 在脚本中,使用return语句将返回值返回给客户端,如果没有return,则返回nil
  • 示例:
127.0.0.1:6379> eval "return redis.call('set',KEYS[1],'bar')" 1 foo
OK
13.5.3 redis-cli --eval

可以使用redis-cli --eval命令指定一个lua脚本文件去执行。

脚本文件(redis.lua)内容如下:

local num = redis.call('GET', KEYS[1]);
if not num then
	return 0;
else
	local res = num * ARGV[1];
	redis.call('SET',KEYS[1], res);
	return res;
end

在redis客户机,执行脚本命令

[root@localhost bin]# ./redis-cli --eval redis.lua lua:incrbyml , 8
(integer) 0
[root@localhost bin]# ./redis-cli incr lua:incrbyml
(integer) 1
[root@localhost bin]# ./redis-cli --eval redis.lua lua:incrbyml , 8
(integer) 8
[root@localhost bin]# ./redis-cli --eval redis.lua lua:incrbyml , 8
(integer) 64
[root@localhost bin]# ./redis-cli --eval redis.lua lua:incrbyml , 2
(integer) 128
[root@localhost bin]# ./redis-cli

命令格式说明

--eval:告诉redis客户端去执行后面的lua脚本
redis.lua:具体的lua脚本文件名称
lua:incrbymul : lua脚本中需要的key
8:lua脚本中需要的value

注意:上面命令中keys和values中间需要使用逗号隔开,并且逗号两边都要有空格

13.5.4 Redis+lua 秒杀

秒杀场景经常使用这个东西,主要利用他的原子性

  1. 首先定义redis数据结构
goodId:
{
"total":100,
"released":0;
}

其中goodId为商品id号,可根据此来查询相关的数据结构信息,total为总数,released为发放出 去的数量,可使用数为total-released

2.编写lua脚本

local n = tonumber(ARGV[1])
if not n or n == 0 then
return 0
end
local vals = redis.call("HMGET", KEYS[1], "total", "released");
local total = tonumber(vals[1])
local blocked = tonumber(vals[2])
if not total or not blocked then
return 0
end
if blocked + n <= total then
redis.call("HINCRBY", KEYS[1], "released", n)
return n;
end
return 0

执行脚本命令 EVAL script_string 1 goodId apply_count 若库存足够则返回申请的数量,否则返回0,不返回可满足的剩余数

3、spring boot调用

pom.xml

<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.0.1.RELEASE</version>
long count = redisHelper.getStrCache().execute(new
RedisCallback<Long>() {
@Nullable
@Override
public Long doInRedis(RedisConnection redisConnection) throws
DataAccessException {
long ret =
redisConnection.eval(script.getScriptAsString().getBytes(),
ReturnType.INTEGER, 1, key.getBytes(), String.valueOf(count).getBytes());
return ret;
}
});

  1. redis->database

    ​ 针对redis到databases的更新,思考了很久,没有找到较好的解决办法,先采用定时任务异步更 新。至于数据是否丢失的问题,如果redis挂了,重启后redis会恢复数据,等下次定时任务就可以 将数据库中的数据保持一致,缺点是redis挂了秒杀活动会失败。

至于redis到database更新方案: redis存一份相关hash键名单表,通过读取名单表来读取更新 通过流式读取databases中的表来读取更新。

十四、redis事务

两个客户端各自发送多条事务,client1,client2 谁先谁后,如何保证原子性。

14.1事务演示

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set s1 111
QUEUED
127.0.0.1:6379> hset set1 name zhangsan
QUEUED
127.0.0.1:6379> exec
1) OK
2) (integer) 1
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set s2 222
QUEUED
127.0.0.1:6379> hset set2 age 20
QUEUED
127.0.0.1:6379> discard
OK
127.0.0.1:6379> exec
(error) ERR EXEC without MULTI
127.0.0.1:6379> watch s1
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set s1 555
QUEUED
127.0.0.1:6379> exec # 此时在没有exec之前,通过另一个命令窗口对监控的s1字段
进行修改
(nil)
127.0.0.1:6379> get s1
111

14.2 事务失败处理

  • Redis 语法错误 整个事务的命令在队列里都清除
  • Redis 运行错误 在队列里正确的命令可以执行 (弱事务性) 弱事务性 : 1、在队列里正确的命令可以执行 (非原子操作) 2、不支持回滚
  • Redis 不支持事务回滚(为什么呢) 1、大多数事务失败是因为语法错误或者类型错误,这两种错误,在开发阶段都是可以预见的 2、 Redis 为了性能方面就忽略了事务回滚。 (回滚记录历史版本)

14.3 Redis乐观锁

乐观锁基于CAS(Compare And Swap)思想(比较并替换),是不具有互斥性,不会产生锁等待而消耗资源,但是需要反复的重试,但也是因为重试的机制,能比较快的响应。因此我们可以利用redis来实现乐观锁。

具体思路如下:

1、利用redis的watch功能,监控这个redisKey的状态值 。WATCH命令可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行。监控一直持续到EXEC命令(事务中的命令是在EXEC之后才执行的,所以在MULTI命令后可以修改WATCH监控的键值)

2、获取redisKey的值

3、创建redis事务

4、给这个key的值+1

5、然后去执行这个事务,如果key的值被修改过则回滚,key不加1

public void watch() {
	try {
	String watchKeys = "watchKeys";
	//初始值 value=1
	jedis.set(watchKeys, 1);
	//监听key为watchKeys的值
	jedis.watch(watchkeys);
	//开启事务
	Transaction tx = jedis.multi();
	//watchKeys自增加一
	tx.incr(watchKeys);
	//执行事务,如果其他线程对watchKeys中的value进行修改,则该事务将不会执行
	//通过redis事务以及watch命令实现乐观锁
	List<Object> exec = tx.exec();
	if (exec == null) {
	System.out.println("事务未执行");
	} else {
	System.out.println("事务成功执行,watchKeys的value成功修改");
	}
	} catch (Exception e) {
	e.printStackTrace();
	} finally {
	jedis.close();
	}
}

14.4 Redis乐观锁实现秒杀

public class SecKill {
	public static void main(String[] arg) {
		String redisKey = "second";
		ExecutorService executorService = 	Executors.newFixedThreadPool(20);
	try {
		Jedis jedis = new Jedis("127.0.0.1", 6378);
		// 初始值
		jedis.set(redisKey, "0");
		jedis.close();
	} catch (Exception e) {
		e.printStackTrace();
	}
	for (int i = 0; i < 1000; i++) {
		executorService.execute(() -> {
		Jedis jedis1 = new Jedis("127.0.0.1", 6378);
		try {
			jedis1.watch(redisKey);
			String redisValue = jedis1.get(redisKey);
            int valInteger = Integer.valueOf(redisValue);
			String userInfo = UUID.randomUUID().toString();
			// 没有秒完
			if (valInteger < 20) {
			Transaction tx = jedis1.multi();
			tx.incr(redisKey);
			List list = tx.exec();
			// 秒成功 失败返回空list而不是空
			if (list != null && list.size() > 0) {
			System.out.println("用户:" + userInfo + ",秒杀成
功!当前成功人数:" + (valInteger + 1));
			}
			// 版本变化,被别人抢了。
			else {
			System.out.println("用户:" + userInfo + ",秒杀失
			败");
			}
		}
	// 秒完了
	else {
		System.out.println("已经有20人秒杀成功,秒杀结束");
			}
		} catch (Exception e) {
		e.printStackTrace();
	} finally {
	jedis1.close();
	}
	});
	}
		executorService.shutdown();
	}
}

十五、redis分布式锁

15.1 业务场景

  1. 库存超卖
  2. 防止用户重复下单
  3. MQ消息去重
  4. 订单操作变更

这些业务场景的共同特征都是:共享资源竞争。

解决方法:共享资源互斥,共享资源串行化

=》也就等于锁的问题

锁的处理

1:单应用中使用锁:(单进程多线程)

synchronize、ReentrantLock

2:分布式应用中使用锁:(多进程多线程)

分布式锁是控制分布式系统之间同步访问共享资源的一种方式。

15.2 分布式锁介绍

分布式锁

分布式锁的状态

  1. 客户端通过竞争获取锁才能对共享资源进行操作(①获取锁);
  2. 当持有锁的客户端对共享资源进行操作时(②占有锁)
  3. 其他客户端都不可以对这个资源进行操作(③阻塞)
  4. . 直到持有锁的客户端完成操作(④释放锁);

分布式锁特点

  • 互斥性: 在任意时刻,只有一个客户端可以持有锁(排他性)
  • 高可用,具有容错性:只要锁服务集群中的大部分节点正常运行,客户端就可以进行加锁解锁操作
  • 避免死锁:具备锁失效机制,锁在一段时间之后一定会释放。(正常释放或超时释放)
  • 加锁和解锁为同一个客户端:一个客户端不能释放其他客户端加的锁了

15.3 分布式锁的实现方式

  • 基于数据库实现分布式锁
  • 基于zookeeper时节点的分布式锁
  • 基于Redis的分布式锁
  • 基于Etcd的分布式锁

15.4 Redis方式实现分布式锁

获取锁

redis分布式锁

分布式锁2

释放锁

redis*lua脚本

public static boolean releaseLock(String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then 			return
		redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script,
		Collections.singletonList(lockKey), Collections.singletonList(requestId));
	if (result.equals(1L)) {
		return true;
	}
	return false;
}

15.5 Redis分布式锁优缺点

优点

Redis是基于内存存储,并发性能好。

缺点

需要考虑原子性、超时、误删等情形。

获锁失败时,客户端只能自旋等待,

在高并发情况下,性能消耗比较大。

本质分析

CAP原则又称CAP定理,指的是在一个分布式系统中, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼。

一致性(C):在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本)

可用性(A):保证每个请求不管成功或者失败都有响应。

分区容忍性(P):系统中任意信息的丢失或失败不会影响系统的继续运作。

​ Redis集群在分布式系统中是一种AP模型,无法保证在主节点宕机时自动完成数据一致性的同步操作,因此在业务要求保证一致性的场景中,Redis的分布式锁会在主节点宕机的情况下丢失锁信息而出现重复上锁的极端情况。

​ 在分布式环境下不可能满足三者共存,只能满足其中的两者共存,在分布式下P不能舍弃(舍弃P就是单 机了)。 所以只能是CP(强一致性模型)和AP(高可用模型)。 分布式锁是CP模型,Redis集群是AP模型。 (base) 为什么还可以用Redis实现分布式锁?

答案:和业务有关。当业务不需要数据强一致性时,比如:社交场景,就可以使用Redis实现分布式锁 当业务必须要数据的强一致性,即不允许重复获得锁,比如金融场景(重复下单,重复转账)就不要使用,可以使用CP模型实现,比如:zookeeper和etcd。

15.6 生产环境中的分布式锁

​ 目前落地生产环境用分布式锁,一般采用开源框架,比如Redisson。下面来讲一下Redisson对Redis分布式锁的实现。

15.6.1 Redisson分布式锁的实现原理

redisson

加锁机制

如果该客户端面对的是一个redis cluster集群,他首先会根据hash节点选择一台机器。 发送lua脚本到redis服务器上,脚本如下:

"if (redis.call('exists',KEYS[1])==0) then "+
"redis.call('hset',KEYS[1],ARGV[2],1) ; "+
"redis.call('pexpire',KEYS[1],ARGV[1]) ; "+
"return nil; end ;" +
"if (redis.call('hexists',KEYS[1],ARGV[2]) ==1 ) then "+
"redis.call('hincrby',KEYS[1],ARGV[2],1) ; "+
"redis.call('pexpire',KEYS[1],ARGV[1]) ; "+
"return nil; end ;" +
"return redis.call('pttl',KEYS[1]) ;"

lua的作用:保证这段复杂业务逻辑执行的原子性。

lua的解释:

KEYS[1]) : 加锁的key

ARGV[1] : key的生存时间,默认为30秒

ARGV[2] : 加锁的客户端ID (UUID.randomUUID()) + “:” + threadId)

第一段if判断语句,就是用“exists myLock”命令判断一下,如果你要加锁的那个锁key不存在的话,你就 进行加锁。如何加锁呢?很简单,用下面的命令:

hset myLock

8743c9c0-0795-4907-87fd-6c719a6b4586:1 1

通过这个命令设置一个hash数据结构,这行命令执行后,会出现一个类似下面的数据结构:

myLock :{“8743c9c0-0795-4907-87fd-6c719a6b4586:1”:1 }

上述就代表“8743c9c0-0795-4907-87fd-6c719a6b4586:1”这个客户端对“myLock”这个锁key完成了加 锁。

接着会执行“pexpire myLock 30000”命令,设置myLock这个锁key的生存时间是30秒。

锁互斥机制

那么在这个时候,如果客户端2来尝试加锁,执行了同样的一段lua脚本,会咋样呢? 很简单,第一个if判断会执行“exists myLock”,发现myLock这个锁key已经存在了。 接着第二个if判断,判断一下,myLock锁key的hash数据结构中,是否包含客户端2的ID,但是明显不 是的,因为那里包含的是客户端1的ID。 所以,客户端2会获取到pttl myLock返回的一个数字,这个数字代表了myLock这个锁key的剩余生存时 间。比如还剩15000毫秒的生存时间。 此时客户端2会进入一个while循环,不停的尝试加锁。

自动延时机制

只要客户端1一旦加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一 下,如果客户端1还持有锁key,那么就会不断的延长锁key的生存时间。

可重入锁机制

第一个if判断肯定不成立,“exists myLock”会显示锁key已经存在了。 第二个if判断会成立,因为myLock的hash数据结构中包含的那个ID,就是客户端1的那个ID,也就是 “8743c9c0-0795-4907-87fd-6c719a6b4586:1” 此时就会执行可重入加锁的逻辑,他会用: incrby myLock 8743c9c0-0795-4907-87fd-6c71a6b4586:1 1 通过这个命令,对客户端1的加锁次数,累加1。数据结构会变成: myLock :{“8743c9c0-0795-4907-87fd-6c719a6b4586:1”:2 }

释放锁机制

执行lua脚本如下:

#如果key已经不存在,说明已经被解锁,直接发布(publish)redis消息
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; " +
"end;" +
# key和field不匹配,说明当前客户端线程没有持有锁,不能主动解锁。
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
" +
"return nil;" +
"end; " +
# 将value减1
"local counter = redis.call('hincrby', KEYS[1],
ARGV[3], -1); " +
# 如果counter>0说明锁在重入,不能删除key
"if (counter > 0) then " +
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
"return 0; " +
# 删除key并且publish 解锁消息
"else " +
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; "+
"end; " +
"return nil;",

– KEYS[1] :需要加锁的key,这里需要是字符串类型。

– KEYS[2] :redis消息的ChannelName,一个分布式锁对应唯一的一个channelName: “redisson_lockchannel{” + getName() + “}”

– ARGV[1] :reids消息体,这里只需要一个字节的标记就可以,主要标记redis的key已经解锁,再结合 redis的Subscribe,能唤醒其他订阅解锁消息的客户端线程申请锁。

– ARGV[2] :锁的超时时间,防止死锁

– ARGV[3] :锁的唯一标识,也就是刚才介绍的 id(UUID.randomUUID()) + “:” + threadId 如果执行lock.unlock(),就可以释放分布式锁,此时的业务逻辑也是非常简单的。 其实说白了,就是每次都对myLock数据结构中的那个加锁次数减1。 如果发现加锁次数是0了,说明这个客户端已经不再持有锁了,此时就会用: “del myLock”命令,从redis里删除这个key。 然后呢,另外的客户端2就可以尝试完成加锁了。

15.6.2 Redisson分布式锁的使用

pom.xml

<dependency>
	<groupId>org.redisson</groupId>
	<artifactId>redisson</artifactId>
	<version>2.7.0</version>
</dependency>

配置Redisson

public class RedissonManager {
	private static Config config = new Config();
	//声明redisso对象
	private static Redisson redisson = null;
	//实例化redisson
	static{
		config.useClusterServers()
		// 集群状态扫描间隔时间,单位是毫秒
		.setScanInterval(2000)
		//cluster方式至少6个节点(3主3从,3主做sharding,3从用来保证主宕机后可以高可用)
		.addNodeAddress("redis://127.0.0.1:6379" )
		.addNodeAddress("redis://127.0.0.1:6380")
		.addNodeAddress("redis://127.0.0.1:6381")
		.addNodeAddress("redis://127.0.0.1:6382")
		.addNodeAddress("redis://127.0.0.1:6383")
		.addNodeAddress("redis://127.0.0.1:6384");
		//得到redisson对象
		redisson = (Redisson) Redisson.create(config);
	}
	//获取redisson对象的方法
	public static Redisson getRedisson(){
		return redisson;
	}
}

锁的获取和释放

public class DistributedRedisLock {
//从配置类中获取redisson对象
private static Redisson redisson = RedissonManager.getRedisson();
private static final String LOCK_TITLE = "redisLock_";
//加锁
public static boolean acquire(String lockName){
//声明key对象
String key = LOCK_TITLE + lockName;
//获取锁对象
RLock mylock = redisson.getLock(key);
//加锁,并且设置锁过期时间3秒,防止死锁的产生 uuid+threadId
mylock.lock(2,3,TimeUtil.SECOND);
//加锁成功
return true;
}
//锁的释放
public static void release(String lockName){
//必须是和加锁时的同一个key
String key = LOCK_TITLE + lockName;
//获取锁对象
RLock mylock = redisson.getLock(key);
//释放锁(解锁)
mylock.unlock();
}
}

业务逻辑中使用

public String discount() throws IOException{
String key = "test123";
//加锁
DistributedRedisLock.acquire(key);
//执行具体业务逻辑
dosoming
//释放锁
DistributedRedisLock.release(key);
//返回结果
return soming;
}

十六、Redis性能调优

16.1 设计优化

16.1.1 估算Redis内存使用量

​ 要估算redis中的数据占据的内存大小,需要对redis的内存模型有比较全面的了解,包括第一节课介绍 的hashtable、sds、redisobject、各种对象类型的编码方式等。

下面以最简单的字符串类型来进行说明。

​ 假设有90000个键值对,每个key的长度是12个字节,每个value的长度也是12个字节(且key和 value都不是整数);

设计优化

​ 下面来估算这90000个键值对所占用的空间。在估算占据空间之前,首先可以判定字符串类型使用的编码方式:embstr。

​ 90000个键值对占据的内存空间主要可以分为两部分:一部分是90000个dictEntry占据的空间;一部分 是键值对所需要的bucket空间。

每个dictEntry占据的空间包括

  1. 一个dictEntry结构,24字节,jemalloc会分配32字节的内存块(64位操作系统下,一个指针8字节,一个dictEntry由三个指针组成)
  2. 一个key,12字节,所以SDS(key)需要12+4=16个字节([SDS的长度=4+字符串长度),jemalloc 会分配16字节的内存块
  3. 一个redisObject,16字节,jemalloc会分配16字节的内存块
  4. 综上,一个dictEntry所占据的空间需要32+16+16+16=80个字节。

bucket空间

​ bucket数组的大小为大于90000的最小的2^n,是131072;每个bucket元素(bucket中存储的都是指 针元素)为8字节(因为64位系统中指针大小为8字节)。 因此,可以估算出这90000个键值对占据的内存大小为:90000X80 + 131072X8 = 82488576

​ 作为对比将key和value的长度由12字节增加到13字节,则对应的SDS变为17个字节,jemalloc会 分配32个字节,因此每个dictEntry占用的字节数也由80字节变为112字节。此时估算这90000个 键值对占据内存大小为:90000X112 + 131072X8 = 11128576。

16.1.2 优化内存占用

​ 了解redis的内存模型,对优化redis内存占用有很大帮助。下面介绍几种优化场景和方式(4种)缩短 键值对的存储长度。

(1)利用jemalloc特性进行优化

​ 上一小节所讲述的90000个键值便是一个例子。由于jemalloc分配内存时数值是不连续的,因此 key/value字符串变化一个字节,可能会引起占用内存很大的变动;在设计时可以利用这一点。

​ 例如,如果key的长度如果是13个字节,则SDS为17字节,jemalloc分配32字节;此时将key长度缩减为12个字节,则SDS为16字节,jemalloc分配16字节;则每个key所占用的空间都可以缩小一 半。

(2)使用整型/长整型

​ 如果是整型/长整型,Redis会使用int类型(8字节)存储来代替字符串,可以节省更多空间。因此在可 以使用长整型/整型代替字符串的场景下,尽量使用长整型/整型。

(3)共享对象

​ 利用共享对象,可以减少对象的创建(同时减少了redisObject的创建),节省内存空间。目前redis中 的共享对象只包括10000个整数(0-9999);可以通过调整REDIS_SHARED_INTEGERS参数提高共享对象的个数;

​ 例如将REDIS_SHARED_INTEGERS调整到20000,则0-19999之间的对象都可以共享 论坛网站在redis中存储了每个帖子的浏览数,而这些浏览数绝大多数分布在0-20000之间,这时 候通过适当增大REDIS_SHARED_INTEGERS参数,便可以利用共享对象节省内存空间。

(4)缩短键值对的存储长度

​ 键值对的长度是和性能成反比的,比如我们来做一组写入数据的性能测试,执行结果如下:

优化01

​ 从以上数据可以看出,在 key 不变的情况下,value 值越大操作效率越慢,因为 Redis 对于同一种数据 类型会使用不同的内部编码进行存储,比如字符串的内部编码就有三种:int(整数编码)、raw(优化 内存分配的字符串编码)、embstr(动态字符串编码),这是因为 Redis 的作者是想通过不同编码实现效率和空间的平衡,然而数据量越大使用的内部编码就越复杂,而越是复杂的内部编码存储的性能就越低。

这还只是写入时的速度,当键值对内容较大时,还会带来另外几个问题:

  • 内容越大需要的持久化时间就越长,需要挂起的时间越长,Redis 的性能就会越低;
  • 内容越大在网络上传输的内容就越多,需要的时间就越长,整体的运行速度就越低;
  • 内容越大占用的内存就越多,就会更频繁的触发内存淘汰机制,从而给 Redis 带来了更多的运行 负担。

因此在保证完整语义的同时,我们要尽量的缩短键值对的存储长度,必要时要对数据进行序列化和压缩 再存储,以 Java 为例,序列化我们可以使用 protostuff 或 kryo,压缩我们可以使用 snappy。

16.2设置键值的过期时间

我们应该根据实际的业务情况,对键值设置合理的过期时间,这样 Redis 会帮你自动清除过期的键 值对,以节约对内存的占用,以避免键值过多的堆积,频繁的触发内存淘汰策略。 Redis 有四个不同的命令可以用于设置键的生存时间(键可以存在多久)或过期时间(键什么时候会被删除)

  • EXPlRE 命令用于将键key 的生存时间设置为ttl 秒。
  • PEXPIRE 命令用于将键key 的生存时间设置为ttl 毫秒。
  • EXPIREAT < timestamp> 命令用于将键key 的过期时间设置为timestamp所指定的秒数时间戳。
  • PEXPIREAT < timestamp > 命令用于将键key 的过期时间设置为timestamp所指定的毫秒数时间戳。
127.0.0.1:6379> set key1 'value1'
OK
127.0.0.1:6379> expire key1 20
(integer) 1
127.0.0.1:6379> get key1 "value1" 127.0.0.1:6379> get key1 "value1"
127.0.0.1:6379> get key1 (nil) 

16.3 限制Redis内存大小(重点)

16.3.1最大缓存
maxmemory 1048576
maxmemory 1048576B
maxmemory 1000KB
maxmemory 100MB
maxmemory 1GB
maxmemory 1000K
maxmemory 100M
maxmemory 1G

​ 没有指定最大缓存,如果有新的数据添加,超过最大内存,则会使redis崩溃,所以一定要设置。

​ 在 64 位操作系统中 Redis 的内存大小是没有限制的,也就是配置项 maxmemory 是被注释掉的, 这样就会导致在物理内存不足时,使用 swap 空间既交换空间,而当操心系统将 Redis 所用的内存 分页移至 swap 空间时,将会阻塞 Redis 进程,导致 Redis 出现延迟,从而影响 Redis 的整体性 能。因此我们需要限制 Redis 的内存大小为一个固定的值,当 Redis 的运行到达此值时会触发内 存淘汰策略,内存淘汰策略在 Redis 4.0 之后有 8 种

16.3.2 LRU原理

​ LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思 想是“如果数据最近被访问过,那么将来被访问的几率也更高”。 最常见的实现是使用一个链表保存缓存数据,详细算法实现如下:

LRU

  1. 新数据插入到链表头部;
  2. . 每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
  3. . 当链表满的时候,将链表尾部的数据丢弃。

在Java中可以使用LinkHashMap去实现LRU

LRU2

案例分析

让我们以用户信息的需求为例,来演示一下LRU算法的基本思路:

LRU3

lru4

以上,就是LRU算法的基本思路。

https://www.itcodemonkey.com/article/11153.html

16.3.3 LFU原理

​ LFU,全称是:Least Frequently Used,最不经常使用策略,在一段时间内,数据被使用频次最少的,优先 被淘汰。最少使用LFU)是一种用于管理计算机内存的缓存算法。主要是记录和追踪内存块的使用次数,当缓存已满并且需要更多空间时,系统将以最低内存块使用频率清除内存.采用LFU算法的最简单方法是为每个加载到缓存的块分配一个计数器。每次引用该块时,计数器将增加一。当缓存达到容量 并有一个新的内存块等待插入时,系统将搜索计数器最低的块并将其从缓存中删除(本段摘自维基百科)

LRU5

解释:上面这个图就是一个LRU的简单实现思路,在链表的开始插入元素,然后每插入一次计数一次,接着 按照次数重新排序链表,如果次数相同的话,按照插入时间排序,然后从链表尾部选择淘汰的数据~

LRU和LFU侧重点不同,LRU主要体现在对元素的使用时间上,而LFU主要体现在对元素的使用频次上。LFU的缺陷是:在短期的时间内,对某些缓存的访问频次很高,这些缓存会立刻晋升为热点数据, 而保证不会淘汰,这样会驻留在系统内存里面。而实际上,这部分数据只是短暂的高频率访问,之后将 会长期不访问,瞬时的高频访问将会造成这部分数据的引用频率加快,而一些新加入的缓存很容易被快速 删除,因为它们的引用频率很低。

13.3.4Redis缓存淘汰策略

​ redis 内存数据集大小上升到一定大小的时候,就会实行数据淘汰策略。maxmemory-policy voltile-lru,支持热配置 内存淘汰策略在 Redis 4.0 之后有 8 种

  1. noeviction:不淘汰任何数据,当内存不足时,新增操作会报错,Redis 默认内存淘汰策略;
  2. allkeys-lru:淘汰整个键值中最久未使用的键值;
  3. allkeys-random:随机淘汰任意键值;
  4. volatile-lru:淘汰所有设置了过期时间的键值中最久未使用的键值
  5. volatile-random:随机淘汰设置了过期时间的任意键值;
  6. volatile-ttl:优先淘汰更早过期的键值。

在 Redis 4.0 版本中又新增了 2 种淘汰策略:

  1. . volatile-lfu:淘汰所有设置了过期时间的键值中,最少使用的键值;
  2. . allkeys-lfu:淘汰整个键值中最少使用的键值。

其中 allkeys-xxx 表示从所有的键值中淘汰数据,而 volatile-xxx 表示从设置了过期键的键值中淘汰数 据。 我们可以根据实际的业务情况进行设置,默认的淘汰策略不淘汰任何数据,在新增时会报错。

16.4 使用lazy free特性

​ lazy free 特性是 Redis 4.0 新增的一个非常使用的功能,它可以理解为惰性删除或延迟删除。意思是在 删除的时候提供异步延时释放键值的功能,把键值释放操作放在 BIO(Background I/O) 单独的子线程处理中,以减少删除删除对 Redis 主线程的阻塞,可以有效地避免删除 big key 时带来的性能和可用性问题。

lazy free 对应了 4 种场景,默认都是关闭的:

lazyfree-lazy-eviction no
lazyfree-lazy-expire no
lazyfree-lazy-server-del no
slave-lazy-flush no

它们代表的含义如下:

  • lazyfree-lazy-eviction:表示当 Redis 运行内存超过 maxmeory 时,是否开启 lazy free 机制删 除;
  • lazyfree-lazy-expire:表示设置了过期时间的键值,当过期之后是否开启 lazy free 机制删除;
  • lazyfree-lazy-server-del:有些指令在处理已存在的键时,会带有一个隐式的 del 键的操作,比如 rename 命令,当目标键已存在,Redis 会先删除目标键,如果这些目标键是一个 big key,就会 造成阻塞删除的问题,此配置表示在这种场景中是否开启 lazy free 机制删除;
  • slave-lazy-flush:针对 slave(从节点) 进行全量数据同步,slave 在加载 master 的 RDB 文件前, 会运行 flushall 来清理自己的数据,它表示此时是否开启 lazy free 机制删除。

建议开启其中的 lazyfree-lazy-eviction、lazyfree-lazy-expire、lazyfree-lazy-server-del 等配置,这样 就可以有效的提高主线程的执行效率.

16.5 禁用长耗时的查询命令

​ Redis 绝大多数读写命令的时间复杂度都在 O(1) 到 O(N) 之间,在官方文档对每命令都有时间复杂度说明,如下图

长耗时

​ 其中 O(1) 表示可以安全使用的,而 O(N) 就应该当心了,N 表示不确定,数据越大查询的速度可能会越 慢。因为 Redis 只用一个线程来做数据查询,如果这些指令耗时很长,就会阻塞 Redis,造成大量延时。

要避免 O(N) 命令对 Redis 造成的影响,可以从以下几个方面入手改造:

  • 决定禁止使用 keys 命令;
  • 避免一次查询所有的成员,要使用 scan 命令进行分批的,游标式的遍历;
  • 通过机制严格控制 Hash、Set、Sorted Set 等结构的数据大小;
  • 将排序、并集、交集等操作放在客户端执行,以减少 Redis 服务器运行压力;
  • 删除 (del) 一个大数据的时候,可能会需要很长时间,所以建议用异步删除的方式 unlink,它会启动一个新的线程来删除目标数据,而不阻塞 Redis 的主线程。

16.6 使用slowlog优化耗时命令

​ 我们可以使用 slowlog 功能找出最耗时的 Redis 命令进行相关的优化,以提升 Redis 的运行速度, 慢查询有两个重要的配置项:

  • slowlog-log-slower-than :用于设置慢查询的评定时间,也就是说超过此配置项的命令,将会 被当成慢操作记录在慢查询日志中,它执行单位是微秒 (1 秒等于 1000000 微秒);
  • slowlog-max-len :用来配置慢查询日志的最大记录数。

​ 我们可以根据实际的业务情况进行相应的配置,其中慢日志是按照插入的顺序倒序存入慢查询日志中, 我们可以使用 slowlog get n 来获取相关的慢查询日志,再找到这些慢查询对应的业务进行相关的优化。

16.7 避免大量数据同时失效

​ Redis 过期键值删除使用的是贪心策略,它每秒会进行 10 次过期扫描,此配置可在 redis.conf 进行配 置,默认值是 hz 10 ,Redis 会随机抽取 20 个值,删除这 20 个键中过期的键,如果过期 key 的比例 超过 25% ,重复执行此流程,如下图所示:优化05

​ 如果在大型系统中有大量缓存在同一时间同时过期,那么会导致 Redis 循环多次持续扫描删除过期字典,直到过期字典中过期键值被删除的比较稀疏为止,而在整个执行过程会导致 Redis 的读写出现明显 的卡顿,卡顿的另一种原因是内存管理器需要频繁回收内存页,因此也会消耗一定的 CPU。

​ 为了避免这种卡顿现象的产生,我们需要预防大量的缓存在同一时刻一起过期,就简单的解决方案就是 在过期时间的基础上添加一个指定范围的随机数。

16.8 检查数据持久化策略

Redis 的持久化策略是将内存数据复制到硬盘上,这样才可以进行容灾恢复或者数据迁移,但维护此持 久化的功能,需要很大的性能开销。

Redis 有 3 种持久化的方式:

  • RDB(Redis DataBase,快照方式)将某一个时刻的内存数据,以二进制的方式写入磁盘;
  • AOF(Append Only File,文件追加方式),记录所有的操作命令,并以文本的形式追加到文件中;
  • 混合持久化方式,Redis 4.0 之后新增的方式,混合持久化是结合了 RDB 和 AOF 的优点,在写入 的时候,先把当前的数据以 RDB 的形式写入文件的开头,再将后续的操作命令以 AOF 的格式存入 文件,这样既能保证 Redis 重启时的速度,又能减低数据丢失的风险。

RDB 和 AOF 持久化各有利弊,RDB 可能会导致一定时间内的数据丢失,而 AOF 由于文件较大则会影 响 Redis 的启动速度,为了能同时拥有 RDB 和 AOF 的优点,Redis 4.0 之后新增了混合持久化的方 式,因此我们在必须要进行持久化操作时,应该选择混合持久化的方式。 需要注意的是,在非必须进行持久化的业务中,可以关闭持久化,这样可以有效的提升 Redis 的运行速 度,不会出现间歇性卡顿的困扰。

16.9 使用 Pipeline 批量操作数据

Pipeline (管道技术) 是客户端提供的一种批处理技术,用于一次处理多个 Redis 命令,从而提高整个交 互的性能。

16.10 客户端使用优化

在客户端的使用上我们除了要尽量使用 Pipeline 的技术外,还需要注意要尽量使用 Redis 连接池,而不 是频繁创建销毁 Redis 连接,这样就可以减少网络传输次数和减少了非必要调用指令。

import redis.clients.jedis.JedisPool;

import redis.clients.jedis.JedisPoolConfig;

16.11 使用分布式架构来增加读写能力

Redis 分布式架构有重要的手段:

  • 主从同步
  • 哨兵模式
  • Redis Cluster 集群

​ 使用主从同步功能我们可以把写入放到主库上执行,把读功能转移到从服务上,因此就可以在单位时间 内处理更多的请求,从而提升的 Redis 整体的运行速度。

​ 而哨兵模式是对于主从功能的升级,但当主节点奔溃之后,无需人工干预就能自动恢复 Redis 的正常使 用。

​ Redis Cluster 是 Redis 3.0 正式推出的,Redis 集群是通过将数据库分散存储到多个节点上来平衡各个 节点的负载压力。

​ Redis Cluster 采用虚拟哈希槽分区,所有的键根据哈希函数映射到 0 ~ 16383 整数槽内,计算公式: slot = CRC16(key) & 16383,每一个节点负责维护一部分槽以及槽所映射的键值数据。这样 Redis 就可 以把读写压力从一台服务器,分散给多台服务器了,因此性能会有很大的提升。

​ Redis Cluster 应该是首选的实现方案,它可以把读写压力自动的分担给更多的服务器,并且拥有自动容灾的能力。

16.12 使用物理机而非虚拟机

​ 在虚拟机中运行 Redis 服务器,因为和物理机共享一个物理网口,并且一台物理机可能有多个虚拟 机在运行,因此在内存占用上和网络延迟方面都会有很糟糕的表现,我们可以通过 ./redis-cli – intrinsic-latency 100 命令查看延迟时间,如果对 Redis 的性能有较高要求的话,应尽可能在物理机上直接部署 Redis 服务器。

16.13 禁用THP特性

Linux kernel 在 2.6.38 内核增加了 Transparent Huge Pages (THP) 特性 ,支持大内存页 2MB 分配, 默认开启。

当开启了 THP 时,fork 的速度会变慢,fork 之后每个内存页从原来 4KB 变为 2MB,会大幅增加重写 期间父进程内存消耗。同时每次写命令引起的复制内存页单位放大了 512 倍,会拖慢写操作的执行时 间,导致大量写操作慢查询。例如简单的 incr 命令也会出现在慢查询中,因此 Redis 建议将此特性进 行禁用,禁用方法如下:

echo never > /sys/kernel/mm/transparent_hugepage/enabled

为了使机器重启后 THP 配置依然生效,可以在 /etc/rc.local 中追加

echo never >/sys/kernel/mm/transparent_hugepage/enabled 。

十七、缓存穿透、缓存击穿、缓存雪崩

17.1 缓存处理流程

​ 前台请求,后台先从缓存中取数据,取到直接返回结果,取不到时从数据库中取,数据库取到更新缓存,并返回结果,数据库也没取到,那直接返回空结果。

缓存

17.2 缓存穿透

描述:

​ 缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。

解决方案:

  1. 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
  2. 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击

17.3 缓存击穿

描述

​ 缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。

解决方案

  1. 设置热点数据永远不过期。
  2. 加互斥锁。

17.4 缓存雪崩

描述

	缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是,    缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

解决方案

  1. 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
  2. 如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中。
  3. 设置热点数据永远不过期。

十八、 module的使用

redis还有一些拓展模块可以使用

RedisModule 地址

18.1 布隆过滤器

本质上布隆过滤器是一种数据结构,比较巧妙的概率型数据结构(probabilistic data structure),特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”

我们可以将库中所有商品条件,放入布隆过滤器中,先经过布隆过滤器查,查不到说明一定不存在,则丢弃。

18.1.1 安装
wget https://github.com/RedisBloom/RedisBloom/archive/refs/heads/master.zip
apt install unzip   # ubuntu 安装zip解压工具
unzip master.zip    #解压布隆过滤器源码
make 
#修改配置文件 loadmodule /path/to/redisbloom.so


18.1.2 简单命令

Redis中,布隆过滤器有两个基本命令,分别是:

  • bf.add:添加元素到布隆过滤器中,类似于集合的sadd命令,不过bf.add命令只能一次添加一个元素,如果想一次添加多个元素,可以使用bf.madd命令。
  • bf.exists:判断某个元素是否在过滤器中,类似于集合的sismember命令,不过bf.exists命令只能一次查询一个元素,如果想一次查询多个元素,可以使用bf.mexists命令。

十九、面试题

1.Redis 有哪些数据结构

  1. 能说一下他们的特性,还有分别的使用场景么?

    String 缓存功能 计数器 共享用户Session

    Hash:产品属性单一,还是爆款产品。

    List:粉丝列表 文章评论 Lrange, 基于list分页,基于Redis实现简单的高性能分页 可以搞一个简单的消息队列

    Set :基于Redis Set 进行全局去重。微博的共同好友。

    Zset:排行榜 热搜

  2. 单机会有瓶颈,怎么解决这个瓶颈?集群

  3. .哪他们之间是如何进行数据交互的?Redis是如何进行持久化的?Redis数据都在内存中,一断电或重启不就没有了吗?问的是主从以及持久化

  4. 哪你是如何选择持久方式的?主要在于RDB和AOF的理解 以及版本问题

  5. Redis还有其他保证集群高可用的方式吗?想问哨兵了

  6. 数据传输的时候网络断了或者服务器断了,怎么办?持久化 和 哨兵

  7. . 能说一下Redis的内存淘汰机制么?主动删除和惰性删除

  8. 如果,定期没删,我也没查询,那可咋整。缓存淘汰策略

  9. 哨兵机制的原理是什么?定时监视和自动故障迁移 监视机制 故障判断机制自动迁移机制

  10. . 哨兵组件的主要功能是什么?

  11. Redis的事务原理是什么?

  12. Redis事务为什么是“弱”事务? 非原子性 不支持回滚

  13. Redis为什么没有回滚操作? 语法和数据类型,不应该 ; 保证性能。

  14. 在Redis中整合lua有几种方式,你认为哪种更好,为什么? 三种方式 lua脚本调用Redis命令 保证原子性

  15. lua如何解决Redis并发问题? 原子性

  16. 介绍Redis下的分布式锁实现原理、优势劣势和使用场景.分布式锁的流程 获取锁的两种方式 释放锁使用LUA脚本 CAP了解

  17. Redis-Cluster和Redis主从+哨兵的区别,你认为哪个更好,为什么。

  18. . 什么情况下会造成缓存穿透,如何解决?主要是Redis没数据 造成大量数据查询 形成阻塞。 布隆过滤器或随机数

  19. 什么情况下会造成缓存雪崩,如何解决?大量KEY同时失效并且有大量并发。 使用时间戳,不一起失效

  20. 什么是缓存击穿,如何解决?热点数据失效,同时大量并发访问。 热点数据设置持久化 。 热点向ng和CDN 推。

  21. 什么情况下会造成数据库与Redis缓存数据不一致,如何解决?本质:数据源不一致。 业务可以偶尔不一致,采用最终一致性。 “缓存+数据库”必须保持一致性。读请求和写请求串行化 ,串到一个内存队列当中。

  22. . 那你了解的最经典的KV,DB读写模式什么样?读:先读缓存,缓存没有的话,就读数据库,然后把取出的数据放入缓存,同时返回响应 写:先更新数据库,然后再删除缓存

  23. . 为什么是删除缓存,而不是更新缓存?删除缓存,而不是更新缓存,就是一个lazy计算思想,不要每次重新做复杂的计算,而是等到它需 要的时候,再重新计算。

当开启了 THP 时,fork 的速度会变慢,fork 之后每个内存页从原来 4KB 变为 2MB,会大幅增加重写 期间父进程内存消耗。同时每次写命令引起的复制内存页单位放大了 512 倍,会拖慢写操作的执行时 间,导致大量写操作慢查询。例如简单的 incr 命令也会出现在慢查询中,因此 Redis 建议将此特性进 行禁用,禁用方法如下:

echo never > /sys/kernel/mm/transparent_hugepage/enabled

为了使机器重启后 THP 配置依然生效,可以在 /etc/rc.local 中追加

echo never >/sys/kernel/mm/transparent_hugepage/enabled 。

十七、缓存穿透、缓存击穿、缓存雪崩

17.1 缓存处理流程

​ 前台请求,后台先从缓存中取数据,取到直接返回结果,取不到时从数据库中取,数据库取到更新缓存,并返回结果,数据库也没取到,那直接返回空结果。

[外链图片转存中…(img-MnRgWgjt-1631585241743)]

17.2 缓存穿透

描述:

​ 缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。

解决方案:

  1. 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
  2. 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击

17.3 缓存击穿

描述

​ 缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。

解决方案

  1. 设置热点数据永远不过期。
  2. 加互斥锁。

17.4 缓存雪崩

描述

	缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是,    缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

解决方案

  1. 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
  2. 如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中。
  3. 设置热点数据永远不过期。

十八、 module的使用

redis还有一些拓展模块可以使用

RedisModule 地址

18.1 布隆过滤器

本质上布隆过滤器是一种数据结构,比较巧妙的概率型数据结构(probabilistic data structure),特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”

我们可以将库中所有商品条件,放入布隆过滤器中,先经过布隆过滤器查,查不到说明一定不存在,则丢弃。

18.1.1 安装
wget https://github.com/RedisBloom/RedisBloom/archive/refs/heads/master.zip
apt install unzip   # ubuntu 安装zip解压工具
unzip master.zip    #解压布隆过滤器源码
make 
#修改配置文件 loadmodule /path/to/redisbloom.so


18.1.2 简单命令

Redis中,布隆过滤器有两个基本命令,分别是:

  • bf.add:添加元素到布隆过滤器中,类似于集合的sadd命令,不过bf.add命令只能一次添加一个元素,如果想一次添加多个元素,可以使用bf.madd命令。
  • bf.exists:判断某个元素是否在过滤器中,类似于集合的sismember命令,不过bf.exists命令只能一次查询一个元素,如果想一次查询多个元素,可以使用bf.mexists命令。

十九、面试题

1.Redis 有哪些数据结构

  1. 能说一下他们的特性,还有分别的使用场景么?

    String 缓存功能 计数器 共享用户Session

    Hash:产品属性单一,还是爆款产品。

    List:粉丝列表 文章评论 Lrange, 基于list分页,基于Redis实现简单的高性能分页 可以搞一个简单的消息队列

    Set :基于Redis Set 进行全局去重。微博的共同好友。

    Zset:排行榜 热搜

  2. 单机会有瓶颈,怎么解决这个瓶颈?集群

  3. .哪他们之间是如何进行数据交互的?Redis是如何进行持久化的?Redis数据都在内存中,一断电或重启不就没有了吗?问的是主从以及持久化

  4. 哪你是如何选择持久方式的?主要在于RDB和AOF的理解 以及版本问题

  5. Redis还有其他保证集群高可用的方式吗?想问哨兵了

  6. 数据传输的时候网络断了或者服务器断了,怎么办?持久化 和 哨兵

  7. . 能说一下Redis的内存淘汰机制么?主动删除和惰性删除

  8. 如果,定期没删,我也没查询,那可咋整。缓存淘汰策略

  9. 哨兵机制的原理是什么?定时监视和自动故障迁移 监视机制 故障判断机制自动迁移机制

  10. . 哨兵组件的主要功能是什么?

  11. Redis的事务原理是什么?

  12. Redis事务为什么是“弱”事务? 非原子性 不支持回滚

  13. Redis为什么没有回滚操作? 语法和数据类型,不应该 ; 保证性能。

  14. 在Redis中整合lua有几种方式,你认为哪种更好,为什么? 三种方式 lua脚本调用Redis命令 保证原子性

  15. lua如何解决Redis并发问题? 原子性

  16. 介绍Redis下的分布式锁实现原理、优势劣势和使用场景.分布式锁的流程 获取锁的两种方式 释放锁使用LUA脚本 CAP了解

  17. Redis-Cluster和Redis主从+哨兵的区别,你认为哪个更好,为什么。

  18. . 什么情况下会造成缓存穿透,如何解决?主要是Redis没数据 造成大量数据查询 形成阻塞。 布隆过滤器或随机数

  19. 什么情况下会造成缓存雪崩,如何解决?大量KEY同时失效并且有大量并发。 使用时间戳,不一起失效

  20. 什么是缓存击穿,如何解决?热点数据失效,同时大量并发访问。 热点数据设置持久化 。 热点向ng和CDN 推。

  21. 什么情况下会造成数据库与Redis缓存数据不一致,如何解决?本质:数据源不一致。 业务可以偶尔不一致,采用最终一致性。 “缓存+数据库”必须保持一致性。读请求和写请求串行化 ,串到一个内存队列当中。

  22. . 那你了解的最经典的KV,DB读写模式什么样?读:先读缓存,缓存没有的话,就读数据库,然后把取出的数据放入缓存,同时返回响应 写:先更新数据库,然后再删除缓存

  23. . 为什么是删除缓存,而不是更新缓存?删除缓存,而不是更新缓存,就是一个lazy计算思想,不要每次重新做复杂的计算,而是等到它需 要的时候,再重新计算。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值