Redis 学习笔记

Redis

Redis是一种基于键值对的NoSql数据库。
与很多键值对数据库不同的是,Redis中的值是有string、hash、list、set、zset、Bitmaps、HyperLogLog、GEO等多种数据结构和算法组成,因此redis可以满足很多应用场景,而且因为redis会将所有数据都放在内存中,所以它的读写性能非常惊人。不仅如此,redis还可以将内存的数据利用快照和日志的形式保存到硬盘上,这样在断电或故障是,内存中的数据不会丢失。
此外,redis还提供了键过期、发布订阅、事务、流水线、Lua脚本等附加功能。

应用场景:

缓存:加快数据的访问速度
排行榜系统:例如按照热度排名
计数器应用:浏览数等
社交网络:赞踩、粉丝、共同好友、推送、下拉刷新
消息队列系统

数据库管理

1.切换数据库:select dbIndex
Redis使用数字作为多个数据库的实现。
redis默认配置中有16个数据库,select 0将切换到第一个数据库,select 15切换到第16个。各个数据库没有关系,可以存在相同的键。
默认使用index=0的那个数据库
数据库管理
但是,redis3.0中已经弱化了多数据库这个功能,原因有3:
1、redis是单线程,如果使用多个数据库,使用的是同一个cpu,彼此间还是会影响。
2、部分redis客户端不支持这种方式,而且容易弄混。
3、多数据使用方式会让调试和运维不同业务的数据库变得困难,如果有一个慢查询会影响其他数据库,会使别的业务方定位问题变得困难。
所以如果需要使用多个数据库功能,可以在一台机器上部署多个reids实例,彼此用端口来区分。(可以充分发挥多cpu资源)
2.清除数据库:flushdb/flushall
flushdb:只清除当前数据库
flushall:清除所有数据库
存在2个问题:
1、误操作,后果不堪设想(rename-command配置可以规避这个问题)
2、存在阻塞redis的可能性

数据结构和内部编码

Redis对外的的数据结构:string、hash、list、set、zset
每种数据结构都有自己底层的内部编码实现,而且是多种实现,用来应对合适的场景。
可以用object encoding命令查询内部编码
内部编码
这样设计的好处:
1-解耦内部编码实现和对外的数据结构和命令;
2-不同场景下选用不同的编码(ziplist省内存等);

单线程架构

redis使用单线程架构和I/O多路复用模型来实现高性能内存数据库服务。
redis单线程处理命令,从客户端到达的命令不会立即执行,都会先进入一个队列,然后逐个执行,不会存在多个命令被同时执行的情况。
为什么这么快?
1-纯内存访问:内存响应时长是100ns
2-非阻塞io:redis使用epoll作为I/O多路复用技术的实现以及使用自身事件模型;
3-单线程避免线程切换和竟态产生的消耗

字符串

字符串类型的值实际可以是字符串、数字、二进制,但是最大值不能超过512M。

内部编码
redis会根据当前值的类型和长度决定使用哪种内部编码实现
int:8个字节长整型
embstr:小于等于39个字节的字符串
raw:大于39个字节的字符串
典型使用场景
1、缓存功能:减小数据库的压力
2、计数
3、共享session
4、限速:流控

哈希

内部编码
1.ziplist(压缩列表):
优点:节省内存。
使用条件:哈希类型元素个数小于hash-max-ziplist-entried(默认512) && 所有值都小于hash-max-ziplist-value(默认64字节)
2.hashtable(哈希表):
在ziplist不满足使用的时候专用hashtable,为了加快速度
使用场景
哈希类型和关系型数据库2点不同:
1-哈希类型是稀疏的,关系型数据库是完全结构化的
2-关系型数据库可做复杂的关系查询,redis如果模拟这类操作的话开发困难,维护成本高;

3种方法缓存用户信息对比
缓存用户信息对比

列表

用来存储多个有序字符串。
一个列表最多可存储2^32-1个元素。
可对两端插入和弹出,可获取指定范围的元素列表、指定索引下的元素(可充当队列和栈)。
2个特点
1.元素有序(按照插入的顺序)
2.列表元素可重复

lrange 0-1:从左到右获取列表所有元素。

内部编码
1、ziplist
使用条件:元素个数小于list-max-ziplist-entried(默认512) && 所有值都小于hash-max-ziplist-value(默认64字节)
2.linkedlist(哈希表):
在ziplist不满足使用的时候专用hashtable,为了加快速度
3.quicklist(v3.2以后)

使用场景
1.消息队列
消息队列
2.文章列表
用哈希存储每篇文章的内容,用list存储每篇文章。
设置文章内容
hmset article:1 title xx cotent xxx
hmset article:2 title xx3 content x4
放进list
lpush user:1:articles article:1 article:2
分页获取
articles = lrange user:1:articles 0 9
for article in {articles}
hgetall {article}

image-20200817172902905

总结:自定义写入和读取的规则,然后将这几种数据结构进行组合使用
这里写图片描述

集合

用来保存多个字符串。
不允许有重复元素。
无序,不能同步下表获取元素。
一个集合最多可存储2^32-1个元素。
除了支持集合内的增删改查,还支持多个集合取交集、并集、差集。

集合间操作
集合操作

内部编码:

intset(整数集合):条件
1、当集合中的元素都是整数
2、个数小于set-max-intset-entries配置(默认512)

hashtable(哈希表):
当intset不能满足时用hashtable

使用场景
标签(给用户打标签)

在微博应用中,每个人的好友存在一个集合(set)中,这样求两个人的共同好友的操作,可能就只需要用求交集命令即可。

有序集合(sort set)

元素不能重复,元素可以排序。(通过用户额外提供一个优先级(score)的参数来为成员排序,并且是插入有序的,即自动排序。)
为每个元素设置一个分数作为排序的依据。(分数可以重复)
提供了获取指定分数和元素范围查询、计算成员排名等功能。
有序集合
列表、集合、有序集合三者异同点
异同

集合间的操作
交集:zinterstore destination numkeys key [key …] [weight weight [weight…]] [aggregate sum|min|max]
按照元素求交集,然后对他们的分数*weight做sum|min|max操作
并集:zunionstore destination numkeys key [key …] [weight weight [weight…]] [aggregate sum|min|max]

内部编码
ziplist:
使用条件
1、元素个数小于zset-max-zpilist-ectries配置(默认128个)
2、每个元素的值小于zset-max-ziplist-value(默认64字节)
skulist

使用场景
排行榜系统
排行榜

Bitmaps

数据结构模型
Bitmaps本身不是一种数据结构,实际上是字符串,但是可以对字符串的位进行操作
Bitmaps单独提供了一套命令,所以在redis中使用bitmaps和使用字符串方法不太相同。
可以把bitmaps想象成一个以位为单位的数组,每个单元只能存储0和1,数组的下标在bitmaps中叫做偏移量;
bitmap示意图
命令
例子:将每个独立用户是否访问过网站存放在Bitmaps中,将访问的用户记做1,没有访问的记做0,用偏移量作为用户id。
命令
示意图
Bitmas分析
场景有所限制。
如果网站有1亿用户,每天访问的用户有5千万,那么每天用集合类型和bitmaps分别存储活跃用户可以得到。
对比1
但如果网站每天的独立访问用户很少,使用bitmaps就不太合适了。
对比2

HyperLogLog

