Redis 基础知识点总结

关系型数据库 VS 非关系型数据库(NoSQL)

关系型数据库

我们过去使用的 mysql、Oracle 都属于关系型数据库。关系型数据库的特点是数据表之间可以存在联系,表内每列数据也存在关联,同时支持事务、复杂的锁机制,这样可以支持复杂操作,在查询时也可以很快得到与之相关联的数据,但同时这些也成为限制数据库速度的因素,在存储大数据的表中进行查询修改、拓展表时会格外消耗时间。在过去受硬件水平的限制,系统架构往往比较简单,并发量也比较小,但随着硬件水平的提高,系统变得越拉越庞大,需要存储的数据量也越来越大,很多业务都需要在海量数据中快速查找到所需要的数据,此时靠关系型数据库已经无法满足了,所以提出了非关系型数据库的概念。

 

非关系型数据库

非关系型数据库中的数据间没有关联关系,数据结构简单,在数据的增删改查时速度都比较快,缺点是不能支持复杂的操作,比如事务的ACID、复杂的锁机制,也因为数据间没有关联关系所以在查询符合条件的数据会很快。所以我们通常只使用 NoSQL 来存储一些简单、不需要复杂操作的数据,如某个用户的粉丝数,某个博客的点赞数、字数等。

 

四种聚合模型

 

总结

传统的关系型数据库因为内部功能多,数据间存在关联,导致在数据量过大时操作起来效率比较低。非关系型数据库则与之相反从而得到了很好的性能,在日益要求性能的今天起到了很好的作用,但是因为其不能实现复杂功能,所以对于一些需要复杂操作(读写锁、事务、多表联查等)还是使用关系型数据库,而对于一些简单数据,不需要太复杂操作的可以使用非关系型数据库。

 

Redis 

redis 是一个单线程(底层使用IO多路复用模型)分布式数据库,也是一个典型的 NoSQL,它的执行效率非常高,其原因主要有以下几点:

1、是非关系型数据库,数据结构简单,且没有复杂的关联关系。

2、单线程操作,避免了多线程之间切换和竞争,并通过IO多路复用模型来避免传统 BIO 的低效执行。

3、数据存储在内存,读取时直接从内存中读取。

基础知识

1、在安装后相应的执行命令和配置文件默认在 /usr/local/bin/ 目录下

2、redis 默认有 16个数据库,0-15,默认是0号数据库,可以通过 " select 数据库号"  来切换数据库。数据库个数可以在 redis.conf 中配置。

3、redis 是统一密码管理,默认情况下没有开启密码,可以在配置文件 redis.conf 中配置开启

4、默认端口是 6379。

5、启动服务器:redis-server  配置文件全路径。配置文件可以是自定义的配置文件。启动客户端:redis -cli -p  6379。

 

五大基本数据类型及常用方法

String

最基本的数据类型,虽然为 String 类型,但是其 value 可以为 string 也可以为 int 类型。其可以用于实现计数器,也可以用于进行 json 格式的对象存储。

常用方法:

set / get / del / append / strlen :  设值 / 获值 / 删值 / 末尾添加值 / 获取长度

Incr / decr / incrby key n / decrby key n:  自增 / 自减 / 增加 n / 减去 n

getrange  n1  n2 / setrange  n1  n2  val:  截取下标n1,n2之间的值(从0开始,两边都是闭区间) / 设置下标n1,n2区间的值

setex  key  time  val / setnx  key  val:  设值并指定过期时间(单位为秒) / 在 val 不存在或者已过期时设值

mset / mget / msetnx:  批量(进行设置  /  获值 / 非空设值)

 

List

底层是链表结构,方法名开头的 l 表示 left,r 表示 right。可以用于实现消息队列、文章列表。

常用方法:

lpush / rpush / lrange n1 n2:  左添 / 右添 / 从左开始截取下标n1,n2之间的内容(0开始,两边都是闭区间)

lpop / rpop:  类似于消息队列和栈的出栈操作,分别是 (左出栈 / 右出栈)

lindex:  从左边计算获取指定下标的值

lrem key n  value:  对 key 对应的 list 数据从左边开始删除 n 个 value

ltrim  key  n1  n2:  获取 key 对应的 list 值,截取 n1 到 n2 之间的值再赋值覆盖当前的 list 值。

rpoplpush  list1  list2:  将list1中的右边尾部数据移到 list2 的左边头部 

lset  key  index  value:  左边开始修改指定索引上的值

linsert  key  before/after  val1  val2:  从左开始,获取第一个 val1,在其 (左 / 右) 插入 val2   

 

Set

和 java 中的 Set 集合一样,唯一无需的结构。可以用来存储好友,然后计算共同好友;抽奖,随机pop出栈元素。

常用方法:

sadd / smembers / sismember key val:  添加(可以批量添加) / 显示所有值 / 查看是否存在val

scard key:  获取集合中元素的个数

srem key val:  删除某个元素

scrandmember  key  n:  随机出 n 个元素(不会从集合中移除改元素)

