redis重新学习

redis基本知识

这篇文件中的redis技术,仅仅涉及到工作常用的,并不是Redis的主要知识,知识大量来源于bilibili的课程视频

redis的命令工具

redis安装好后,自带官方提供的许多sh文件,这些文件就是redis的工具文件

  1. 性能测试文件 redis-benchmark
  2. 修复aof持久化文件 redis-check-aof
  3. 修复rbd持久化文件 redis-check-rdb
  4. Redis客户端命令行 redis-cli
  5. redis哨兵集群启动 redis-sentinel
  6. redis服务启动 redis-server

总结:

  1. 都是redis开头的sh文件
  2. 三个启动,两个修复,一个测试

redis服务的启动配置参数

也就是redis-server这个sh文件的参数

redis-server启动以后,会获取到这个命令后面带的参数

常见参数:

  1. 接文件全路径

这个参数最为常见,redis-server执行之后,会自动根据给定的文件全路径找到一个文件,作为redis-server启动的配置

配置文件中的各种属性

常见的配置:

  1. 后台启动(守护线程)

在daemonize 中设置值为yes

  1. 设置密码

在requirepass属性设置的值就是密码

redis客户端命令

对服务器的操作

一些需要redis客户端使用的通用操作

  1. 退出客户端

exit 这个命令几乎所有的正规命令行窗口都支持

  1. 关闭服务器

shutdown 使用这个命令会关闭此客户端连接到的服务器

  1. 测试连接到服务器

ping 命令.服务器会回复一个pong表示连接正常

对于服务器中的元信息操作

  1. 切换数据库

select [db_id]

在redis中,没有定义数据库的概念,redis中内置就有16个库,通过编号表示,0-15就是其编号,通过select指定当前所在数据库,数据库的概念和其他数据库类似,都起到命名空间的作用

  1. 验证密码

auth [your_password]

redis中的密码直接通过配置文件定义在配置文件的password属性中,在客户端连接到服务器之后,需要通过密码验证才能执行相应的操作

所有数据库中的密码都是一个,实际上也就是连接到redis服务器只有一个密码

  1. 操作数据总体

dbsize 查看当前数据库中键值对的个数

flushdb清空数据库中的所有数据

flushall清空所有数据库中的所有数据

keys [parrtern]查看数据库中的所有键

  1. 获取某个数据的数据

exists [key] [key1] … 返回值就是找到的键的个数

type [key] 查看对应键的值的类型(键值对中的键只能是二进制字符串类型(也就是用二进制来表示的字符串))

object encoding [key] 可以查看到值底层的数据结构,相比于type更底层

  1. 删除一个键

为什么删除操作属于元信息操作呢?因为删除不需要知道这个键值对的类型,而设置一种类型的键值对需要不同的格式和数据来指定,而删除只需要一个删除信息即可确定,所以这个是不涉及到具体数据的操作,即元信息操作

del [key] 这个是直接删除

unlink [key] 这个是先使得外界无法查询到(从keyspace元数据中删除),等空闲再异步删除

  1. 过期时间

这个操作是redis的一大特点,为一个键设置过期时间,等时间一到就会自动将键删除

expire [key] [seconds] 为给定的key设置多少秒的过期时间,如果键存在返回1,不存在返回0表示没有设置成功

pexpire这个只是单位改为了millseconds毫秒

ttl [key]检查过期时间 如果该键设置了过期时间,那么会返回剩余的时间,如果键没设置过期时间返回-1表示没有过期时间,如果键都不存在返回-2表示两个条件都不满足,键没过期时间,没有键

对于具体数据类型的操作

redis中支持五种常用的数据类型,而不常用的数据类型实际上也是通过五种数据类型的组装得来的

这里列举一下,并做简单的介绍:

  1. String 字符串类型
  2. Hash hash表类型
  3. List 列表类型
  4. Set 集合类型
  5. ZSet 有序集合类型

数据类型需要重点介绍,相关内容移步到二级标题:五种常用数据类型

操作String类型键值对

  1. 设置新的键值对

set [key] [value] [一些可选参数,通过空格分隔]

有一些可选参数:

  • nx | xx 添加上就可以表示当键不存在?或者当键存在时?
  • ex 就是expire 相当于直接将前一个set设置的key作为参数设置过期时间
  • px 用毫秒作为单位设置过期时间,与ex只能设置一个
  1. 获取值

get [key]

如果键存在,服务器就会返回一个对应的值

如果不存在会返回一个空值(nil)

  1. 追加append操作

实际上就是先get再set,是set和get的联合操作

append [key] [value2]

需要注意,这个操作只支持字符串类型,如果键值对类型不匹配会返回(error)值

  1. 获取一些具有类型特定的元信息

strlen [key] 获取字符串值的长度


  1. 一些语法糖命令

senx [key] [value] 实际上就是set [key] [value] nx的简化写法

因为nx很常用所以有了这个命令,但是没有setxx命令,因为setxx并不常用(setxx的意思就是不要引入新的键值对)

setex [key] [seconds] [value]实际上就是同时设置过期时间,但是这个注意在键之后就过期时间而不是在vaue之后

mset [key1] [value1] [key2] … 根据字符串键值对的特点,mset命令得以出现,其他类型的键值对设置就不一定有mset这种批量设置的命令了.

mget [key1] [key2] … 批量获取值

msetnx [key] [value] … 在批量设置的同时,保证了nx操作,所以实际上批量设置还是不能面面俱到,不能对每个键值对设置ex,nx,xx等,当有一个不满足时会回滚,就像事务操作一样(原子性)

getset [key] [value] 就是先获取之前的值,再进行覆盖,即使没有取到值,也会设置一个值

  1. 字符串特有的操作

incr [key] 自增+1 需要string值可以转换为数值

如果这个键不存在会自动创建一个值为0的键值对然后+1

decr [key] 和incr操作相反,当时也会创建键

incrby [key] [increment] 相当于给了一个参数作为自增操作的数值(步长也可以是负数)

decrby [key] [decrement]

需要注意,在java中自增操作具有不具原子性(也就是可能有一部分成功一部分失败),而在redis中自增操作是原子操作

getrange/substr [key] [startIndex] [endIndex] 截取字符串,其中getrange是新的命令,substr是老的命令,现在已经要废弃了不推荐使用,需要注意的是在redis中的getrange是一个包含起始位置和结束位置的,这一点和其他编程语言不同

setrange [key] [offset偏移量] [value] 局部覆盖操作,可以改变字符串的长度(只能加长)

操作List类型的键值对

  1. 添加新元素

lpush [key] [value] [value12]… 实际上,通过lpush只是一种,我们只需要想象有一个横向的队列,从左边依次压入即可

如果键值对不存在会自动创建一个新的键值对

加上可选参数:

redis特点的nx操作,xx操作,以及ex操作

lpushx 如果键不存在就不会自动创建

rpush 以及rpushx [key] [value1] … 从右边向列表中添加元素实际上就是添加列表的尾部

  1. 获取值

lrange [key] [startIndex] [endIndex] 从左到右返回列表元素,注意这个和之前的String类型的getrange一样都是包含结束下表元素的