HyperLogLog并不是一种新的数据结构(实际为字符串类型),而是一种基数算法,通过HyperLogLog可以利用极小的内存空间完成独立总数的统计,数据集可以使IP、Email、ID等。
HyperLogLog提供了3个命令:pfadd、pfcount、pfmerge。
HyperLogLog示意
3个命令
命令
集合类型和HyperLogLog占用空间对比
对比
注意:
HyperLogLog内存占用率非常小,但是存在错误率,所以开发者需要权衡,只需要考虑以下两条:
1、只为了计算独立总数,不需要获取单条数据;
2、可以容忍一定的误差率;

image-20200817205416267

GEO

Redis3.2版本提供了GEO(地理信息定位)功能,支持存储地理位置信息用来实现注入附近位置、摇一摇依赖于地理位置信息的功能。

  • geoadd:添加地理位置的坐标。
  • geopos:获取地理位置的坐标。
  • geodist:计算两个位置之间的距离。
  • georadius:根据用户给定的经纬度坐标来获取指定范围内的地理位置集合。
  • georadiusbymember:根据储存在位置集合里面的某个地点获取指定范围内的地理位置集合。
  • geohash:返回一个或多个位置对象的 geohash 值。

geoadd

geoadd 用于存储指定的地理空间位置,可以将一个或多个经度(longitude)、纬度(latitude)、位置名称(member)添加到指定的 key 中。

geoadd 语法格式如下:

GEOADD key longitude latitude member [longitude latitude member ...]

以下实例中 key 为 Sicily、Catania 为位置名称 :

image-20200817205559751

消息订阅

Pub/Sub 从字面上理解就是发布(Publish)与订阅(Subscribe),在Redis中,你可以设定对某一个key值进行消息发布及消息订阅,

当一个key值上进行了消息发布后,所有订阅它的客户端都会收到相应的消息。这一功能最明显的用法就是用作实时消息系统,比如普通的即时聊天,群聊等功能。

客户端1:subscribe rain

客户端2:PUBLISH rain “my love!!!”

发布一发布就发布出去了 所以测试时要先监听

image-20200817174626436

image-20200817174814160

慢查询日志

记录下来执行超过阈值的那些命令,做分析。

Redis执行一条命令的4个步骤:
1.发送命令
2.命令排队
3.命令执行(慢查询统计的时间)
4.返回结果
执行步骤
慢查询的两个配置参数
慢查询分析配置
修改参数的方法:
1-修改配置文件
2-用config set命令
config rewrite:将配置持久化到本地配置文件
慢查询日志的存放和访问
存放在内存中,可以通过一组命令来对慢查询进行访问和管理
慢查询访问
实践
1、slowlog-max-len:增大慢查询列表可见或慢查询被提出的可能,例如线上可设置1000以上;
2、slowlog-log-slower-than:默认是10ms,根据并发进行调整。对于高并发场景,命令执行在1ms以上,那么qps最多只能到1000,所以高QPS场景建议设置1ms;
3、当客户端出现超时请求时,首先应该查询是否有慢查询,因为慢查询只记录了命令的执行时间,如果有多个慢查询的话会影响到后续的请求
4、可以定期执行slowget命令,并将其进行持久化,防止man查询日志丢失

事务与lua(原子性操作/分布式锁)

为保证多条命令的原子性,redis提供了事务功能以及集成lua脚本来解决这个问题。
redis中的事务
命令放在multi和exec中间,如果要取消用discard代替exec

image-20200817175844510

multi
sadd user:a:follow user:b
sadd user:b:fans user:a
exec

出错情况

出错情况

redis的事务不具备原子性

从下图可以看出zyf不是integer不能加1 所以出错了 但是不影响后面命令的执行

image-20200817191958166

watch监听

可以使用watch key[key…]来指定监听键,如果值发生变化那么事务不会执行

监听一个键 然后开启事务

用另外一个客户端来模拟修改键的值

然后执行事务的命令

返回nil 表示事务执行失败**(虽然执行失败 但是值还是被其他的改变了)**

image-20200817192736990

单命令实现原子性操作
  • Redis 提供了 INCR/DECR/SETNX 命令,把RMW(读、自增、写)三个操作转变为一个原子操作
  • Redis 是使用单线程串行处理客户端的请求来操作命令,所以当 Redis 执行某个命令操作时,其他命令是无法执行的,这相当于命令操作是互斥执行的
加锁(分布式锁)

加锁主要是将多客户端线程调用相同业务方法转换为串行化处理,比如多个客户端调用同一个方法对某个键自增(这里不考虑其它方法或业务会对该键同时执行自增操作)

  • 调用SETNX命令对某个键进行加锁(如果获取锁则执行后续RMW操作,否则直接返回未获取锁提示)

  • 执行RMW业务操作

  • 调用DEL命令删除锁

    因为 setnx只能是不存在的键才能设置成功 只要有一个请求进来设置了 那么其他的执行到这里都是null

    $rs = $redis->setNX($key, $value);
    if ($rs) {
        //处理更新缓存逻辑
        // ......
        //删除锁
        $redis->del($key);
    }
    
  1. 加锁风险一
  • 假如某个客户端在执行了SETNX命令加锁之后,在后面操作业务逻辑时发生了异常,没有执行 DEL 命令释放锁。

  • 该锁就会一直被这个客户端持有,其它客户端无法拿到锁,导致其它客户端无法执行后续操作。

    解决思路:给锁变量设置一个过期时间,到期自动释放锁

SET key value [EX seconds | PX milliseconds] [NX]
  1. 加锁风险二

    如果客户端 A 执行了 SETNX 命令加锁后,客户端 B 执行 DEL 命令释放锁,此时,客户端 A 的锁就被误释放了。如果客户端 C 正好也在申请加锁,则可以成功获得锁。

    解决思路加锁操作时给每个客户端设置一个唯一值(比如UUID),唯一值可以用来标识当前操作的客户端。

​ 在释放锁操作时,客户端判断当前锁变量的值是否和唯一标识相等,只有在相等的情况下,才能释放锁。(同一客户端线程中加锁、释放锁)

​ 但是还是有风险 如果判断了UUID相同 但是锁过期了 同时别的客户端拿到锁 去执行解锁就变成了解锁其他客户端的锁了

 SET lock_key unique_value NX PX 10000
 
 $rs = $redis->set($key, $random, array('nx', 'ex' => $ttl));
if ($rs) {
     //处理更新缓存逻辑
    // ......
    //先判断随机数,是同一个则删除锁
    if ($redis->get($key) == $random) {
    	//锁过期了  其他客户端拿到锁了 那就是把其他客户端的锁释放了
        $redis->del($key);
    }
}

正确解锁方案

 public static boolean releaseDistributedLock(Jedis jedis, 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 (RELEASE_SUCCESS.equals(result)) {
                return true;
            }
            return false;
 }

解锁只需要两行代码就搞定了!第一行代码,我们写了一个简单的Lua脚本代码,第二行代码,我们将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。

eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。

Lua用法简述
目标:作为嵌入式程序移植到其他应用程序,由c语言实现,许多应用选用做脚本语言。(魔兽世界、愤怒的小鸟、ngnix)
Redis将Lua作为脚本语言帮助开发者定制自己的redis命令。
Lua
1、数据类型及逻辑处理
blleans
numbers
strings
tables
数据类型