spop key:  随机出栈一个元素(会从集合中移除该元素)

smove key1  key2  key1中的某个值val:  将 key1 对应集合中的某个值val 移入 key2 对应的集合中

sdiff  key1  key2...:  获取存在与于key1 对应集合中但不存在后面所有key对应集合中的元素

sinter key1  key2...:  获取存在于 key1 对应的集合中且存在于后面所有key对应集合中的元素

sunion  key1  key2...:  获取存在于 key1 对应的集合中或者存在于后面所有key对应集合中的元素

 

Hash

类似于 java 中的 Map 结构,可以用于存储对象。

常用方法:

hset / hget / hmset / hmget / hgettall  key/ hdel  key  key(hash):  设值 / 获值 / 批量设值 / 批量获取 / 获取 key 对应所有的键值对数据 / 删除 key 对应 hash 结构中的 key(hash) 对应的值。

hlen  key :  获取元素个数

hexists  key  key(hash):  查看 key 对应 hash 结构的 key(hash) 对应的值是否存在

hkeys key / hvals key:  获取 key 对应 hash 结构所有(key 值 / val 值)

hincrby key  key(hash)  val / hincrbyfloat  key  key(hash)  val:  对 key 对应 hash 结构中的 key(hash) 对应的值添加(整数 / 小数) val

hsetnx  key  key(hash)  val:  不存在时赋值

 

ZSet

每个数据关联一个分数,排序时会按分数升序排列,相当与一个有序 Set。可以用于实现各种排行榜。

常用方法:

zadd  key  score  val / zrange  key  n1  n2  withscores:  添加 / 获取所有值

zrangebysorce key score1 score2:  查找 score1 与 score2 之间的数据

zrem key val:  删除某个值

zcard key / zcount key score1  score2 / zrank key value / zscore key value:  获取数据数 / 统计在 score1 与 score2 之间元素的个数 / 获取指定数据所在下标 / 获取指定数据的分数

zrevrant key value:  逆序获取指定值的下标

zrevrangebyscore key score1 score2:  逆序获取 score1 与 score2 之间的数据

 

Key 及其他操作方法

keys  *:  获取所有的key

exists  key:  查看是否存在key,返回1是存在,0不存在

expire key  时间:  为 key 设置过期时间,单位是秒

ttl  key:  查看 key 还有多久过期,-1表示永不过期,-2表示已过期。

type  key:  查看 key 是什么类型

Dbsize:  查看当前数据的 key 数量

Flushdb:  清除当前库中的所有数据

Flushall:  清除所有库中的所有数据

 

配置文件 redis.conf(Linux)

下面列出的各个配置可能存在多出,因为参考了多个版本的配置文件,同时可能存在遗漏,请见谅。修改配置文件的原则是不要动默认的配置文件,应该将默认配置文件复制一份到指定目录,然后去处理,防止修改错误无法恢复。

Units配置大小单位,可以用于自定义一些度量单位,底层单位只支持 bytes,大小写不敏感。

includes:可以来引入其他配置文件

general:

  Daemonize:  是否以守护进程的方式运行,默认为no,也就是服务器窗口关闭后就会关闭服务器,如果需要后台运行可以设置为 yes。

  protected-mode:  保护模式是否开启,默认是 yes。关闭其他任何ip 地址都可以来访问连接,关闭后必须通过 bind 来配置相应的 ip 后,其才能连接。

  Pidfile:  如果 redis 以守护进程的方式运行时,系统就会将这个守护进程的 id 记录下来,记录的位置就是通过 Pidfile 来配置,默认是 /var/run/redis.pid

  Port:  端口端口号

  Tcp-backlog:  设置 tcp 的连接队列长度,其值 = 未完成三次握手队列 + 已完成三次握手队列。默认是 511,如果并发量比较大时可以设置为 2048。

  timeout:  设置最大空闲时间,也就是多久没有操作服务器就会自动断开。默认是0,也就是永不断开。

  Bind:  设置允许访问的 ip 地址,在 protected-mode 为 yes 时使用。

  tcp-keepalive:  服务器会检测客户端是否还在使用,如果一段时间内客户端没有操作,那么 redis 服务器就会释放这条连接,tcp-keepalive 就是设置客户端的最大空闲时间的,默认是0,也就是永不断开,而官方推荐是 60,也就是超过60秒没操作服务器就会回收这一条的连接。

  loglevel:  日志级别。从高到低分别为 warning、notice、verbose、debug。默认是 notice。

  logfile:  日志文件存放位置。

  Syslog-enable:  是否把输出日志保存到日志文件中。默认关闭

  Syslog-ident:  设置日志中的日志标识。

  Syslog-facility:  指定输出 syslog 的设备,可以为 user 或 local0-local7。

  Databases:  设置数据库个数。默认16

  always-show-logo:  是否总是显示 logo,默认 yes。

REPLICATION(主从复制相关):

  一般配置主从复制时,需要自己手动通过 salveof 命令来配置从机,而如果各机器事先就决定好角色,可以直接在配置文件中配置来避免手动配置。

  masterip:  主机的 ip 地址

  masterport:  主机的端口号

  masterpassword:  主机的密码