可以使用负数来表示从右到左,redis实际上会将复制自动计算,例如-1就是最后一个元素

没有rrang或者range也就是没有从右到左的读取命令,实际上redis提供了其他形式例如zrange

lpop [key] [可选弹出个数] 会获得列表头元素,并移除

rpop [key] [count] 从右边弹出

  1. 类型特有操作

rpoplpush [pop列表的key] [push列表的key] 也就是将一个列表中的元素移除加入到目标列表中

没有lpoprpush命令

实际上在操作list元素的时候有可能会导致一些并发问题,redis提供了BLpop或者BRpop或者BrpopLpush等操作,这种操作前边的B表示blocking阻塞的意思,可以使得没有获得锁的申请阻塞在队列中

lindex [key] [index] 从左到右根据索引取的数据

llen [key] 查看列表长度

linsert [key] [after|before] [fixElement] [insertElement] 给了基准元素,然后可以在基准元素的左右插入,如果key不存在是不会自动创建一个键值对的返回0,如果基准元素没找到,会返回-1,操作成功会返回执行之后的列表长度

lrem [key] [根据正负决定删除的顺序,根据大小决定删除的个数] [删除的值] 删除列表中的一些元素,会返回删除元素的个数

lset [key] [index] [value] 直接通过索引设置值,重新修改进行覆盖

redis没有直接提供查询list中某个元素的命令

ltrim [key] [start] [stop] 修剪其余元素,保留区间内的元素,当然也是包含头尾

  1. 阻塞操作

blpop或者brpop [key] [timeou_seconds] 阻塞式弹出,当列表中没有元素的时候,命令陷入阻塞,如果时间到了还无法满足执行条件,就会返回nil

操作Hash类型的键

Hash实际上是一个键值对表,这个表被一个键映射在一起这个就是hash类型

特别适合用于存储对象

  1. 基本操作

hset [key] [field] [value] [field] [value] …有点像mset(实际上在redis4.0版本之前hset只能设置一个字段值,如果要设置多个需要使用hmset,后来发现这个是多余的,直接就将hmset改成了hset,现在其实也能使用hmset实际上的作用和hset是一样的)

hget [key] [field] 获取一个字段值

hmget [key] [field1] [field2] …获取多个字段值,实际上hget不能充当hmget使用

hgetall [key] 获取所有的字段和字段值

hsetnx [key] [field] [value] 只能设置一个字段,当字段不存在才能设置

hexists [key] [field] 判断一个字段是否存在,存在返回1否则返回0

hkeys [key] 返回所有字段名称

hvals [key] 返回所有字段值

其实和hgetall一起记忆比较好

hlen [key] 获取字段的个数

hdel [key] [field] [field] 删除字段

hincrby [key] [field] [step] 自增操作也有

hincrbyfloat [key] [field] [flaot]因为因可能操作的是一个浮点数,所以有这个命令,如果使用了浮点数进行操作,那么实际上这个值的类型就改变了,不能再使用整型进行操作,及时在字面值上看是一个整型

操作Set类型的键值对

  1. 基本操作

sadd [key] [member] [member …] 实际上的顺序并不重要,重要的是这里是add因为每一个元素都是独一无二的

smembers [key] 返回集合中的所有成员

sismember [key] [member] 判断成员是否是集合的成员,不是就返回0是返回1

scard [key] 返回集合的成员个数,card的意思就是王牌

srem [key] [member] [member …] 移除集合中的元素,记得有一个lrem是list的操作

spop [key] [count]随机弹出一个成员或者count个,这个参数可选

srandmember [key] [count]随机得到一个成员,并不作删除,这个count可选

smove [source_key] [目标集合] [member] 移动一个成员到目标集合中去,只能移动一个

sinter [key1] [key2] 这个是交集操作,求两个集合的交集

sinterstore [set-name] [key1] [key2] 求交集之后存储在一个集合中

sunion [key1] [key2] 返回集合的并集

sunionstore [set-name] [key1] [key2] 将并集存储在一个集合中

sdiff [key1] [key2] 返回集合的差集,差集不是不同的元素,而是在第一个集合中做排除,第二个集合中的是不要的

sdiffstore [set-name] [key1] [key2] 将求得差集存储在集合中

操作Zset类型的键值对

  1. 基本操作

zadd [key] [ 一些可选参数] [score] [memeber] [ score memeber … ] 需要注意的是,这里可以使用可选参数,实际上用法和msetnx是一样的,只要有一个不满足那么全部都不能执行,具有原子性

zrange [key] [min] [max] [byscore | bylex] [rev] [limit offset count] [withscores] 这个命令可以设置三种模式,按照分数范围获得,按照值的字典序范围获得,还是按照下标范围获得,默认情况下就是按照下标获得

按照字典序获得需要注意:

  1. 按照顺序全部查询

zrange [key] - + bylex

如果要反序也需要加上rev参数,并且反向min max参数

  1. 按照范围查询