2、Lua的Redis API
Lua可以使用redis.call函数实现对redis的访问(redis.call(“set”, “hello”, “world”))
redis.pcall也可以实现对redis的访问,与上面不同的是,遇见错误会继续执行
案例
Lua脚本功能为Redis带来的好处:
1、Lua脚本在redis中是原子执行的
2、lua脚本可以帮助开发和运维人员创造出自己定制的命令,并将这些命令常驻在redis内存中,实现复用;(将原来需要多步执行的命令放在一个lua脚本中,一起执行)
3、lua脚本可以将多条命令一次性打包,有效减少网络开销;
Redis如何管理Lua脚本
Redis提供了4个命令实现对Lua脚本的管理:
管理LUA

脚本相关命令
EVAL语法: eval script numkeys key [key …] arg [arg …]

​ 通过key和arg这两类参数向脚本传递数据,它们的值在脚本中分别使用KEYS和ARGV两个表类型的全局变量访问。

script: 是lua脚本

numkeys:表示有几个key,分别是KEYS[1],KEYS[2]…,如果有值,从第numkeys+1个开始就是参数值,ARGV[1],ARGV[2]…

注意: EVAL命令依据参数numkeys来将其后面的所有参数分别存入脚本中KEYS和ARGV两个table类型的全局变量。当脚本不需要任何参数时,也不能省略这个参数(设为0)

       192.168.127.128:6379>eval "return redis.call('set',KEYS[1],ARGV[1])" 1 name liulei
       OK

       192.168.127.128:6379>get name
       "liulei"

image-20200817194146076

evalsha命令
在脚本比较长的情况下,如果每次调用脚本都需要将整个脚本传给Redis会占用较多的带宽。为了解决这个问题,Redis提供了EVALSHA命令,允许开发者通过脚本内容的SHA1摘要来执行脚本,该命令的用法和EVAL一样,只不过是将脚本内容替换成脚本内容的SHA1摘要。

​ Redis在执行EVAL命令时会计算脚本的SHA1摘要并记录在脚本缓存中,执行EVALSHA命令时Redis会根据提供的摘要从脚本缓存中查找对应的脚本内容,如果找到了则执行脚本,否则会返回错误:“NOSCRIPT No matching script. Please use EVAL.”

在程序中使用EVALSHA命令的一般流程如下。

1)、先计算脚本的SHA1摘要,并使用EVALSHA命令执行脚本。

2)、获得返回值,如果返回“NOSCRIPT”错误则使用EVAL命令重新执行脚本。

​ 虽然这一流程略显麻烦,但值得庆幸的是很多编程语言的Redis客户端都会代替开发者完成这一流程。执行EVAL命令时,先尝试执行EVALSHA命令,如果失败了才会执行EVAL命令。

持久化

redis是一个内存数据库,数据保存在内存中,但是我们都知道内存的数据变化是很快的,也容易发生丢失。幸好Redis还为我们提供了持久化的机制,分别是RDB(Redis DataBase)和AOF(Append Only File)。

RDB机制

RDB其实就是把数据以快照的形式保存在磁盘上。什么是快照呢,你可以理解成把当前时刻的数据拍成一张照片保存下来。

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

既然RDB机制是通过把某个时刻的所有数据生成一个快照来保存,那么就应该有一种触发机制,是实现这个过程。对于RDB来说,提供了三种机制:save、bgsave、自动化。我们分别来看一下

1、save触发方式