SNAPAHOTTING快照(RDB相关):

  save :  设置RDB自动备份的时间间隔,save 配置的格式是 "save  时间间隔  次数",默认配置是

    save  900  1

    save  300  10

    save  60  10000

从第一行开始作用分别是在 900s 内执行了一次写操作就会触发一次备份;300s 内执行了 10次写操作就会触发一次备份;60s 内执行了10000次写操作就会触发一次备份。

一般来说使用默认配置就可以,如果想要禁用自动备份可以将其删除或者改成 save ""。

  Stop-writes-on-bgsave-error:  通过bgsave备份出错时,主线程是否继续工作。

  rdbcompression:  是否压缩 rdb文件,需要消耗一些 cpu 资源。压缩会使文件占用空间减小。

  rdbchecknum:  存储快照后是否进行数据校验。如果想要提高执行效率可以关闭。

  dbfilename:  备份的文件名。默认是 dump.rdb。

  dir:  RDB、AOF 备份文件的存储地址。在服务器启动时恢复数据也会在 dir 配置的目录中读取对应的备份文件。默认在执行目录下。可以通过 "config  get  dir" 获取 dir 。

APPEND ONLY MODE追加(AOF相关)

  appendonly:  是否启用AOF。

  appendfilename:  备份文件的文件名,默认为 appendonly.aof。

  appendfsync:  存储策略。共有以下三种。

    always:同步持久化,每次写操作后都会立刻记录到磁盘,性能较差但是数据完整性最好。

    everysec:异步操作,每一秒执行一次记录,可能会有少量数据丢失。

    no:不进行记录。

  No-appendfsync-on-rewrite:  rewrite 时是否执行存储策略(进行存储)。一般使用默认 no 即可,保证数据安全性。

  Auto-aof-rewrite-min-Size:   设置 rewrite 触发的最小基准值。

  Auto-aof-rewrite-percentage:  设置 rewrite 触发的超出百分比基准值。

Security(安全权限相关)

默认情况下,安全权限是关闭的,在客户端连接时不需要输入密码,执行操作也不需要密码,但是如果开启了安全检测,那么所有客户端在执行命令时都需要先执行 "auth 密码" 来验证身份。

可以在配置文件中配置 "requirepass  密码" 来配置密码,也可以在命令行执行 "config  set  requirepass  密码" 来配置,查看密码可以使用 "config  get  requirepass"。如果想要关闭可以直接在命令行执行 "config  set  requirepass "" " 。

limit

  maxclients:  最大连接的客户端数,默认无限制,下同。

  maxmemory:  redis 服务器占用的最大内存。默认无限制,推荐是最大内存的四分之三。单位 byte 。 1024 * 1024 * 100 byte = 100MB。可以通过 info memory 来查看 redis 内存使用情况

  maxmemory-policy:  redis 内存淘汰策略。类似与线程池的拒绝策略,就是当存储新数据时内存空间不足执行的操作。策略主要有以下六种。

    1)volatile-lru:使用 LRU 算法移除 key,只对设置了过期时间的键

    2)allkeys-lru(常用):使用 LRU 算法移除 key。

    3)volatile-random:在过期集合中移除随机的 key,只对设置了过期时间的键

    4)allkeys-random:移除随机的 key。

    5))volatile-ttl:移除那些 TTL 最小的 key,即那些最先要过期的 key

    6)noeviction(默认):不进行移除。针对写操作,只是返回错误信息。

    7)allkeys-lfu:使用 LFU 算法移除 key

    8)volatile-lfu:使用 LFU 算法移除 key,只对设置了过期时间的键

    LRU 算法就是被使用的数据会被移到头部,未使用的就会慢慢向尾部靠近,在移除时从尾部移除。

    LFU 算法是按频率排列,频率低的移到尾部,频率高的移到头部。

  maxmemory-samples:  设置样本数。在清除时,因为LRU算法和最小TTL算法都并非是精确的算法,而是估算值,所以我们可以设置一个具体的大小,redis会抽出这个数量的数据并根据算法进行清除。

 

redis 删除策略

定时删除:在 key 到达过期时间后,就会立刻删除。优点是可以最大程度利用内存,缺点是每次删除都需要调用CPU,在过期数据多时CPU调用次数过于频繁,影响CPU执行业务代码效率。

惰性删除:在 key 过期后,不会删除,完全依靠配置文件 maxmemory-plicy 配置的内存淘汰策略来在内存不足时删除。优点是对 CPU 友好,缺点是对内存不友好,可能会浪费大量的内存。

定期删除:是前两种的折中方案,可以通过添加 "  hz  10 " (默认配置也是10)来选择一秒钟执行定期删除的次数。定期删除会进行随机抽样,删除抽样中已过期的 key ,同时会消耗规定峰值以下的 CPU,不影响业务代码的效率。但是这样还是会遗漏一部分已过期的 key (一直未被随机抽样到),那么就需要依靠前面的内存淘汰策略了。

 