zrange [key] [aa [ac bylex

中括号表示包含这个值,小括号表示不包含这个值

​ 这个命令还有一些可选参数

​ rev : zrange [key] [min] [byscore | bylex] rev 表示反转,从大到小,需要注意此时我们的max min也要反向从大到小,如果是按照下标范围获得,那么就不需要反向min和max参数

​ limit offset count :做分页处理 对结果进行分页

​ withscores : 带上这个参数那么最后返回的结果都会带上自己的分数

zrangebyscore [key] [min] [max] [rev] [withscores] [limit offset count] 实际上就是zrange的一种

zrangebylex 又是一种

zrevrange [key] [min] [max] [limit offset count]这个就是直接按照下反转,很zrange一样,下标范围不需要反转min max参数

zrevrangebyscore [key] [max] [min] [limit offset count] 这个就需要反转,所以直接写成了max min

zrevrangebylex [key] [max] [min] [limit offset count] 这个也需要反转

zcard [key] 返回成员个数

zincrby [key] [incrememt] [member]增加分数

zrem [key] [member … ] 删除就像srem一样就像lrem一样

zcount [key] [min] [max] 返回查询到的元素个数,两边都是包含的

zrank [key] [member] 获取成员的排序位置

zscore [key] [member] 获取成员的分数

这个命令还常被用于判断一个member是否是zset成员

五种常用数据类型

五种常用数据类型也就是String,hash,list,set,zset

String 二进制安全

实际上在内存中就是通过二进制表示的,在存入和读出的时候将字符串转换为二进制数组,读取的时候将二进制数组转换为字符串显示,这就是所谓的二进制安全字符串

可以用String类型存放,字符串,数值,json,图像数据…

这个string类型的值最大内存是512M,也许不能存储下一些视频…

字符串类型的价值

  1. 用来做单值缓存

​ 也就是只用来存一个值

  1. 用来做对象缓存

也就是将讲一个对象通过字符串存储下来

例如 set id:1001 “{‘name’:’wzy}” 通过json字符串表示一个对象

或者mset id:1001:name wzy id:1001:age 20 通过一些系列的键值对表示一个对象,这些键值对通过键名联系在一起

  1. 用来做分布式锁

因为string类型最为简单,加上redis的特点setnx这种非常好

上锁可以通过setnx lock true 如果返回1表示拿到锁了,返回0表示没有拿到锁,这就是分布式锁(因为redis可以是分布式的所以这个设置在redis中的锁就可以是分布式锁)

可以再加上ex来防止一直不释放(可能程序挂了,我不知道)

  1. 做计数器

redis中的incr操作是原子操作,所以在多线程分布式情况下做计数器都是很好的

List 字符串列表

redis中的列表是简单的字符串列表

实际上就是单键多值,可以按照顺序插入或者删除

主要的操作就是添加元素到列表的的头部或者尾部(左边或者右边)

一个列表最多可以包含2^32-1个元素,那么可以知道,实际上列表存储的是一个值的映射表,实际上的值分布在内存个部分

底层实际上双向链表,对两端的操作性能好高,对于中间的元素操作性能不好

应用场景

  1. 作为队列或者堆栈

redis中的list是一个双向链表,所以可以用来做成队列和堆栈都是可以的

堆栈的实现:
使用lpush和lpop就可以是实现或者rpush,rpop这种至少只要保证出口和入口是一个就是堆栈

队列的实现:
一端作为出口一段作为出口

​ lpush + rpop

​ 或者rpush lpop都可以

阻塞式消息队列:
阻塞式消息队列的英文就是blocking messgae queue

​ lpush + brpop就是一个阻塞队列了

订阅号时间线:
我们显示一些东西给客户端不可能全部一下子给到客户端,可以根据客户端的操作,一点一点的给客户端

​ 就可以使用lrange [key] [start] [stop]

​ 其实就是滚动

Hash类型

实际上就是一张表,这张表中的字段值的操作和redis中对于字符串的操作是一样的,例如什么hsetnx hexists hmget hlen hgetall hkeys hdel hvals hincrby hincrbyfloat

应用场景

  1. 缓存对象

最为常用的操作就是使用hash保存一个对象

  1. 购物车操作

实际上利用hinreby操作,这样可以很好的提供数量操作

查看商品总数 就可以使用hlen

删除商品

获取所有商品

Set 字符串集合

实际上就是无需的集合,类型是stirng.

提供的功能和list相似,但是相比于list提供distinct功能,这样就可以自动去重,并且无序

实际上set是通过hash表实现的,查找的时间复杂度是O(1)

应用场景

常见的比如

  1. 抽奖

用到的就是set的随机获得

srandmember 操作

将所有可能的成员存储在一个集合中,使用spop或者srandmember操作可以随机获得

  1. 用户群体收集,利用去重

例如点赞之类的,收集用户,再次点击就移除

显示点赞次数 smemebers

获取点赞的次数:scard

  1. 利用并集,交集,差集操作

关注模型,例如共同关注的操作

ZSet有序集合

实际上和set是一样的都是String类型的集合,但是zset的每一个成员带有一个排序参数,这个参数是double类型,redis中叫做score分数,zset会根据这个参数对成员进行排序

分数可以重复

有序集合的作用实际上是为了根根据分数或者次序来获取一个范围内的元素

应用场景

  1. 排序任务

例如按照时间排序,分数存入时间戳

计算热度来作为分数

计算topN

redis运维

redis毕竟是一个分布式数据库,在集群管理,备份,以及分片等方面都需要了解

redis的持久化机制

redis实际上是内存字典,但是也会需要将数据持久化到磁盘上,再出现错误的时候就可以从磁盘上恢复数据

redis现有的持久化机制有两种:

  1. AOF 也就是append only file 只追加文件
  2. RDB 也就是redis database 文件,是内存数据的直接拷贝

AOF 机制

实际上AOF是redis的操作日志,将对redis服务器的全部**对数据有影响的操作做记录,**例如修改添加,删除等操作记录到一个文件中,这样就可以根据AOF的操作步骤一步一步的还原出之前的数据

配置文件:

redis默认没有持久化处理,需要在redis服务启动配置文件中,配置启动aof

appendonly no #修改为yes即可开启aof

appendfilename “yourfilepath”

redis的支持

在redis服务启动的时候就回去寻找该文件,根据日志文件再次执行一次恢复之前的数据

如果aof文件损坏,继续启动redis服务将无法正常启动,这个时候就可以使用redis提供的工具了,在第一部分redis基础知识中的redis的命令工具这个以标题下,提到的redis-check-aof文件就是用来修复aof文件的

redis-check-aof –fix filepath

实现细节

实际上这个写操作的日志还有一些细节问题

一种是先写日志后执行操作,写前日志

一种是先执行操作后来记录日志,写后日志

redis实际上是用的写后日志的顺序

好处:

  1. 避免检查开销,错误指令先通过执行流程进行检查
  2. 不会耽误当前的写操作,不会阻塞写操作,这样执行效率更快(可能会阻塞后面的命令)

风险:

  1. 来不及存储日志就宕机
  2. 可能会阻塞后续的命令

应对策略:

  1. 来不及的情况只能尽可能的减小影响,不能完全避免

配置文件中的对应属性是:

appendfsync 属性

redis提供了三种策略来应对不同的读写情况,以减小来不及写入情况带来的影响

Always 每条指令都同步写入一次(最多丢失一条 但是写入压力很大) 同步写回

everysec 每一秒将累积的指令一起写入(默认,可能丢失一秒的数据) 异步写回

no 先写入内存缓冲,操作系统决定写入时机(很可能是缓冲满了操作系统才去写,可能发生灾难性数据丢失) 异步写回

AOF重写机制(优化机制)

AOF文件可能会有很多冗余操作,导致文件很大,所以可以对AOF文件内容进行优化,以减少AOF内容,这样性能也会提高

redis提供了AOF重写命令

1
bgrewriteaof #实际上就是background rewrite aof

手动调用这个命令就会进行重写,但是实际上我们需要某个条件下自动的重写

条件触发自动重写通过配置文件设置

1
2
auto-aof-rewrite-percentage value # 必要条件: 新增大小达到上次重写后的百分比
auto-aof-rewrite-min-size somemb # 必要条件: aof文件需要足够大才进行重写

实际上这个检测条件的是一个子进程bgrewriteaof进程,在不影响主进程情况下进行aof重写

重写流程:

  1. 检测到触发重写条件
  2. 主进程fork一个子进程执行重写操作(也就是根据内存中的数据,计算命令)(fork出的子进程只能访问fork那一刻主进程中的内存数据)
  3. 主进程仍然提供redis服务,为了防止此时新写入的数据被子进程丢失,开辟AOF重写缓冲作为重写时期的AOF文件,原本的AOF缓冲也依然开着(也就是AOF缓冲写入不再是写入AOF文件中了,而是AOF重写缓冲中)
  4. 子进程重写完毕,通知主进程,主进程将AOF重写缓冲中的内容添加到子进程重写的AOF内容后,然后覆盖原来旧的AOF文件即可

Redis RDB技术

实际上为了解决AOF文件冗余的问题,也就是使用过多的内存去表示数据,以及aof文件效率缓慢的问题,redis提供了RDB技术,是Redis database的缩写,实际上就是内存数据的直接拷贝,存储磁盘中

内存快照,专业术语叫做.用于记录内存中某一时刻所有数据的状态

实际上也有缺点,就是每次执行快照操作,都需要全量复制数据,如果频繁的进行快照无疑会有大量开销

提供的命令操作

redis提供快照的命令有两个

  1. save 会导致主线程阻塞
  2. bgsave 不会影响主线程 (默认快照方式)

在后台进行复制,还要同时提供写服务,这是一个需要解决的问题,redis使用了写时复刻技术来实现

写时复刻技术

为了保证RDB中的数据都是同一时刻的数据,当我们在主进程进行修改的时候,主进程会保留下旧的数据作为副本,供子进程如写RDB用,等写完了,旧的副本就没用了

这个操作依据的就是断定快照时间内主进程不会写入大量的数据,这样创建的副本数量也是有限的

配置与策略

主要就是配置快照的频率,根据某些条件来触发

在配置文件中的

save 时间段 更新个数

混合使用AOF和RDB

默认配置下是RDB机制默认开启的,所以在混合使用情况下,需要将appendonly yes打开

然后同时打开混合使用的配置

aof-use-rdb-preamble yes

混合机制

实际上用一个文件,前部分使用RDB形式保存,后半部分使用AOF格式保存

持久化线程实际上就变成了AOF的重写线程了

当AOF进行重写的时候,将AOF部分表示的数据全部转换为rdb格式,没出触发重写的时候就使用AOF记录

主从复制

主从复制,用于读写分离,可以大大提高redis的读性能,并且对于数据恢复,高可用性都很好

提供了高可用性,基本可用的nosql要求

为什么读写分离,实际上如果不可以多端写入,那么就会出现数据不一致的情况,读写分离,那么我们就可以以写服务器上的数据为基准,实现数据的一致性

主库宕机,选举某个从库作为主库,宕机恢复

集群配置

每一个redis实例,要开启主从模式都需要配置自己的节点的一些信息,用于集群中redis实例之间的通信

  1. redis启动配置文件

include 配置文件路径 ,这个文件就是单机redis启动的配置文件

  1. 配置进程文件路径

这个文件是linux进程通信需要创建的文件,表示这个进程存在,一般放在/tmp目录下

pidfille filepath

  1. 配置进程的端口

端口用于通信

port number

  1. 配置持久化文件

dbfilename dump.rdb

因为从节点的所有数据变化都是由主节点同步而来的,从节点不会执行任何写命令,所以从节点使用aof没有任何意义,aof文件不会记录什么东西.所以从节点只开启rdb即可

调试命令

  1. 查看节点状态

info replication

  1. 随从其他节点

replicaof <ip> <port>

之前好像是slaveof

复制原理和策略

从库宕机

如果从库宕机,再次连接会从主库去读数据覆盖当前节点中的值

这就有一个问题,大量冗余的数据,在从库中持久化文件中还有保存,却被全部覆盖

主库宕机

主服务宕机之后,从服务器可以继续进行读数据操作,当主服务重新上线,会恢复对主节点的依赖

主从同步原理

分为几个阶段

  1. 第一阶段 建立连接:

主从建立连接,协商同步.知道是从库repliaof命令,也就是从库向主库发送请求建立连接,并且告诉主库要进行同步操作,主库回复之后就可以进行同步操作了

从库发送指令:

psync ? -1

表示的含义就是,1. psync指令标识主从复制,从库请求,其中的参数?表示不知道主库的runid(每个redis实例都有一个唯一的runid) -1表示这是从库的第一次复制,其实这个字段是offset复制进度,是用来减少冗余同步的

主库回复命令:

fullresync 指令 携带runid 以及当前的复制进度offset ,从库会记录下这两个参数

  1. 第二个阶段 同步阶段:

第一个阶段之后,主从之间信息已经相同,为同步做好了准备(数据属于那个实例?同一个ip:port的redis服务不一定就是同一个实例)(当前复制的进度)

这个阶段就需要将数据发送到从库,实际上发送的就是rdb文件,此时从库需要清空原来的数据,加载主库发送来的数据

在主库发送rdb文件的时候,此时是不允许rdb文件进行改变的,所以此时主库会开辟一块新的空间,replication buffer用来在发送期间,充当临时rdb文件,保存此时的改动

  1. 第三个阶段:
    发送 数据的修改部分即可,其实就是一些aof文件命令

主从从策略

主从模式因为只能有一个主库,而从库可以有若干个,实际上主库对若干的从库都需要负责同步,所以主库的压力很大,主从从策略就是向在不减少从库的前提下,尽可能减小主库的压力,所以采取分多个阶级的主从模式,就像统治者和人民一样,统治者不能同时管理全部的人民,所以统治者增加了官僚这个阶级,来作为自己和人民的中间层,帮助自己来管理人民,但是这样会导致信息传递效率更慢.

实际上在计算机中,这种概念叫做级联,一级一级,就像分级

redis实际上支持主从从策略,从库连接到一个从库,从库并不知道自己连接的是一个从库,而被依赖的从库在主库发送同步数据之后会同步转发给自己的从库,所以实际上主库可以提取出两个角色,一个是领导者,一个是最高领导者

所以集群中可以有很多领导,但是最高领导只能有一个

一些异常处理

主从复制最容易出现的问题就是断线问题导致巨大的同步开销

为了减小同步开销,我们尽可能不进行全量同步,所以某些情况下,我们对于短时间的掉线并不认为宕机了,而是进行增量的检查,如果实在是数据时代差距过大,才进行全量同步

网络连接异常

在redis2.8之前如果网络异常,再次连接就需要全量复制,对于不稳定网络很恼火

在之后就进行增量复制,使用的底层原理是环形缓冲区,标记主从之间数据的时代

每一个写操作(影响数据的操作)都会进入这个环形缓冲区,通过偏移量来记录当前最新的写操作是在哪

唯一的问题就是有可能主库新写入的数据超过一圈的时候,此时从库还没读到的数据就将被覆盖,所以在配置文件中有对这个缓冲区大小的配置

1
repl-backlog-size 1mb

具体配置多大,需要根据网络情况进行计算

缓冲空间大小 = 主库写入速度(也就是平均写入速度) * 操作大小(平均每个操作所占用的内存) - 主从网络传输速率* 操作大小

实际上配置要是缓冲空间的两倍

哨兵模式 -1

实际上哨兵模式是对主库宕机的解决方案,也是后面数据分片的基础

哨兵在逻辑上实现的操作就是当主库宕机的时候,将从从库中选举出一个作为主库,以继续提供reids的读写服务,减小主库宕机对整个redis服务的影响

更换主库的操作实际上可以手工操作,记得之前所有的redis实例都是一个主库,通过replicaof命令使得redis实例向其他的redis实例臣服变成从库,实际上也可以从新成为主库的

自动更换主库这个操作的实现需要解决几个问题:

  1. 检测主库宕机的手段?
  2. 推举新主库的机制?
  3. 通知外界客户端使其知晓变化?(就好像换总统,谁知道啊,下属知道了,但是也需要让外国知道)

什么是哨兵?

哨兵就像监管者,对于主库的检测,采取了很谨慎的策略,主观下线和客观下线的判断

不同的哨兵可能会得到不同的对于主库是否下线的判断,此时就需要统一意见,如果同意意见之后,那么就认为主库确实下线了,认为是客观下线(也就是确实下线了),自己得到的主库下线的判断只是主观下线(也就是我认为下线了,我不确定是否是真的下线了)

哨兵来自于不同的redis启动时开启的哨兵进程,实际上这个哨兵进程与本机上的redis实力没有什么逻辑关系,只是我位于同一台服务器而已,这些哨兵不仅仅检测主库是否下线,哨兵相互之间达成通信,

哨兵的一些工作
  1. 监控主库以及从库

    实际上哨兵会首先从主库中获得所有从库的信息,然后哨兵会依次取得所有从库的联系,实际上是发送了info命令从主库中获得从库信息

  2. 自动维护主从结构

​ 例如一个主库下线之后,哨兵会选出一个新的主库,实际上是哨兵通知其他从库,臣服那个新的主库,如果旧的主库再次上线,此时哨兵会检测到,并通知其臣服新的主库,此外还会通知客户端

​ 可见哨兵的通知功能很强大,如果没有哨兵,一个从库下线之后,那么他并不知道之前是属于某个主从结构的,而如果之前是开启了哨兵机制的话,哨兵会通知Redis实例修改启动配置文件,将replicaof命令写入到配置文件中,这样当redis实例重启的时候就能够自动重连会集群中来了(也就是说实际上是通过修改配置文件来实现记忆前世兄弟的功能)

哨兵配置文件

要启动哨兵,需要配置哨兵进程的参数

  1. 哨兵进程的通信端口
  2. 哨兵检测的主库网络地址
  3. 哨兵检测的灵敏度,也就是对判断主库下线的条件ping 回复时间
1
2
3
port 端口号
sentinel monitor <自定义的主节点名称> <ip> <端口> <客观下线条件> # 注意需要空格分隔ip和port
sentinel down-after-milliseconds <自定义的主节点名称(必须和上面的一样)> <毫秒数> # 主观判断

启动哨兵:

redis-sentinel命令 ,可见实际上哨兵的是独立于redis-server和redis-cli是同样的地位

1
redis-sentinel sentinel.conf

新主库选举机制

实际上是两部分操作组成了哨兵对于一个主库的评选

  1. 剔除(筛选)
  2. 打分

​ 有一些不符条件的自然不会有资格进入流程,进入流程也是需要开销的

  1. 已经掉线的库不要
  2. 频繁掉线的不要(哨兵确实知道你这个redis实例是否是频繁掉线 )

打分来自:

  1. 优先级

优先级是通过配置文件 静态设置的

replica-priority <优先级>

这个参数主要用来体现服务器的质量,服务器和网络质量好的我们自然设置高一点

  1. 同步程度

主库下线,我们希望尽可能的少丢失数据,所以我们可以通过同步偏移量来判断那个从库更完全

  1. 第三轮,看岁数

继承人都很优秀或者都很差劲,剩下的交给天意,根据redis实例的runid小的就直接上

哨兵集群

单个哨兵显然可信程度不高,容易造成误判,所以我们多来集合哨兵一起判断

哨兵集群相比于单个哨兵主要的改进方向

实际上多个哨兵主要干的就是少数服从多数,对于主库客观下线的判断上更好

另一个是集群有更好的故障容错率,掉线几个哨兵对哨兵的基本功能没有影响

如何建立集群?哨兵如何通信?

当有新的哨兵找上主库的时候,主库会在哨兵都监听的一个频道上发布新的哨兵的ip port信息,这样集群中的哨兵就知道有新的哨兵加入了,会主动去联系新哨兵

哨兵运行日志的给出的信息:

  1. +sdown 主观认为下线
  2. -sdown 不再认为下线
  3. +odown 认为确实下线
  4. -odown 认为已经上线
  5. +switch-master 切换了检测的主库
  6. +slave-reconf-inpro 从库更新了主库,但是还没有同步数据
  7. +slave-reconf-done 从库完成对于新主库的同步
  8. +slave-reconf-sent 哨兵配置新上线的库服从新主库的命令
哨兵运行机制

更底层的通信机制是基于redis中的pub/sup发布/订阅机制

简单来说,发布订阅就是一些频道,大家都可以在上面发送东西,大家通过监听一些频道来通信

这样的好处就是可以实现多对多的通信

发布订阅机制

因为发布订阅机制很重要,所以直接开启一个三级标题进行描述,实际行文逻辑上这部分内容是属于哨兵模式的

一些概念

这个机制是在主从复制,哨兵集群,等机制下都存在的一个底层的机制,用于通信,所以建立了主从集群和哨兵集群之后,哨兵发布的消息,redis的不同实例都可以接收到,实例发送的消息其他的实例哨兵,客户端也能够接收到

  1. subscribe订阅

订阅会进入等待状态,此时是阻塞的,所以实际上会启用多线程进行异步订阅而不是同步订阅

  1. publish发布

  2. channel频道

频道是一个字符串,实际上就是保存在一个字典中

哨兵模式 -2

哨兵之间通信的底层

哨兵会在一些事件频道上发布和订阅,例如+sdown是一个频道,哨兵发布信息在上面,那么所有的哨兵都知道了这个事

客户端也可以订阅这些频道

哨兵执行者

执行某个操作,例如切换主库的操作究竟是那个哨兵发出的命令,这个其实也需要投票,目的是选出最靠谱的哨兵,实际上就是网速最快的哨兵,每个哨兵会同意第一个做领导的请求,拒绝后面的党领导请求,所以如果网速更快,先发送到更多的哨兵,那么就更可能当上此次行动的领导

首先发现主库掉线的哨兵通知其他哨兵对主库进行查看,如果最后得到超过配置文件中的配置的票数,那么你就是领导

注意事项

基于pub/sub实现的哨兵集群(如何建立集群,哨兵之间的通信,通知更换主库)

info命令获取从库信息

给予哨兵自身的sub/pub功能,实现客户端的通知

注意哨兵之间的down-after-milliseconds需要配置相同,否则会出现逻辑问题(隐藏的错误)

redis分片集群

在上述的集群中,还是只能有一台主库负责整个redis集群的写服务,而每个服务器实际上的内存和性能有限,根本无法满足如今大数据时代海量的数据,所以分片集群解决的事对于主库的横向扩展,使得多个库共同负责主库的写入工作

为了保证数据的一致性,实际上不同的分片所负责的数据是唯一的,并不会与其他分片冲突,这样就不会出现数据的不一致情况,那么这个数据如何分配呢?

redis槽的概念

redis是键值对数据的操作,所以redis可以根据操作数据的键名,计算hash落在哪个区间,这个计算可以是客户端自己计算,redis集群查找这个区间在哪台机器上,将操作命令转交给对应负责槽的服务器,如此一来每个数据就都能被分成基本上均匀的分片

集群配置

在配置文件中除了配置主从复制的部分

1
2
3
4
include redis.conf
pidfile "/tmp/run/redis_6379.pid"
port 6379
dbfilename "dump.rdb"

关于分片的配置:

1
2
3
4
cluster-enabled yes #启动分片集群
cluster-config-file nodes-6379.conf #生成的分片集群配置文件
cluster-node-timeout 15000 #分片集群的节点超时时间,会自动进行主从切换

使用此配置文件启动redis-server之后,每个redis实例就拥有承担槽的能力,算是redis主库的一部分,因为从库是有可能被提升为主库的,所以从库也需要开启cluster-enable等配置

启动六个实例之后,通过redis-cli进行设置集群信息,将六个实例分为不同的

1
redis-cli --cluster create --cluster-replicas 1 someportandip

实际上是使用cluster create命令

还有一些配置在redis的启动配置文件中

  1. 分片的主从都宕机整个集群是否关闭

cluster-require-full-coverage yes #默认yes表示全部宕机

redis客户端

客户端要使用分片模式启动,这样才能够去计算槽,以及去重定向

redis-cli -c -p <port>

计算hash槽位置

有时候我们希望我们的数据存储在一个分片上

例如mset [key1] [value1] [key2] [value2] …

如果这些key存储在一个分片上我们可以使用特殊的key形式,

例如 name{user:001} age{user:001} 这种键在计算的时候只会计算大括号里的值

常用命令

1
2
3
4
cluster nodes #查看集群信息
cluster keyslot <key> #查看key对应的hash槽
cluster countkeysinslot <slot> #返回hash槽中的key的数量
cluster getkeysinslot <slot> <count> #获取对应count个key从hash槽中读取

redis中的亿量数据运算

redis对于大量的数据的处理可谓是专业,常用的大量数据的运算也就这几种

1 聚合统计(集合运算)

实际上可以叫做集合运算,例如集合的交集,差集,并集,这些在redis的set数据类型就一共这些运算

使用set的sinter ,sunion,sdiff就可以了 记得使用siterstore来保存做一个集合

2 排序统计(找到某一段)

常见的问排序就是zset和lList可以进行排序

List的顺序和添加顺序是映射的,所以这个只适合保存顺序,而不适合排序

所以使用zset来进行亿量数据的排序

3 二值状态

也就是每次的可能都只是 两种可能,这些可能的一连串发生就是二值状态

例如签到,打卡,等等

这种情况可以使用1bit来表示状态从而二值状态可以得到最大可能的压缩,非常高效

redis中提供的bitmap实际上就是string二进制安全的也是一样的,我甚至可以使用setbit来修改字符串键值对中的值,实际上通过查看object encoding可以发现底层都是raw实现,使用type可以发现都是string类型

image-20240114160633025

bitmap就是bit的数组,关键是redis提供的操作:

  1. setbit 可以改变一个位的状态

例如 setbit user:sign:2022 0 1

实际上的模板是 setbit key offset 0或1

  1. getbit 获取一位的状态

例如gitbit user:sign:20222 0

实际模板就是 getbit key offset

  1. bitcount 统计1的个数,也就是说默认都是0

实际上可以选中一个区域进行统计

1
bitcount <key> [startIndex Of byte] [endIndex of byte] #这里包含左右两边,实际上index是字节的index而不是bit的index,一个byte = 8bit

如果想要精确统计位数的count,可以先进行与运算,将不想要统计的位数都设置为0,想统计的位数都设置为1,这样一进行与运算,就只会保留想要统计的位数的1,ok这时候再进行bitcount即可

  1. 与运算
1
bitop and <save_key> <key1>[ <key2> ... <key2>]

bitop指的是bit操作

and表示的是与操作

表示的就是保存的bitmap的key

  1. 设置过期时间

实际上set也就是字符串类型可以设置过期时间,但是可以使用bitmap命令操作这些类型,从而实现设置过期时间

通过expire命令就可以设置过期时间了,而string类型的ex参数,只是让expire更好用了而已

4 基数统计

基数统计实际上就是成员统计,去除重复元素,计算实际上有多少个成员

使用到的技术是概率统计,通过概率算法计算大概的有多少

传统的解决方法就是使用两种数据结构:

  1. set集合

通过sadd来添加成员

通过scard来统计成员个数

  1. hash类型

先查看hexists,不存在再进行添加

通过hlen获得成员个数

redis提供的专门基数统计类型

HyperLogLog类型

实际上这个也是set集合的一种.

优点:

​ hll类型的空间占用是固定的,不会随着数据量的增加而增加

缺点:

​ 有误差,0,81%左右的误差

hyperloglog操作命令

pfadd <key> [member1] [member2] #添加

pfcount <key> #统计

pfmerge <key1> … #做合并 ,全部合并到第一个hyperloglog中

小结

image-20240114163448972

Geospatial类型

就是二维空间定位的类型,这个类型不仅仅是保存一个位置的信息,而且可以进行位置而运算,能够计算位置之间的距离,空间范围内的位置

先从底层开始理解

底层数据结构

底层是使用zset实现的geo类型,主要的技术就是生成权重分数,这个分数是根据经度和纬度计算的

这意味着zset的命令可以适用于geo

根据二分算法,如果经度在右边半边就是0,左半区就是1,然后再将之前的去划分成两个区,重复进行操作

最后合成的分数就是依次每位取一个计算的经度二值,然后就是取一个纬度的二值

这样就形成了从模糊到精确的位置编码

image-20240114164346924

geo类型的操作命令

  1. 创建和添加

geoadd [key] [经度] [纬度] [位置名] [经度] [纬度] [位置名] …

因为geo实际上也是zset,所以这些命令都很像zset

  1. 通过位置名 取得位置的经纬度

geopos [key] [ member]

就是geo position的意思

取出来的都是double类型,会有误差

  1. 计算距离

geodist [key] [one_member] [other_member] [m|km]

最后的参数是单位的选择

  1. 排序范围查询

查找距离半径内的数据

georadius [key] [经度] [纬度] [半径] [单位] [可以添加withcoord会带上经纬度信息] [withdist会带上距离信息] [可以使用 count 具体的数量来限制结果数量] 可以通过 des asc来设置结果顺序 可以通过 [store key] 来保存结果到一个geo

会返回geo中的符合条件的member

redis中的事务操作

事务是为了保证不会发生一些问题

  1. 原子性 要么全部执行,要么全部不执行
  2. 一致性 数据库的数据任何时间都是正确的
  3. 隔离性 执行期间不会被其他干扰
  4. 持久性 操作要被持久化保存下来,重启之后也不会丢失

ACID要求

redis中的事务实际上并不能完全满足四个要求,但是也基本上满足满足事务的特征

开启事务

使用multi命令开启事务模式,以下的命令都是事务的一部分

exec命令执行事务

实际上的情况分几个阶段

  1. 开启事务

客户端使用multi命令开启事务模式

  1. 客户端发送其他命令

这个时候服务武器会接受这些命令,但不会立即执行,但是在接受命令的时候会检查语法错误,无法检测运行错误

  1. 客户端发送exec命令

客户端提交exec命令之后,服务器去执行事务中的具体命令队列中的命令

其他操作

  1. 取消事务

客户端中途取消事务使用命令

1
discard #此时服务器会丢弃命令队列

ACID的问题

事务中命令的语法出现题

当客户端提交语法有问题的命令的时候,服务器会检测出语法问题,并给出报错,如果客户端仍然执行exec命令,服务器会拒绝执行,返回事务失败结果

原子性满足

一致性满足 (没有脏数据)

服务器未检测出错误,但运行时出错(不能保证原子性)

一些运行时错误,redis在一开始无法检测出

出现运行时操作并不会回滚,后续指令也能成功执行**(这一点和ACID要求不符)**

原子性 不满足

一致性 满足

redis服务器突然宕机

此时一部分修改进入到aof文件,但是此时不能正常启动redis服务,使用aof修复工具,相当于会撤销不完整的事务操作,

原子性满足

一致性 取决于rdb和aof机制是否开启

无法保证事物的持久性

aof 的三种策略也无法保证不会丢失数据

rdb业务无法保证

所以实际上redis无法保证事务的持久性

手动实现redis事务的隔离性

为了防止在事物执行过程中,需要用到的值被其他所修改,所以事务在需要使用一个数据的时候,需要保证只能自己操作

redis提供的watch命令可以监控一个键值对,之所以不使用互斥锁,是因为不想要大幅降低redis的并发性能,所以这个是一个乐观还是悲观锁?

悲观锁: Synchronized每次修改都要加锁,确保正确执行(必须要执行)

乐观锁: Atomic原子操作 如果被修改就放弃,不强制有要求能够执行(不能执行就算了)

如果watch的键值对,在事务期间被修改那么,事务执行失败.

1
2
3
4
watch key1
multi
get key1
exec

使用unwatch取消所以watch,事务执行或者取消的之后就自动unwatch了,不需要再次调用

redis的缓存应用

redis在实际应用中,最为广泛的用法就是作为关系型数据库的缓存

缓存的两种构型

  1. 穿透性缓存
image-20240114191144780

服务端直接从缓存中读取数据,向服务端隐藏调用细节,如果无法读取到数据就再从数据库中更新数据到缓存,再去返回给服务端.

写入数据也是先写入缓存中,再去同步到数据

这个就像是数据库的外交官一样

服务只认识缓存

而缓存认识服务和数据库

  1. 旁路型缓存
image-20240114192654312

先到缓存中寻找,如果没有再到数据库中读取,服务再去同步数据库到缓存

这个就像是服务的备忘录一样

服务认识缓存和数据库

缓存只认识服务

redis实际上实现的就是旁路型缓存

缓存的特征
  1. 效率高,速度快
  2. 容量小

redis的的两种情况

缓存命中和缓存缺失

缓存缺失就需要更新缓存

相关代码:

1
2
3
4
5
6
7
8
String cache_key = "user001";
String cache_value = redis.get(cache_key);
if(cache_value != null){
//相关业务逻辑代码
}else{
cache_value = mysql.getUserById(cache_key); //向数据库获取
redis.set(cache_key,cache_value); //缓存更新
}

所以想要使用redis就必须修改服务端的逻辑代码,如果没法修改源码,那么无法使用redis作为缓存

缓存的类型

只读缓存

读写缓存

只读缓存

这个只读指的是,写,等修改操作,直接向数据库操作,而不操作缓存.

好处就是:
只需要一次写入即可,对于本次的写入效率高

问题就是:
缓存中可能出现旧数据,需要将旧数据删除,下一次还得到数据库读取数据并更新缓存数据

实际上就是默认我们的数据不具备局部性原理,这样种类型就适合只读缓存

读写缓存

直接对缓存进行操作,尽量少的对数据库进行操作

所利用的就是数据的局部性原理,相信刚刚修改的数据会在近期接受大量请求

优点:

​ 相应特别快

缺点:
可能造成数据丢失,毕竟是内存数据

为了保证数据不丢失,持久化到数据库,另一方面又要高效响应

就有了两种策略

  1. 同步直写
  2. 异步写回

同步直写: 就是当缓存中更新更新数据之后立马先去更新到数据库,保证数据不丢失

异步写回: 另外有异步线程去负责写入更新到数据库,等到淘汰可能才会去写入数据

缓存数据的淘汰

须知缓存的容量小的这个特点,所以将使用频率低的数据淘汰是一种必要的操作,就像是操作系统中的LRU页面置换算法一样,为了提高利用率

淘汰数据有几种方法来确定一个数据是否无用

设置淘汰时间

有些数据在某个时间点一过就失去了价值,这种数据就可以使用淘汰时间,另外大部分的数据都遵循局部性原理,时间越长那么再次访问该数据的概率就越小,所以通过时间来决定淘汰很好

expire命令

可以通过ttl查看key状态,实际上已过期的数据,redis并不是立马删除,就像是unlink一样,立马删除需要很大的性能开销,redis有限去执行读写操作,删除操作延后执行

淘汰操作

redis中的键都存储在一个map中

所有的设置了过期时间的键值都存在expires这个map中,值就是过期的时间戳

  1. 定时删除

    创建一个定时器,当过期时间到达,就删除key,expires中的也删除

优点:

​ 节约内存资源

缺点:

​ CPU压力大,对redis的正常执行效率有影响

  1. 惰性删除(延时删除)

先不去删除,等到下一次再次访问这个数据之前,一定会去删除

在get数据的时候,限制性redis中的内部函数,expireIfNeeded(),如果没过期就返回,过期就删除,并返回-2

优点:
节约了cpu资源

缺点:

​ 内存浪费

  1. 定期删除

redis启动服务的时候,读取参数,server.hz的值,默认就是10,可以通过info server查看

每秒执行server.hz次定时轮询,调用serverCron()函数 这个函数就是去对每个数据库进行databaseCron(),有16个数据库,执行activeExpireCycle(),检测其中的元素的过期情况,每次轮询执行250ms/server.hz的时长(限制cpu占用)随机从对应的库中取出20个(默认)key进行检查

如果过期就删除key

如果抽取中了1/4那么再来查一次这个数据库,

否则检查下一个数据库去了

redis使用的就是惰性删除加上定时删除策略

数据逐出算法

和淘汰差不多,实际上就是淘汰操作是根据时间知道淘汰时间,而逐出操作就是计算没有淘汰时间的数据的是否淘汰

也就是相当于比淘汰数据多了一步就是计算是否淘汰

设置最大的缓存容量,来作为redis的内存数据的占用,一般情况下设置为15%到30%,实际情况下设置50%可以,不加设置就是全部使用

这个参数就是maxmemory <bytes>

在启动配置文件中,还需要配置逐出算法

image-20240114201508395

上图就是具体的算法

默认情况下redis不进行数据的淘汰noevction,如果redis写满,再有添加,直接返回错误

过期数据淘汰

我们只删除设置了过期时间的数据,虽然这些数据暂时还没过期…

valotile-ttl

根据过去时间的先后顺序,越做过期的越先删除

valotile-random

在设置了过期时间的数据中随机删除

valotile-lru

实际上和操作系统的页面置换算法中的lru是一样的,需要计算上次访问时间

最近最久未使用的淘汰

缺点就是:
需要使用链表来管理所有的缓存数据,进行一个使用频率上的排序,产生内存开销,每次访问需要改变链表中节点的位置,造成redis的性能下降

一次性淘汰N个数据

这个配置在启动配置文件中的maxmemory-sample 这个属性

valotile-lfu

最近使用频率,频率低的淘汰,如果

这种就可以避免偶发性的访问

全部数据淘汰

在所有数据范围内进行淘汰

实际上就是

allkeys-random/lru/lfu三种算法

算法选择

优先选择 allkeys-lru

如果业务访问频率不达,选择allkeys-random也不错

如果有些数据很重要,不是能逐出,我们就是用volatile-lru,这样不给重要数据设置过期时间,那么就永远不会淘汰

逐出可能导致的问题

记得使用读写缓存的时候,一种策略就是异步写回,这种写回等到下次访问这个数据才会去同步到数据库,如果在redis还没有同步到数据库的时候,这个数据被redis逐出了,那么redis实际上并不会去做什么别的操作,直接就会去删除,那么数据库中就永远丢失了这最新的数据

redis缓存可能导致的问题

四个方面

  • 数据不一致
  • 缓存雪崩
  • 缓存击穿
  • 缓存穿透

数据不一致情况

数据一致的情况还需要做以下描述:

  1. 缓存中有数据,并且和数据库中的数据一样
  2. 缓存中没有数据,并且数据库中的数据是最新数据

如果不符合以上两种情况就是数据不一致,实际上就是新数据不能读到的意思

<><>如果是我们的只读缓存:

  • 对于新增操作 :

​ 数据会直接写在数据库中,缓存中没有新增数据的信息,所以数据是一致的

  • 对于删改操作:
    如果先删除缓存中旧的数据,再去更新数据库,可能导致删除成功,更新失败的情况,这样可能导致,新数据丢失,只能拿到旧的数据

​ 如果先去更新数据,再去删除旧缓存数据,可能导致没有成功删除旧的缓存,导致还是拿到旧的数据

实际上的问题就是服务器并不知道失败了,及时知道失败了,可能自己也把数据忘了,

这个问题无论那种顺序都会遇到

解决方法:

​ 重试机制,也就是服务端先把这个数据记忆一下存储在消息队列中,我们更新之后再去访问一编看看是否成功,失败了再去重新更新,成功了就从消息队列中去除这个数据

  • 并发访问的情况

<>如果是先删除缓存,再去更新数据库:

​ 知道我们缓存中的更新是在下一次访问缓存发现未命中是发生的,如果在更新数据的时候,删除了缓存中的旧值,还没来得及更新数据库的时候,有另一个请求去到缓存发现未命中,再去数据库中读取到旧值,同步到redis中,相当于就是第一个更新操作没有执行删除缓存中的旧值一样,会导致缓存中是旧值,而数据库中是新值的情况

​ 实际上就是没有隔离性

解决方法:
让更新进程,等一会,缓存中出现被其他更新的旧数据,再删除一次

​ 叫做”延迟双删”

​ 实际上就是变成了先更新书数据库,再删除缓存

<>如果是先更新数据库,再去删除缓存:
这种方式就是我们推荐的方式,本身延时双删就是模拟这种操作

实际上就是多了一点点的时候延迟更新了,也就是更新更慢一点而已

缓存雪崩

也就是同一时间,大量的数据过期了,被删除了,缓存相当于一片空白

缓存突然失去作用,这就是缓存雪崩,会导致大量的请求进入到数据库,导致数据库崩溃,导致应用服务器崩溃

引起雪崩的原因有几种:

<><>缓存中的大量数据短时间同时过期

解决方案:
1. 数据放置在服务器段,做静态处理,别放在数据库中,生成静态页面,少用动态页面
1. 避免大量数据过期时间过于集中,为过期时间追加随机数,均匀分布在一些时间段(1-3分钟).
1. 构建多级缓存架构: redis缓存 + nginx缓存 + echcache缓存 ,这样压力不会一下子给到数据库,而是给到了下面的缓存架构
1. 延长或者取消热度超高的数据过期时间,我们可以人工判断,等到请求量下降我们再去删除
1. 服务降级,也就是把服务关了,不让这些服务进入缓存

<><>或者redis服务实例出故障:
解决方案:

  1. 服务熔断或者限流 ,也就是敷衍,发现redis崩了,服务端就敷衍客户,而不是去强数据库,实际上熔断也相当就是暂停服务,影响其实也是挺大的,可以用限流来降低影响,通过拒绝一些请求来进行限流
提前预防

除了事后解决之外,预防操作才是平常最关键的

可以通过几种方式来预防:

  • 灾难预警: 监控redis的服务器性能指标

  • 配置redis集群: redis某些实例出现故障,通过主从切换等操作降低影响

缓存击穿

从名字上来看,击穿就是某个点压力过大,导致失效

也就是某个热点数据,失效,然后也是大量请求给到数据库,与缓存雪崩的情况不同之处就是一个是大量数据,一个是某些数据,所导致的本质后果都是一样的,大量请求给到数据库压力

解决方案:
1. 预先设定 延长过期时间,让热点数据不热了再去过期
1. 实时监控 监控访问量,如果发现淘汰某个数据导致访问量激增,我们要把这个数据搞成永久的或者长时间的
1. 定时任务 在后台去控制数据的有效期,如果发现热点数据快要过期就延长过期时间
1. 分布式锁 (不太推荐,性能下降) 也就是获取锁,然后我现在进行查询数据库并更新缓存的任务,其他的竞争者都阻塞

缓存穿透

也就是访问数据库中不存在的数据,本来如果没有缓存,也只会经历一次数据库的查询

如果加上缓存,那么就会再多上一次查询缓存中是否有数据,再去查询数据库中是否有数据.

可能的原因:

  1. 业务层误操作,把数据删了,导致查询不到
  2. 恶意攻击 故意去查询不存在的数据

解决方案:
1. 没有数据我们制造数据,可以给一个空值之类的,使得这些请求被拦截在缓存这一层
1. 布隆过滤器,可以做到快速判断数据是否存在,但是会导致所有请求的性能下降
1. 将不合理请求拦截在服务端, 在服务端拦截不合理的请求
1. 实时监控 redis的命中率下降可以就是异常出现
1. key加密 会将所有数据的key加密后保存在

jedis java连接redis

第一步,添加依赖,使用maven的管理工具很轻松就可以导入jedis依赖

需要注意的是,如果是直接通过java操作jedis,没有使用什么框架依赖的话,需要为jedis添加一些日志依赖,不然会报错

另外注意redis的bind_ip以及linux的防火墙还有protect-mode no

确保redis能被外界连接


redis重新学习
https://wainyz.online/wainyz/2024/03/15/redis重新学习/
作者
wainyz
发布于
2024年3月15日
许可协议