该命令会阻塞当前Redis服务器,执行save命令期间,Redis不能处理其他命令,直到RDB过程完成为止。具体流程如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f8yoNUcL-1608776776012)(https://pics1.baidu.com/feed/e7cd7b899e510fb3aa8c05042b22c093d0430ca7.jpeg?token=7ed4cf784a82d04e60b8dc72cf7e3c24&s=EDBAA5565D1859C85444707E02005071)]

执行完成时候如果存在老的RDB文件,就把新的替代掉旧的。我们的客户端可能都是几万或者是几十万,这种方式显然不可取。

2、bgsave触发方式

执行该命令时,Redis会在后台异步进行快照操作,快照同时还可以响应客户端请求。具体流程如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uI9crBfx-1608776776013)(https://pics5.baidu.com/feed/023b5bb5c9ea15cefb035bc8431132f53b87b21e.jpeg?token=a72f072d65d2de548d71bb459cd0bf4f&s=05AAFE168FF04C8A10FD2DEE0300E032)]

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

3、自动触发

自动触发是由我们的配置文件来完成的。在redis.conf配置文件中,里面有如下配置,我们可以去设置:

①save:这里是用来配置触发 Redis的 RDB 持久化条件,也就是什么时候将内存中的数据保存到硬盘。比如“save m n”。表示m秒内数据集存在n次修改时,自动触发bgsave。

默认如下配置:

#表示900 秒内如果至少有 1 个 key 的值变化,则保存save 900 1

#表示300 秒内如果至少有 10 个 key 的值变化,则保存save 300 10

#表示60 秒内如果至少有 10000 个 key 的值变化,则保存save 60 10000

不需要持久化,那么你可以注释掉所有的 save 行来停用保存功能。

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

**③rdbcompression ;**默认值是yes。对于存储到磁盘中的快照,可以设置是否进行压缩存储。

**④rdbchecksum :**默认值是yes。在存储快照后,我们还可以让redis使用CRC64算法来进行数据校验,但是这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能。

**⑤dbfilename :**设置快照的文件名,默认是 dump.rdb

**⑥dir:**设置快照文件的存放路径,这个配置项一定是个目录,而不能是文件名。

我们可以修改这些配置来实现我们想要的效果。因为第三种方式是配置的,所以我们对前两种进行一个对比:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qGhIjcY5-1608776776014)(https://pics5.baidu.com/feed/1c950a7b02087bf43b4490d50ac25f2a11dfcf7e.jpeg?token=22f387ba78130c6115420059481b2393&s=EF48A15796784D8816E1D9EB03007024)]

4、RDB 的优势和劣势

①、优势

(1)RDB文件紧凑,全量备份,非常适合用于进行备份和灾难恢复。

(2)生成RDB文件的时候,redis主进程会fork()一个子进程来处理所有保存工作,主进程不需要进行任何磁盘IO操作。

(3)RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。

②、劣势

RDB快照是一次全量备份,存储的是内存数据的二进制序列化形式,存储上非常紧凑。当进行快照持久化时,会开启一个子进程专门负责快照持久化,子进程会拥有父进程的内存数据,父进程修改内存子进程不会反应出来,所以在快照持久化期间修改的数据不会被保存,可能丢失数据。

AOF机制

全量备份总是耗时的,有时候我们提供一种更加高效的方式AOF,工作机制很简单,redis会将每一个收到的写命令都通过write函数追加到文件中。通俗的理解就是日志记录。

1、持久化原理

他的原理看下面这张图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CUiniGqH-1608776776016)(https://pics3.baidu.com/feed/32fa828ba61ea8d3c2502e396b1b3848251f58b0.jpeg?token=394597ccd73bd15778c518b5c5be6998&s=2D62E7169D305F8A847546E20200B036)]

每当有一个写命令过来时,就直接保存在我们的AOF文件中。

2、文件重写原理

AOF的方式也同时带来了另一个问题。持久化文件会变的越来越大。为了压缩aof的持久化文件。redis提供了bgrewriteaof命令。将内存中的数据以命令的方式保存到临时文件中,同时会fork出一条新进程来将文件重写。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vJsvZ9Zl-1608776776016)(https://pics7.baidu.com/feed/09fa513d269759ee28454d2c4cea4b106c22dfd3.jpeg?token=86eda46b8bcd54a7a0e7d8a37d87bee8&s=EDB2A4579D317B824660D4DF0200E036)]

重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,这点和快照有点类似。

3、AOF也有三种触发机制

(1)每修改同步always:同步持久化 每次发生数据变更会被立即记录到磁盘 性能较差但数据完整性比较好

(2)每秒同步everysec:异步操作,每秒记录 如果一秒内宕机,有数据丢失(一般是这个

(3)不同no:从不同步

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NQm5Ha0B-1608776776017)(https://pics5.baidu.com/feed/b17eca8065380cd7df69859ba056a5325982816c.jpeg?token=a060f459d81c409c3d6c7208d2118888&s=AF4AA5574ED85CC841D04BE60300A036)]

4、优点

(1)AOF可以更好的保护数据不丢失,一般AOF会每隔1秒,通过一个后台线程执行一次fsync操作,最多丢失1秒钟的数据。(2)AOF日志文件没有任何磁盘寻址的开销,写入性能非常高,文件不容易破损。

(3)AOF日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写。

(4)AOF日志文件的命令通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复。比如某人不小心用flushall命令清空了所有数据,只要这个时候后台rewrite还没有发生,那么就可以立即拷贝AOF文件,将最后一条flushall命令给删了,然后再将该AOF文件放回去,就可以通过恢复机制,自动恢复所有数据

5、缺点

(1)对于同一份数据来说,AOF日志文件通常比RDB数据快照文件更大

(2)AOF开启后,支持的写QPS会比RDB支持的写QPS低,因为AOF一般会配置成每秒fsync一次日志文件,当然,每秒一次fsync,性能也还是很高的

(3)以前AOF发生过bug,就是通过AOF记录的日志,进行数据恢复的时候,没有恢复一模一样的数据出来。

四、RDB和AOF到底该如何选择

选择的话,两者加一起才更好。因为两个持久化机制你明白了,剩下的就是看自己的需求了,需求不同选择的也不一定,但是通常都是结合使用。有一张图可供总结:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1WbN9FPc-1608776776018)(https://pics5.baidu.com/feed/8326cffc1e178a82c532308ef2117b8ba977e8ae.jpeg?token=fea28817e45f0e091b5be3854d856fbb&s=BD48B55F1C784C095E61DCEB0300D036)]

混合持久化

重启redis恢复数据集时,很少会使用rdb来恢复内存状态,因为会丢失大量数据。通常会使用aof日志恢复数据,但是重放aof日志性能相对rdb来说要慢很多,这样在redis实例很大的情况下,启动需要花费很长时间。Redis4.0为了解决这个问题,带来了新的持久化选项——混合持久化。

aof-use-rdb-preamble yes

混合持久化aof文件结构:

img

如果开启了混合持久化,aof在重写时,不再是单纯将内存数据转换为RESP命令写入aof文件,而是将重写这一刻之前的内存做rdb快照处理,并且将rdb快照内容和增量的aof修改内存数据的命令存在一起,都写入新的aof文件新的aof文件一开始不叫appendonly.aof,等到重写完成后,新的aof文件才会进行改名,原子的覆盖原有的aof文件,完成新旧两个aof文件的替换。
于是在redis重启的时候,可以先加载rdb文件,然后再重放增量的aof日志就可以完全替代之前的aof全量文件重放,因此重启效率大幅得到提高。

主从复制

在分布式系统中为了解决单点问题,通常会把数据复制到个副本部署到其他机器,满足故障恢复和负载均衡等需求。
Redis为我们提供了复制功能,实现了相同数据的多个Redis副本。

每个节点只能有一个主节点,而主节点可以有多个从节点。
数据流是单向的,从主节点流向从节点。

主从复制 读写分离 主机写 从机读

主从复制的作用:

1.数据冗余:实现了数据的热备份,是持久化之外的一种数据冗余的方式

2.故障恢复:当主节点出现问题时,可以从节点提供服务,实现快速的故障恢复,

3.负载均衡:主从复制的基础上实现读写分离,写Redis数据时连接主机,读redis服务时连接从机,大大提高redis的并发量

4.高可用基石:是哨兵模式和集群能够实施的基础

配置

info replication 查看当前库的信息 可以看到角色是master(主机) 连接的从机为0 slave为从机

image-20200818135448381

建立集群环境

在linux环境下 复制多个配置文件

然后要修改端口号、pid、log名称、rdb文件名称

然后分别以不同的配置文件启动服务来模拟集群环境。

一主二从(在一开始时每个都是主机)

#在要设置为从机的客户端编写命令
slaveof ip 端口号
slaveof 127.0.0.1 6379

这样的话 端口为6380即为6379的从机

可以通过 info replication来查看信息

(这样的配置只是暂时的,如果要永久的可以在配置文件中配置)

一旦建立主从关系后,主机可以写可以读 但是从机只能读,并且主机中的所有信息和数据会自动保存在从机中

slave本身是异步命令,后续同步流程在节点内部异步执行。

断开复制
断开复制状态:执行slaveof no one

断开复制会执行2个操作:(手动操作使从机升为主机,但是假如一开始的主机是宕机了,通过手动设置主机,那么原有的主机恢复了 就不是主机了)
1、断开与主节点复制关系
2、从节点晋升为主节点

哨兵模式(当主机宕机或者故障后,会自动选中一个从机升级为主机)

哨兵是一个独立的进程,通过发送命令等待redis服务器的响应来监控运行多个redis实例

哨兵也可能死了(所以哨兵最好也配个集群)

首先编写配置文件

#sentinel.conf
sentinel monitor myredis 127.0.0.1 6379 1
# myredis 为被监控的名称 随意 然后是host port
# 最后的1表示最低有多个个哨兵认为主机挂掉了就去选举新的主机  这里就是1

然后启动哨兵

redis-sentinel sentinel.conf

然后模拟主机宕机 把主机停掉后 过一会就会通过(投票算法)选举一个从机作为新的主机,当旧的主机回来后也只能是作为新主机的从机

传输延迟
repl-disable-tcp-nodelay参数:控制是否关闭TCP_NODELAY,默认为关闭。
控制是否合并TCP数据包,来节省带宽。
1、关闭:任何tcp包都会立即发送——delay小,但是占用带宽。
2、开启:合并小的tcp数据包——节省带宽,delay变大。

原理

复制的过程
1、保存主节点信息;从节点只保存主节点的地址信息变直接返回(复制流程还没有开始)
2、主从建立socket连接:无限重试到链接成功或者执行slaveofnoone取消复制;
3、发送ping命令(1、检测主从指尖socke是否可用;2、检测主节点当前是否接受处理命令)
4、权限验证
5、同步数据集(全量同步和部分同步)
6、命令持续复制:主节点持续把写命令发送给从节点

数据同步
Redis2.8以上使用psync完成主从数据同步,分为全量复制和部分复制。
全量复制:用于初次复制场景
部分复制:用于处理在主从复制中因网络闪断等原因造成的数据丢失场景。

复制偏移量
主和从都会维护一份复制偏移量(已经写入命令的长度和)。每秒钟,从节点会上报自身的复制偏移量给主,所以主不仅保存自己的也保存从节点的偏移量
通过对比可以知道主从节点数据是否一致。

复制积压缓冲区
主节点上的一个固定长度队列,默认为1MB。用于保存最近写入从的命令,用于数据的重传

主节点运行ID:
唯一识别Redis节点
psync命令
格式:psync {runId} {offset}
命令运行流程:
1、从发送psync给主
2、主根据psync参数和自身数据情况决定响应结果:
+FULLRESYNC:全量复制
+CONTINUE:部分复制
+ERR:主节点版本低于2.8,进行全量复制

全量复制
全量复制是Redis最早支持的复制方式,也是主从第一次建立复制时必须经历的阶段。
流程
1、发送psync:psync ? -1
2、回复+FULLRESYNC
3、从节点接收主节点的响应数据保存运行ID和偏移量offset
4、主节点执行bgsave保存RDB文件到本地
5、主节点发送RDB文件给从节点(传送有可能失败:时间大于repl-timeout,所以需要合理配置)
6、在从接收RDB期间,主可以继续接收写命令,这部分写命令会写入复制客户端缓冲区内,当从加载完RDB后,主再把缓冲区的命令发送过去(缓冲区有可能溢出,需要合理配置)
7、从节点接收完主节点传过来的全部数据后清空自身旧数据。
8、从节点清空数据后开始加载RDB文件(读写分离情况下,有可能脏读,那么就要关闭slave-server-stale-data参数)
9、从节点加载完RDB后,如果开启了AOF持久化,那么会立刻做bgrewriteof操作。
部分复制
是对全量复制做出的一种优化措施。
1、主从节点间网络出现中断时,如果超过repl-timeout时间,主会认为从故障并中断复制
2、中断期间,主的命令写入复制缓冲区;
3、从连上主
4、执行psync操作,带上响应的参数
5、如果复制缓冲区中存在偏移量的数据,那么表示可以复制,否则进入全量复制
6、进行复制步骤

心跳
主从建立复制后,他们维护着一个长连接并彼此发送心跳命令。
每10秒ping一下从机
从机每1秒发送replconf ack {offset}给主机

异步复制
在持续写入阶段,命令的发送是异步的。

阻塞

发现阻塞

发现Redis异常有2种方式:
1、加日志,对异常进行统计、计算并做报警;
2、用现成的监控工具

内在原因

定位到具体的Redis节点异常后,先排查是否是Redis自身的原因导致的。
主要围绕以下三个方面进行排查:
1、API或数据结构使用不合理;
2、CPU饱和的问题;
3、持久化相关的阻塞;
API或数据结构使用不合理
1、发现慢查询:
用slowlog get {n}查询最近n条慢查询命令,针对这些命令做优化
1)修改为低算法度的命令
2)调整大对象
2、如何发现大对象:
利用Redis自带的大对象的工具 命令:redis-cli -h {ip} -p {port}
CPU饱和
单线程的Redis处理命令时只能使用一个cpu。
发现qps不是特别高但是cpu却饱和了,那么有一种情况是对内存做了过度的优化导致cpu复杂度变高。
持久化阻塞
持久化引起主线程阻塞的操作主要有:fork操作、AOF刷盘阻塞、HUgepage写操作阻塞。