RDB

默认的持久化方式。其本质就是保存当前时刻的数据快照默认生成文件名是 dump.rdb。

默认会在一段时间内自动保存数据,规则就是上面配置文件 RDB 部分 save 的配置。需要注意的是,RDB 的持久化方式分为 save 与 bgsave。

save 是中断当前进程,然后进行持久化操作,等到持久化完成后再继续执行其他操作;

bgsave 是 fork 一个子进程,fork 出来的子进程可以访问redis主内存的数据进行备份,不会阻塞当前进程执行。如果中途有写操作,会把涉及到的数据拷贝一份,然后直接更新数据(备份的子进程备份读取的是写操作之前的拷贝数据)。(cow,copy on write)

RDB 持久化触发时机

1、来自配置文件中配置的自动备份,也就是上面说得 save。其实现方式是 bgsave。

2、执行命令 save 或 bgsave。执行 save 就是中断状态来实现的;而 bgsave 则是 fork 子进程来实现,不会中断当前进程执行。

3、执行flushdb、flushall、shutdown 命令后在命令生效前也会先备份一次。其实现方式是 save。

 

RDB 文件恢复

默认情况下只需要将备份文件放在启动目录下然后在启动目录下启动服务器即可。

启动目录指的是启动 redis-server 命令的目录,在备份时会自动备份到该目录下,比如在 /temp/ 启动,那么默认会读取该目录下的 dump.rdb 文件,备份也会在该目录,如果下次启动在 /myredis/ 下,那么也会读取 /myredis/下的 dump.rdb 文件,备份数据也是会在该目录下备份。这个目录也可以自定义,配置参数是 redis.conf 中 dir 参数。

 

如何关闭 RDB 的自动备份

可以在配置文件中将 save 改成 save "",也可以直接执行 " redis-cli config set save ""  "

 

优势

1、执行效率高,适用于大规模数据的备份恢复。自动备份不会影响主线程工作。

2、备份的文件占用空间小。其备份的是数据快照,相对于 AOF 来说文件大小要小一些。

 

劣势

1、可能会造成部分数据丢失。因为是自动备份,所以如果修改的数据量不足以触发自动备份,同时发生断电等异常导致 redis 不能正常关闭,所以也没有触发关闭的备份,那么在上一次备份到异常宕机过程中发生的写操作就会丢失。

 

AOF

AOF 是在 RDB 的补充备份方式,其本质是保存执行的每一条写操作(包括flushdb、flushall),所以其产生的备份文件是可以直接阅读的。默认备份文件名是 appendonly.aof,保存位置和 RDB 备份文件一样。因为其保存的是每一条写操作,所以会比较占用 CPU,同时生成的备份文件也比较占空间,所以默认是关闭的。使用时需要在配置文件中将其打开。

AOF 持久化规则

AOF 备份也是采用自动备份,但是备份的频率会比 RDB 要高,其备份方式分为三种:

1、always:同步持久化,每次写操作后都会立刻记录到磁盘,性能较差但是数据完整性最好。

2、everysec(默认):异步操作,每一秒执行一次记录,可能会有少量数据丢失,但是性能更好。

3、no:不进行记录。

 

除此之外,redis 为了防止随着写操作越来越多,AOF 的备份文件越来越大,设置了 rewrite 机制。

Rewrite 机制类似于 RDB 的 bgsave,同样在后台开启一个子进程,可以访问当前进程的数据,然后将这些数据生成对应的写操作,然后将这些写操作依次一个临时文件,等到全部写入完毕,再将这个临时文件覆盖掉默认备份文件 appendonly.aof,在覆盖过程中 AOF 自动备份会被阻塞。过程中新的写操作会直接更新数据,并把写操作记录在aof写缓存中,等待Rewrite完成后再将操作追加写入文件。因为 Rewrite 需要消耗额外的 CPU,同时在写入原文件时还会造成阻塞,所以应该避免执行 Rewrite。

 Rewrite触发机制:

1、通过配置文件中配置的规则默认触发。

  1)Auto-aof-rewrite-min-Size:触发重写最小的基准值。默认是 64M。

  2)Auto-aof-rewrite-percentage:触发重写的超出百分比。默认是 100%。

  如果配置按照上面默认配置,那么触发 AOF 自动配置需要当前 AOF 文件超过 64M,同时文件大小达到了上一次 Rewrite 后文件大小的两倍。如果没有 Rewrite 过那么会在达到 64M 后触发第一次。

2、手动通过 " bgrewriteaof " 来触发重写。 

 

AOF 文件恢复

和 RDB 备份文件恢复一样,在默认情况下将备份文件放在启动目录,然后启动服务器即可。

文件修复:因为 AOF 文件是可修改的,如果内部有一些异常操作,那么在下次启动时就会报错,此时可以通过 redis 提供的修复工具来修复备份文件。执行命令 " redis-check-aof  --fix  文件名" 来修复。

 

优势

总体上来说,要比 RDB 备份方式数据完整性要更好,在数据完整性要求高的场景下可以使用 AOF。

而因为 AOF 有两种不同的备份规则,所以在数据完整性最优先、性能可以不考虑的场景可以使用 always 方式;在数据完整性要求比较高,但是也允许少量的数据丢失,但是要求性能也不会差,那么可以选择 everysec。

 

劣势

1、因为保存的是每一步操作,所以执行效率低。

2、虽然引入 rewrite 来避免备份文件过大,但是 rewrite 造成的 CPU 资源消耗加上原本备份的 CPU 资源消耗会比只使用 RDB 要多得多,所以如果不是对数据完整性有特别高的要求建议只使用 RDB。

 

两种持久化总结

1、默认情况下,redis 只使用 RDB 持久化。因为 AOF 会消耗过多的 CPU,同时执行效率低。

2、如果开启了 AOF 持久化,那么在恢复数据时优先使用 AOF 配置文件来恢复,因为 AOF 保存的数据更完整。

3、如果 redis 只用于做缓存,那么可以直接禁用 RDB 和 AOF 的自动持久化。

4、RDB 持久化一般用作数据的定期备份,如果对数据完整性要求没有那么高,那么可以只使用 RDB,同时在配置文件中只保存 "save  900  1" 这条规则,以此来减少不必要的 CPU 消耗。如果需要使用 AOF ,应该调大 Auto-aof-rewrite-min-Size 来避免频繁的 Rewrite。

 

Redis 事务

mysql 中的事务拥有 ACID 特性,即原子性、一致性、隔离性、持久性。那么 Redis 的事务呢? Redis 的事务拥有隔离性,但是不包证原子性。并且其没有隔离级别的概念,也就是说它不像 mysql 中执行了操作,但是因为隔离级别的影响而导致 " 操作未执行 " 的假象。

基本操作

开启事务:multi;

提交事务:exec;

放弃事务:discard;

事务执行的流程是 " 开启事务--->操作入队---->提交事务执行所有操作",下面的截图就是典型的事务执行过程

如果在提交之前想中断此次事务,可以通过 "discard" 来取消当前事务。 

 

特点

1、隔离性:事务的执行不会被其他客户端的操作打断。

2、不保证原子性:如果事务中的某一条操作执行失败,那么其不会影响该事务中的其他操作。

3、在事务提交时,如果某个操作有异常(操作本身的格式有问题,在入队时就报错,也就是编译异常),那么这个事务在提交后不会生效,内部的所有操作都不会生效。如下图。

 

4、在事务提交时,如果某个操作在入队时没有异常,在提交时发生异常,那么这个操作不会影响其他操作的执行。

 

除此之外,Redis 还可以通过 "watch  key" 来对指定的数据设置乐观锁,此后如果其他会话对该数据操作后,当前会话执行的事务就会被中断。

 

Redis 的发布订阅

虽然这模块功能一般是由消息队列来实现,但是如果是简单的 "发布-订阅" 操作通过 redis 也是可以实现的。

相关操作

1、订阅一个或多个:SUBSCRIBE  订阅名1  订阅名2 ...

2、消息发布: PUBLISH  订阅名   消息内容

  实现如下:

3、订阅通配符: PUBLISH  通配符*

  实现如下:

 

主从复制

在操作量较大时,一台 redis 服务器往往不能满足需求,所以需要搭建 redis 服务器集群,而实现的方式一般是搭建 "主从复制" 的服务器模式,其本质就是主机来处理写操作,从机处理读操作。

配置

首先,要知道的是每台服务器启动后,默认就是 master,也就是主。所以我们只需要去配置 salve(从机),配置方式就是在需要成为从机的客户端上执行 " slaveof  主库ip  主库端口 ",在执行后,可以通过 " info  replication"来查看当前服务器的状态。

在完成配置后,在主机上进行写操作后从机上就可以进行对应的读操作。

 

 

同步原理 

在从机与主机完成交互关系后,主机就会收到从机发送的一条 sync 命令,这条命令会使 master 启动后台的存盘进程,等到进程存盘完成后,就会将整个数据文件发送给 slave,slave 接收到数据然后加载到内存,这种所有数据全部复制给 slave 叫做 "全量复制"。而后续 master 进行写操作,相关的写操作会依次复制传给 slave,这种附加的复制叫做 "增量复制"

 

注意细节

配置上:

  在进行主从复制时,需要将配置文件拷贝服务器台数的数量,然后需要修改相应配置文件,打开守护模式,修改其端口号、pid 文件名字、Log 文件名、RDB 备份文件名,如果使用了 AOF 还需要修改 AOF 备份文件名。然后再启动服务器并指定相应的配置文件。如果一开始就确定主从机关系也可以通过配置 masterip、masterport 来避免手动配置。