内存

学习如何高效利用内存
从以下三个方面进行学习:
1、内存消耗分析
2、管理内存的原理与方法
3、内存优化技巧

内存消耗

内存使用的统计
我们可以使用info memory来观测相关指标
重点需要关注的是used_memory_res和used_memory以及他们的比值mem_framentation_ratio
mem_framentation_ratio>1 表示 有内存碎片,如果比值太大说明碎片太多(多出的部分并没有被用来存储数据)
mem_framentation_ratio<1 表示 内存被置换到了硬盘

redis进程内消耗主要包括:
自身内存:太小,可以忽略不计
对象内存:占用最多的一块儿,要避免使用太长的key
缓冲内存:包括客户端缓冲、复制积压缓冲、AOF缓冲区
内存碎片:redis默认的内存分配器使用jemalloc。
容易造成内存碎片的原因有:频繁的更新操作、大量过期键删除。
出现内存碎片时可以采取的手段是——数据对齐和安全重启。

子进程内存消耗
子进程是指RDB/AOF对数据做持久化。
写时复制技术:子进程和父进程共享内存页,当父进程需要写的时候将内存也取出copy一份
THP:会增加写时复制技术使用的内存量
1、redis产生的子进程并不需要消耗1倍的父进程内存,实际消耗根据期间写入命令量决定,依然要预留一些内存防止溢出
2、设置sysctl vm.overcommit-memory=1允许内核可以分配所有的物理内存,防止redis进程fork时因剩余内存不足失败
3.关闭THP

内存管理

主要通过2个手段实现内存的管理
1、设置内存上限
2、回收策略

设置内存上限
redis使用maxmemory参数限制最大可用内存,主要目的是:
1、超出内存上限时,使用LRU释放空间
2、防止所用内存超过服务器物理内存
动态调整内存上限
可以使用config set memory对内存上限进行动态修改

内存回收策略
主要包括
1、删除过期键对象:
删除过期对象的策略:如果redis一直监控着键是否过期消耗太大,所以redis使用以下两种策略相结合的方式来对过期的键值进行删除
1)惰性删除:当客户端读取带有超时属性的键时,如果已经超过键设置的过期时间,会执行删除操作并返回空;
2)定时任务删除:redis内部维护一个定时任务,默认每秒运行10次(可配);定时任务中删除过期键逻辑采用了自适应算法每次检查20个key,如果过期超过25%那么继续执行,直到25%以下),根据键的过期比例,使用快慢两种速率模式(超时时间不一样快1毫秒 慢25毫秒)回收键。

2、内存使用达到maxmemory上限时触发内存溢出控制策略
当redis所用内存达到maxmemory上限时会触发响应的溢出控制策略。具体策略受maxmemory-policy参数控制。
Redis支持6种策略。通过config set maxmemory-policy设置。

noeviction:默认,不删除任何数据,拒绝所有写入操作并返回错误。
volatile-lru:根据lru删除设置了超时属性的键,直到腾出足够空间,如果没有可删的,使用noeviction。
allkeys-lru:根据lru不管数据有没有设置超时属性,直到有足够空间。
allkeys-random:随机删除所有键,直到腾出足够空间。
volatile-random:随机删除过期键,直到腾出足够空间。
volatile-ttl:根据键值对象的ttl属性,删除最近将要过期数据,如果没有会执行noeviction。

4.0 版本后增加以下两种:

  1. volatile-lfu:从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
  2. allkeys-lfu:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key

内存优化技巧

1、关闭vm选项,即vm-enabled 为 no;
2、Redis Hash是value内部为一个HashMap,如果该Map的成员数比较少,则会采用类似一维线性的紧凑格式来存储该Map, 即省去了大量指针的内存开销,配置如下:
hash-max-zipmap-entries 1024 成员数量大于将会采用hashmap形式 解决方法是分段
hash-max-zipmap-value 512 :key的字节大于512字节将会转成hashmap 解决方法是md5加密
HashMap的优势就是查找和操作的时间复杂度都是O(1)的,而放弃Hash采用一维存储则是O(n)的时间复杂度
list-max-ziplist-entries 512
list数据类型多少节点以下会采用去指针的紧凑存储格式。
list-max-ziplist-value 64
list数据类型节点值大小小于多少字节会采用紧凑存储格式。
set-max-intset-entries 512
set数据类型内部数据如果全部是数值型,且包含多少节点以下会采用紧凑格式存储。