执行时:

  1、默认情况下 master 用于处理写操作, slave 用于处理读操作。

  2、主机宕机后,从机会原地待命(还是不能执行写操作可以执行读操作), 等待 master 重新连接后又恢复正常。

  3、当一台 slave 成为另外一台 slave 的 " master "后,其身份还是 slave,不能执行写操作。

  4、从机断开后需要重新通过 " slaveof " 来连接成为从机,但如果配置进配置文件则不需要。

  5、使用 " salveof no  one " 可以让当前从机退出关联,重新成为 "master" 状态。

 

不足

在 master 向 slave 复制数据时会有一定的延迟,这种在数据量小的情况下不会有明显感觉,但是在操作数多的情况下,或者在 slave 服务器较多时,在 slave 读取的数据就不能保证是最新的值了。

 

哨兵模式

对于上面的配置方式是有明显缺陷的,如果 master 出现故障宕机,那么此系统就会无法正常工作,如果是 "一主二从" ,那么我们完全可以在 master 宕机后从两台 slave 中选择一台成为新的 master,以此来保证系统的正常执行。" 哨兵" 就是干这件事的,通过它可以在 master 宕机后自动从剩下的 slave 中选择一台成为新的 master。

配置

1、创建文件 sentinel.conf 。在文件中编写哨兵 "监视" 的服务器信息:" sentinel  monitor  数据库名字(自定义)   数据库所在ip   端口号  1 " 。 结尾的 1 表示投票数,也就是在 master 宕机后,哨兵会对剩下的 slave 进行投票,得票数多的成为下一个 master。而总票数就是 1。为了不让每台服务器得到的票数相同就设为 1。一个哨兵可以监视多个 master,也就是一个文件中可以配置多个。

2、另开一个窗口执行 " Redis-sentinel  sentinel.conf所在目录/sentinel.conf " 。

 

注意细节

1、在主机宕机从机成为新的 master后,前主机重新连接,那么其会被哨兵分配成 slave 执行读操作。

2、当前说的是配置一个哨兵,如果这个哨兵宕机,那么就存在着隐患,所以一般在项目中会搭建哨兵集群,来避免哨兵的宕机,同时哨兵搭建配置参数并没有这么少,这里展现的只是核心配置,如有其他要求需要另行配置。

 

客户端

redis 的Java客户端主要有 jedis、lettuce、Redisson。

1、在 SpringBoot 里封装的 Redis 依赖使用的是 lettuce,其优点是底层使用的是 netty,执行效率非常高,同时是线程安全的,支持绝大多数的 redis 功能。

2、jedis 是老牌的 Java 客户端,但是随着硬件的升级,其效率变得越来越低,同时也是线程不安全的。其优点或许只是能全面支持 Redis 的操作特性了吧。

3、 Redisson 则是在封装了 Redis 许多高级功能的基础上效率也比较高,使用起来也比较方便。但是其对字符串操作支持比较差。

 

高并发下的问题与解决

缓存穿透

查询一个不存在的值,导致缓存未生效。如查询的结果值是null,那么就去数据库进行查询,而如果数据库查询的结果就是null,那么就造成下次查询还是会走数据库,即使结果就是null。也就是故意恶意去查询不存在的值让数据库承受高并发的查询最终造成性能上的影响。

解决:将null值进行缓存,并加入短暂的过期时间。过期后再去查询数据库。

 

缓存雪崩

在增加缓存时设置了同一个过期时间,使得这些数据在某一时刻同时大面积失效,而在这时刻过来的所有请求全部需要去数据库进行查询,造成数据库压力过大。

解决:设置一个随机的时间,避免在同一时刻大面积过期。

 

缓存击穿

指的是某个热点数据的key失效后收到大量的请求,那么这些请求都会去查询数据库造成数据库压力过大。

解决:加锁,先由一个人去查,然后添加缓存,添加完成后解锁,其他人再从缓存中查询。

 

读问题(本地锁与分布式锁)

redis 缓存在高并发下的读取会有三个问题,缓存穿透(查询不存在的值都会走数据库)、缓存雪崩(同一时刻大量的缓存同时过期导致数据库压力过大)、缓存击穿(热点数据在过期失效时大量请求进来访问数据库)。前两个可以通过缓存空数据、设置不同的过期时间来解决。第三个就需要添加锁来解决。

如果加的是本地锁,且应用是单体应用,那么因为Spring默认是单例的,所以是可行的,但是如果是分布式集群项目,那么每个集群的 bean 都是不同的,所以集群之前是锁不住的

 

分布式锁

分布式锁应该满足下面的条件:

在缓存查询前通过对特定 key 中尝试插入一个期限时间(防止加锁后的线程发生异常造成死锁)的数据来模拟加锁,加锁后其他线程尝试设置数据失败进入自旋。在业务执行完成后将数据添加到缓存,然后再进行解锁。加锁并设置过期时间、校验是否是当前锁(每个线程设置的value都不同,在删除前检查是否是之前加锁时设置的值,防止在业务执行时当前设置的数据已经到期而其他线程新设置了锁,导致当前线程将其他线程加的锁删除了)并解锁都是原子操作。

 

Redisson锁

特点:

1、默认会设置一个30s过期时间的锁,所以即使断电没有释放也不会死锁

2、当业务达到三分之一的看门狗时间(30/3=10s),会触发看门狗机制,如果还没执行完会续期30s。(如果执行服务的线程崩溃或被操作系统杀死那么不会触发续期,但是如果是代码问题导致的死锁那么还是会自动续期)

3、如果在加锁时自定义了过期时间,那么在到达过期时间后锁也会过期。(在大业务场景(执行超过30s)效率高一些)

 

配置:

@Configuration
public class RedissonConfig {

    @Bean
    public RedissonClient redissonClient(){
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.56.10:6379");
        RedissonClient redisson = Redisson.create(config);
        return redisson;
    }
}

 

使用

读写锁

信号量

CountDownLatch

 

写问题

如果一个线程修改了缓存中的某个原数据(也就是修改了数据库),那么缓存中的数据就不是最新数据了,造成写问题。

解决:

1、对于一致性要求不高的,可以等待缓存数据过期后,自动更新最新数据。

2、对于一致性要求高的。

1)使用 Redisson 的读写锁,限制读写同时执行。缺点是在写多的场景下执行效率非常低。

2)双写模式。在写操作完成也更新缓存数据。这个效率不低,但是可能还是会造成数据不一致的问题。在缓存数据过期后就会恢复正常。

进一步优化就是在写操作和更新缓存整个操作加锁,保证只能有一个线程在执行写操作。

3)失效模式。在更新后删除缓存数据。这个和双写模式一样,也可能会存在数据不一致的问题。并且在缓存数据被删除或过期后会达成最终一致性。

4)使用 Canal 中间件(阿里开源的中间件),通过监控mysql的binlog日志,在数据修改后会自动更新对应的缓存数据。

总结:

在要求数据一致性的场景下,也就是解决写问题,通常的方法就是加锁。而加锁所带来的就是性能上的下降。所以两者之间的取舍就需要结合业务场景来看。

 

SpringCache

SpringCache 是 Spring 对各种缓存的封装,在引入 Spring-boot-starter-cache 后就可以使用,通过注解直接实现缓存的获取、删除、添加。

特点:

1、划分区域。SpringCache 可以将缓存区划分为多个区域,每个区域用于存放某一类数据。删除时也可以直接删除某区域的数据。如@Cacheable({"category"}),category 就是指定的区域。{}可以指定存入多个分区。

2、自定义缓存类型,存活时间、是否存储null、缓存名前缀等属性:

3、可以自定义Redis的配置类,改变Redis key、value的序列化器,解决存储数据的乱码问题。但是需要对配置文件中的配置进行另外赋值(也可以只对CacheManager进行修改,修改其关联的组件。CacheManager 是RedisCacheConfiguration 内的一个组件

@EnableConfigurationProperties(CacheProperties.class)       // 将读取配置缓存的类CacheProperties加入Spring容器(因为默认没用加到容器中,所以这里需要加入容器)
@Configuration
@EnableCaching      // 开启缓存配置
public class MyCacheConfig {

    @Bean
    public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties){    // CacheProperties 会自动从容器中获取到
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();

        // 设置 key 序列化(默认String)
        config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                // 设置 value 序列化(默认JDK)
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericFastJsonRedisSerializer()));


        // 默认在自定义 RedisConfiguration 后会不使用配置文件中配置,所以我们需要额外设置配置文件中的配置,也就是读取配置文件中的配置赋值到配置对象中
        CacheProperties.Redis redisProperties = cacheProperties.getRedis();
        //将配置文件中所有的配置都生效
        if (redisProperties.getTimeToLive() != null) {
            config = config.entryTtl(redisProperties.getTimeToLive());      // 存活时间
        }
        if (redisProperties.getKeyPrefix() != null) {
            config = config.prefixKeysWith(redisProperties.getKeyPrefix());     // 缓存 key 前缀
        }
        if (!redisProperties.isCacheNullValues()) {
            config = config.disableCachingNullValues();             // 是否缓存空值
        }
        if (!redisProperties.isUseKeyPrefix()) {
            config = config.disableKeyPrefix();             // 是否启用 key 前缀
        }

        return config;
    }
}

@EnableConfigurationProperties(CacheProperties.class),因为默认的CacheProperties类未加入到容器,所以这里必须指定加入容器才能在方法里直接获取。

4、缓存key在未指定key时是”(分区名或前缀名)::SimpleKey[]”,如果指定了key,那么就是”(分区名或前缀名)_key”。(新版的key好像不显示(分区名::),但是如果指定前缀名格式还是一样。)

5、缓存未指定时间则默认是永不过期

6、注解使用