3、Redis内部实现没有对内存分配方面做过多的优化,在一定程度上会存在内存碎片,不过大多数情况下这个不会成为Redis的性能瓶颈,不过如果在Redis内部存储的大部分数据是数值型的话,Redis内部采用了一个shared integer的方式来省去分配内存的开销,即在系统启动时先分配一个从1~n 那么多个数值对象放在一个池子中,如果存储的数据恰好是这个数值范围内的数据,则直接从池子里取出该对象,并且通过引用计数的方式来共享,这样在系统存储了大量数值下,也能一定程度上节省内存并且提高性能,这个参数值n的设置需要修改源代码中的一行宏定义REDIS_SHARED_INTEGERS,该值默认是10000,可以根据自己的需要进行修改,修改后重新编译就可以了。

缓存穿透和雪崩

缓存穿透

什么是缓存穿透?

缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上(穿透redis到了数据库),根本没有经过缓存这一层。举个例子:某个黑客故意制造我们缓存中不存在的 key 发起大量请求,导致大量请求落到数据库。

缓存穿透情况的处理流程是怎样的?

用户的请求最终都要跑到数据库中查询一遍。

缓存穿透情况

有哪些解决办法?

最基本的就是首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。比如查询的数据库 id 不能小于 0、传入的邮箱格式不对的时候直接返回错误消息给客户端等等。

1)缓存无效 key

如果缓存和数据库都查不到某个 key 的数据就写一个到 Redis 中去并设置过期时间,具体命令如下: SET key value EX 10086 。这种方式可以解决请求的 key 变化不频繁的情况,如果黑客恶意攻击,每次构建不同的请求 key,会导致 Redis 中缓存大量无效的 key 。很明显,这种方案并不能从根本上解决此问题。如果非要用这种方式来解决穿透问题的话,尽量将无效的 key 的过期时间设置短一点比如 1 分钟。

另外,这里多说一嘴,一般情况下我们是这样设计 key 的: 表名:列名:主键名:主键值

如果用 Java 代码展示的话,差不多是下面这样的:

public Object getObjectInclNullById(Integer id) {
    // 从缓存中获取数据
    Object cacheValue = cache.get(id);
    // 缓存为空
    if (cacheValue == null) {
        // 从数据库中获取
        Object storageValue = storage.get(key);
        // 缓存空对象
        cache.set(key, storageValue);
        // 如果存储数据为空,需要设置一个过期时间(300秒)
        if (storageValue == null) {
            // 必须设置过期时间,否则有被攻击的风险
            cache.expire(key, 60 * 5);
        }
        return storageValue;
    }
    return cacheValue;
}

2)布隆过滤器

布隆过滤器是一个非常神奇的数据结构,通过它我们可以非常方便地判断一个给定数据是否存在于海量数据中。我们需要的就是判断 key 是否合法,有没有感觉布隆过滤器就是我们想要找的那个“人”。

具体是这样做的:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。

加入布隆过滤器之后的缓存处理流程图如下。

image

但是,需要注意的是布隆过滤器可能会存在误判的情况。总结来说就是: 布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。

为什么会出现误判的情况呢? 我们还要从布隆过滤器的原理来说!

我们先来看一下,当一个元素加入布隆过滤器中的时候,会进行哪些操作:

  1. 使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值(有几个哈希函数得到几个哈希值)。
  2. 根据得到的哈希值,在位数组中把对应下标的值置为 1。

我们再来看一下,当我们需要判断一个元素是否存在于布隆过滤器的时候,会进行哪些操作:

  1. 对给定元素再次进行相同的哈希计算;
  2. 得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。

然后,一定会出现这样一种情况:不同的字符串可能哈希出来的位置相同。 (可以适当增加位数组大小或者调整我们的哈希函数来降低概率)

更多关于布隆过滤器的内容可以看我的这篇原创:《不了解布隆过滤器?一文给你整的明明白白!》 ,强烈推荐,个人感觉网上应该找不到总结的这么明明白白的文章了。

缓存雪崩

什么是缓存雪崩?

我发现缓存雪崩这名字起的有点意思,哈哈。

实际上,缓存雪崩描述的就是这样一个简单的场景:缓存在同一时间大面积的失效,后面的请求都直接落到了数据库上,造成数据库短时间内承受大量请求。 这就好比雪崩一样,摧枯拉朽之势,数据库的压力可想而知,可能直接就被这么多请求弄宕机了。

举个例子:系统的缓存模块出了问题比如宕机导致不可用。造成系统的所有访问,都要走数据库。

还有一种缓存雪崩的场景是:有一些被大量访问数据(热点缓存)在某一时刻大面积失效,导致对应的请求直接落到了数据库上。 这样的情况,有下面几种解决办法:

举个例子 :秒杀开始 12 个小时之前,我们统一存放了一批商品到 Redis 中,设置的缓存过期时间也是 12 个小时,那么秒杀开始的时候,这些秒杀的商品的访问直接就失效了。导致的情况就是,相应的请求直接就落到了数据库上,就像雪崩一样可怕。

有哪些解决办法?

针对 Redis 服务不可用的情况:

  1. 采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用
  2. 限流,避免同时处理大量的请求。

针对热点缓存失效的情况:

  1. 设置不同的失效时间比如随机设置缓存的失效时间。
  2. 缓存永不失效。

如何保证缓存和数据库数据的一致性?

细说的话可以扯很多,但是我觉得其实没太大必要(小声BB:很多解决方案我也没太弄明白)。我个人觉得引入缓存之后,如果为了短时间的不一致性问题,选择让系统设计变得更加复杂的话,完全没必要。

下面单独对 Cache Aside Pattern(旁路缓存模式) 来聊聊。

Cache Aside Pattern 中遇到写请求是这样的:更新 DB,然后直接删除 cache 。

如果更新数据库成功,而删除缓存这一步失败的情况的话,简单说两个解决方案:

  1. 缓存失效时间变短(不推荐,治标不治本) :我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据。另外,这种解决办法对于先操作缓存后操作数据库的场景不适用。
  2. 增加cache更新重试机制(常用): 如果 cache 服务当前不可用导致缓存删除失败的话,我们就隔一段时间进行重试,重试次数可以自己定。如果多次重试还是失败的话,我们可以把当前更新失败的 key 存入队列中,等缓存服务可用之后,再将 缓存中对应的 key 删除即可。

常用命令

http://redisdoc.com/ 指令查询地址

在安装了Redis的目录下 打开cmd 输入 redis-cli启动

然后如果有密码的话 输入 auth password来启动

image-20200815164558885

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

keys *查看所有的数据

image-20200815165532572

exists key[key…] :查看是否存在

image-20200815172017009

del key[key…]:删除对应的键

img

1.string

set : 存储一个string 格式:set key value

get:获取一个 格式 get key

image-20200815164731236

设置一个值的过期时间:set key value ex 时间值(秒数)

image-20200815164823630

SET key value PX milliseconds (时间为毫秒)

set key value nx (当键不存在是才设置 存在不设置)

都要带上nx

image-20200815165141522

set key value xx (当键存在时修改值 不存在则不创建)

image-20200815165317540

getset key value :将键 key 的值设为 value , 并返回键 key 在被设置之前的旧值。(如果key不存在返回nil,并且创建该值)

image-20200815165921015

strlen key:返回键存储的值的长度

image-20200815170007054