@Cacheable(value={“category”},key=”#root.method.name”)

先执行方法前先查询是否包含缓存,分区是 category,key 是方法名,也就是寻找

“category_方法名” 为key的 缓存

 

@CacheEvict(value=”category”,key=” ’getCataJson ’ ”)   (相当于上面一致性实现的失效模式)

删除category_getCataJson 为 key 的缓存

 

@Caching():执行多个缓存操作

 

@CacheEvict(value=”category”, allEntries = true)

删除 category分区的所有数据,也就是删除 category_ 开头的 key 所有缓存

 

@CachePut 同理,是将返回值来更新到指定的缓存上,如果返回值为null,执行的就是删除,不为空执行的就是修改更新。    (相当于上面一致性实现的双写模式)

 

SpringCache 与 Spring 封装的Redis依赖的区别

在引入Spring 对 redis 封装的依赖spring-boot-starter-cache就可以使用 redis了。传统的Redis只需要配置其端口,路径就可以了,然后使用封装好的 StringRedisTemplate来操作数据库。

而如果需要使用SpringCache(注解),那么还需要配置类型(至少要有一个类型)以及其他配置,然后加入容器才能使用。SpringCache 相比较于 普通的 Redis 开发更方便简洁,但是只适用于常规的使用。

 

SpringBoot 中的使用

在 springboot2.0 之前,底层默认使用的是 jedis,而 2.0 以后变成了 lettuce,这是因为 jedis 采用的是直连,当多个线程操作时,是不安全的。此时可以通过 jedis 的连接池来避免线程不安全,但是在执行时还是会比较慢。而 lettuce 底层使用的是 netty,实例可以在多个线程中共享,不会发生线程不安全的情况。

乱码问题

在使用 redisTemplate 将数据存入 redis 后往往会发现在 redis 客户端中读取会乱码,这是为什么?

redis 内部维护的 redisTemplate 底层使用的是 JDK 序列化器,在 redis 中以二进制形式保存,所以我们在客户端直接读取的是二进制数据,相关源码可以看下面代码

// RedisAutoConfiguration.class
    
    @Bean
    @ConditionalOnMissingBean(name = "redisTemplate")
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
            throws UnknownHostException {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }   



// RedisTemplate.class

 public void afterPropertiesSet() {
        super.afterPropertiesSet();
        boolean defaultUsed = false;
        if (this.defaultSerializer == null) {
            this.defaultSerializer = new JdkSerializationRedisSerializer(this.classLoader != null ? this.classLoader : this.getClass().getClassLoader());
        }

        if (this.enableDefaultSerializer) {
            if (this.keySerializer == null) {
                this.keySerializer = this.defaultSerializer;
                defaultUsed = true;
            }

            if (this.valueSerializer == null) {
                this.valueSerializer = this.defaultSerializer;
                defaultUsed = true;
            }

            if (this.hashKeySerializer == null) {
                this.hashKeySerializer = this.defaultSerializer;
                defaultUsed = true;
            }

            if (this.hashValueSerializer == null) {
                this.hashValueSerializer = this.defaultSerializer;
                defaultUsed = true;
            }
        }
        ...
    }

关于 redis 的序列化器有以下几种:

我们着重看一下常用的几种:

1、JdkSerializationRedisSerializer。RedisTemplate默认的序列化器,存储的对象必须实现Serializable接口,不需要指定对象类型信息,在redis中以二进制格式来保存,不可读,且转化后的二进制数据往往比json数据要大。

2、StringRedisSerializer。已String类型进行保存,不需要指定,不需要实现Serializable接口

3、Jackson2JsonRedisSerializer。需要指定序列化对象的类型,不需要实现Serializable接口

4、GenericJackson2JsonRedisSerializer。不需要指定序列化对象类型。不需要实现Serializable接口,与 Jackson2JsonRedisSerializer 区别是其保存的对象数据虽然也是 Json 格式的,但是会显示存储对象的类型,以及元素对象所在的类路径。具体可以百度。

redisTemplate 默认 key 与 value 使用的都是 JDK 序列化器,我们可以自定义一个 redisTemplate 组件来覆盖默认的,key 可以使用 String 序列化器,value 使用 Jackson2 序列化器。代码如下:

@Configuration
public class RedisConfig {

    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
        template.setConnectionFactory(factory);
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        // key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value序列化方式采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的value序列化方式采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();

        return template;
    }
}

同时如果要存储的数据就是字符串类型的,那么也可以直接使用 redisTemplate,其 key 与 value 都是使用 String 序列化器。

 

常用方法

redisTemplate.opsForValue();//操作字符串
redisTemplate.opsForHash();//操作hash
redisTemplate.opsForList();//操作list
redisTemplate.opsForSet();//操作set
redisTemplate.opsForZSet();//操作有序set
 
redistempalate.boundValueOps;
redistempalate.boundSetOps;
redistempalate.boundListOps;
redistempalate.boundHashOps;
redistempalate.boundZSetOps;
//两者区别:ops就相当于创建一个operator,前者是通过一个operator来执行各个数据类型的操作,后者是选中数据类型再为这个类型来创建一个operator,
//也就是前者是一个operator执行多种数据,后者是一个operator操作一种数据



// 通过 connection 对象来执行数据库相关的操作
RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
connection.flushDb();  
connection.flushAll();

 

posted on 2020-12-19 12:42  萌新J  阅读(1437)  评论(2编辑  收藏  举报