append key vlaue: 将值追加到已存在的键的值的后面,如果key不存在 则创建并且以value作为值

image-20200815170248440

setrange key offest value: 从offest开始 用value重写 key的值 下标从0开始

image-20200815170513611

getrange key start end: 返回key对应的值的start-end部分字符 下标也是从0开始

image-20200815170655492

incr key:为key存储的数字+1

image-20200815170733848

incrby key increment:增加指定的值 如果键 key 不存在, 那么键 key 的值会先被初始化为 0 , 然后再执行 INCRBY 命令。

image-20200815170935630

与相反的就是减一或者减指定的值

decr key

decrby key decrement

MSET key value [key value …]:同时为多个键设置值,如果某个键存在,则用新的值去覆盖

MSET 是一个原子性(atomic)操作, 所有给定键都会在同一时间内被设置, 不会出现某些键被设置了但是另一些键没有被设置的情况。

mget key [key…]:获取多个键的值 某个值不存在则用nil表示

image-20200815171350293

MSETNX : 这个命令只会在所有给定键都不存在的情况下进行设置(即使只有一个给定键已经存在, MSETNX 命令也会拒绝执行对所有键的设置操作。)

image-20200815171514870

2.hash

Redis中一个哈希存储一条数据,一个字段field则存储一条数据中的一个属性,字段值value是属性对应的值。每个哈希hash可存储2^32-1个键值对,约40多亿个。Redis中的哈希散列类型与Java中的HashMap相似,都是一组键值对的集合,并且支持单独对其中一个键进行增删改查操作。

所以hash适合存储对象

例如hmset user:123 name zyf age 21这样的形式(hmset 批量设置)

则user:123表示hash表 name age都是域

123其实就相当于我们数据库表中的id

hset hash field value: 将哈希表 hash 中域 field 的值设置为 value

如果域 field 已经存在于哈希表中, 那么它的旧值将被新值 value 覆盖。

image-20200815173150281

hsetnx hash field value

当且仅当域 field 尚未存在于哈希表的情况下, 将它的值设置为 value

如果给定域已经存在于哈希表当中, 那么命令将放弃执行设置操作。

如果哈希表 hash 不存在, 那么一个新的哈希表将被创建并执行 HSETNX 命令。

image-20200815173414785

hget hash field:返回哈希表中给定域的值。

image-20200815173229645

hexists hash field :判断给定哈希表中的域是否存在

image-20200817162842210

hdel hash field [field…]:删除域 可以删除多个

image-20200817163032186

hlen hash :返回指定哈希表中域的个数

image-20200817163115569

hstrlen hash field:返回字段的值的长度

image-20200817163253379

hincrby hash field increment:为域的值增加指定的增量

image-20200817163449455

hincrbyfloat hash field increment:增加浮点数的增量

image-20200817163656211

hmset hash field value [field value …]:添加多个域和值

hmget hash filed [field…]:获取多个域的值

hkeys hash :返回哈希表中的所有的域

hvals hash:返回哈希表中的所有的值

image-20200817163946086

hgetall hash:返回哈希表中所有的域和值

image-20200817164024208

3.list

lpush key value[value…]:将一个值或者多个值插入列表的表头(左边插入)

注意 这里插入zyf、21 在列表中是 21-》zyf 从表头插入

image-20200817164252233

lpushx key value :插入列表的表头 当且仅当key存在并且是一个列表

rpush key value[value…]:在表尾插入 (右边插入)

image-20200817164448626

rpushx key value:插入列表的表尾 当且仅当key存在并且是一个列表

lpop key :移除并返回列表的表头元素

image-20200817164549761

rpop key:移除表尾并返回表尾元素

image-20200817164612661

rpoplpush source destination:将source列表表尾的元素移除,并返回,然后将这个元素添加到destination列表的表头

image-20200817164711371

lrem key count value:

根据参数 count 的值,移除列表中与参数 value 相等的元素。

count 的值可以是以下几种:

  • count > 0 : 从表头开始向表尾搜索,移除与 value 相等的元素,数量为 count
  • count < 0 : 从表尾开始向表头搜索,移除与 value 相等的元素,数量为 count 的绝对值。
  • count = 0 : 移除表中所有与 value 相等的值。

llen 返回列表的长度

image-20200817164943830

lindex key index :返回列表下标为index的元素(下标从0开始)

image-20200817165028423

linsert key before|after pivot value:将value插入到pivot的前面或者后面

image-20200817165157510

lset key index value:将列表下标为index的值设置为新的value

如测试 如果列表这个下标没有值 会返回错误

image-20200817165305427

lrange key start end:列表 key 中指定区间内的元素,区间以偏移量 startstop 指定。

image-20200817165437749

ltrim key start stop:保留指定区间的值

image-20200817165640824

blpop key [key…] timeout:它是 LPOP key 命令的阻塞版本,当给定列表内没有任何元素可供弹出的时候,连接将被 BLPOP 命令阻塞,直到等待超时或发现可弹出元素为止。(timeout以秒为单位)

当给定多个 key 参数时,按参数 key 的先后顺序依次检查各个列表,弹出第一个非空列表的头元素。

一开始list列表没有元素 然后启动另外一个客户端插入元素 然后才会弹出 限时300s

image-20200817170238915

image-20200817170249388

给定多个key 如果不想等待超时就设置为0 第一个返回的是具有表头元素的列表key 然后是表头元素

image-20200817170449293

brpop key[key…] timeout:与上面类似 这个从表尾弹出 上面是表头

brpoplpush source destinastion timeout:是rpoplpush的阻塞版本

Redis延时双删

一、只先删缓存

问题:先删缓存,在改库前,其他事务又把旧数据放到缓存里去了。

在这里插入图片描述

二、只后删缓存

问题:改了库,清理缓存前,有部分事务还是会拿到旧缓存

在这里插入图片描述

三、普通双删

问题:第一次清空缓存后、更新数据库前:其他事务查询了数据库
第二次清空缓存后:其他事务更新缓存,此时又会把旧数据更新到缓存

在这里插入图片描述

四、为什么需要延时双删?

在三中,第二次清空缓存之前,多延时一会儿,等B更新缓存结束了,再删除缓存,这样就缓存就不存在了,其他事务查询到的为新缓存。

延时是确保 修改数据库 -> 清空缓存前,其他事务的更改缓存操作已经执行完。
在这里插入图片描述

五、以上策略还能不能完善

四中说到,采用延时删最后一次缓存,但这其中难免还是会大量的查询到旧缓存数据的。
在这里插入图片描述
这时候可以通过加锁来解决,一次性不让太多的线程都来请求,另外从图上看,我们可以尽量缩短第一次删除缓存更新数据库的时间差,这样可以使得其他事务第一时间获取到更新数据库后的数据。

Redis为什么这么快?

1.基于内存实现

Redis 是基于内存的数据库,那不可避免的就要与磁盘数据库做对比。对于磁盘数据库来说,是需要将数据读取到内存里的,这个过程会受到磁盘 I/O 的限制。

2.高效的数据结构

Redis 中有多种数据类型,每种数据类型的底层都由一种或多种数据结构来支持。正是因为有了这些数据结构,Redis 在存储与读取上的速度才不受阻碍。这些数据结构有什么特别的地方,各位看官接着往下看:

**
**

Image

2.1、简单动态字符串

这个名词可能你不熟悉,换成 SDS 肯定就知道了。这是用来处理字符串的。了解 C 语言的都知道,它是有处理字符串方法的。而 Redis 就是 C 语言实现的,那为什么还要重复造轮子?我们从以下几点来看:

(1)字符串长度处理

Image

这个图是字符串在 C 语言中的存储方式,想要获取 「Redis」的长度,需要从头开始遍历,直到遇到 ‘\0’ 为止。

Image

Redis 中怎么操作呢?用一个 len 字段记录当前字符串的长度。想要获取长度只需要获取 len 字段即可。你看,差距不言自明。前者遍历的时间复杂度为 O(n),Redis 中 O(1) 就能拿到,速度明显提升。

(2)内存重新分配

C 语言中涉及到修改字符串的时候会重新分配内存。修改地越频繁,内存分配也就越频繁。而内存分配是会消耗性能的,那么性能下降在所难免。

而 Redis 中会涉及到字符串频繁的修改操作,这种内存分配方式显然就不适合了。于是 SDS 实现了两种优化策略:

  • 空间预分配

对 SDS 修改及空间扩充时,除了分配所必须的空间外,还会额外分配未使用的空间。

具体分配规则是这样的:SDS 修改后,len 长度小于 1M,那么将会额外分配与 len 相同长度的未使用空间。如果修改后长度大于 1M,那么将分配1M的使用空间。

  • 惰性空间释放

当然,有空间分配对应的就有空间释放。

SDS 缩短时,并不会回收多余的内存空间,而是使用 free 字段将多出来的空间记录下来。如果后续有变更操作,直接使用 free 中记录的空间,减少了内存的分配。

(3)二进制安全

你已经知道了 Redis 可以存储各种数据类型,那么二进制数据肯定也不例外。但二进制数据并不是规则的字符串格式,可能会包含一些特殊的字符,比如 ‘\0’ 等。

前面们提到过,C 中字符串遇到 ‘\0’ 会结束,那 ‘\0’ 之后的数据就读取不上了。但在 SDS 中,是根据 len 长度来判断字符串结束的。

看,二进制安全的问题就解决了。

2.2、双端链表

列表 List 更多是被当作队列或栈来使用的。队列和栈的特性一个先进先出,一个先进后出。双端链表很好的支持了这些特性。

Image

- 双端链表 -

(1)前后节点

Image

链表里每个节点都带有两个指针,prev 指向前节点,next 指向后节点。这样在时间复杂度为 O(1) 内就能获取到前后节点。

(2)头尾节点

Image

你可能注意到了,头节点里有 head 和 tail 两个参数,分别指向头节点和尾节点。这样的设计能够对双端节点的处理时间复杂度降至 O(1) ,对于队列和栈来说再适合不过。同时链表迭代时从两端都可以进行。

(3)链表长度

头节点里同时还有一个参数 len,和上边提到的 SDS 里类似,这里是用来记录链表长度的。因此获取链表长度时不用再遍历整个链表,直接拿到 len 值就可以了,这个时间复杂度是 O(1)。

你看,这些特性都降低了 List 使用时的时间开销。

2.3、压缩列表

双端链表我们已经熟悉了。不知道你有没有注意到一个问题:如果在一个链表节点中存储一个小数据,比如一个字节。那么对应的就要保存头节点,前后指针等额外的数据。

这样就浪费了空间,同时由于反复申请与释放也容易导致内存碎片化。这样内存的使用效率就太低了。

于是,压缩列表上场了!

Image

它是经过特殊编码,专门为了提升内存使用效率设计的。所有的操作都是通过指针与解码出来的偏移量进行的。

并且压缩列表的内存是连续分配的,遍历的速度很快。

2.4、字典

Redis 作为 K-V 型数据库,所有的键值都是用字典来存储的。

日常学习中使用的字典你应该不会陌生,想查找某个词通过某个字就可以直接定位到,速度非常快。这里所说的字典原理上是一样的,通过某个 key 可以直接获取到对应的value。

字典又称为哈希表,这点没什么可说的。哈希表的特性大家都很清楚,能够在 O(1) 时间复杂度内取出和插入关联的值。

2.5、跳跃表

作为 Redis 中特有的数据结构-跳跃表,其在链表的基础上增加了多级索引来提升查找效率。

Image

这是跳跃表的简单原理图,每一层都有一条有序的链表,最底层的链表包含了所有的元素。这样跳跃表就可以支持在 O(logN) 的时间复杂度里查找到对应的节点。

下面这张是跳表真实的存储结构,和其它数据结构一样,都在头节点里记录了相应的信息,减少了一些不必要的系统开销。

Image

3.合理的数据编码

对于每一种数据类型来说,底层的支持可能是多种数据结构,什么时候使用哪种数据结构,这就涉及到了编码转化的问题。

那我们就来看看,不同的数据类型是如何进行编码转化的:

String:存储数字的话,采用int类型的编码,如果是非数字的话,采用 raw 编码;

List:字符串长度及元素个数小于一定范围使用 ziplist 编码,任意条件不满足,则转化为 linkedlist 编码;

Hash:hash 对象保存的键值对内的键和值字符串长度小于一定值则使用ziplist;

Set:保存元素为整数及元素个数小于一定范围使用 intset 编码,任意条件不满足,则使用 hashtable 编码;

Zset:zset 对象中保存的元素个数小于及成员长度小于一定值使用 ziplist 编码,任意条件不满足,则使用 skiplist 编码。

4.合适的线程模型

4.1 IO多路复用模型

IO:网络IO

多路:多个TCP连接

复用:公用一个线程

Redis使用这种线程模型 使一台Server可以监听多个客户端,接收多个客户端的命令事件 推送到一个队列中,然后队列中命令逐个执行,再将结果返回给客户端

4.2 避免上下文切换

多线程在执行时需要进行CPU的上下文切换,操作比较耗时,而Redis又是基于内存实现的,对于内存来说,没有上下文切换时的效率是最高的

4.3单线程模型

Redis 中使用了 Reactor 单线程模型,你可能对它并不熟悉。没关系,只需要大概了解一下即可。

Image

这张图里,接收到用户的请求后,全部推送到一个队列里,然后交给文件事件分派器,而它是单线程的工作方式。Redis 又是基于它工作的,所以说 Redis 是单线程的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
尚硅谷是一个教育机构,他们提供了一份关于Redis学习笔记。根据提供的引用内容,我们可以了解到他们提到了一些关于Redis配置和使用的内容。 首先,在引用中提到了通过执行命令"vi /redis-6.2.6/redis.conf"来编辑Redis配置文件。这个命令可以让你进入只读模式来查询"daemonize"配置项的位置。 在引用中提到了Redis会根据键值计算出应该送往的插槽,并且如果不是该客户端对应服务器的插槽,Redis会报错并告知应该前往的Redis实例的地址和端口。 在引用中提到了通过修改Redis的配置文件来指定Redis的日志文件位置。可以使用命令"sudo vim /etc/redis.conf"来编辑Redis的配置文件,并且在文件中指定日志文件的位置。 通过这些引用内容,我们可以得出结论,尚硅谷的Redis学习笔记涵盖了关于Redis的配置和使用的内容,并提供了一些相关的命令和操作示例。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [Redis学习笔记--尚硅谷](https://blog.csdn.net/HHCS231/article/details/123637379)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [Redis学习笔记——尚硅谷](https://blog.csdn.net/qq_48092631/article/details/129662119)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值