redis

常见5种数据类型:string(字符串),list(列表),hash(哈希),set(集合),zset(有序集合)

第一节 key操作的相关命令

语法 功能
keys * 查看当前库所有key (匹配:keys *1)
exists key 判断某个key是否存在
type key 查看你的key是什么类型
del key 删除指定的key数据
unlink key 非阻塞删除,仅将keys从keyspace元数据中删除,真正的删除会在后续异步操作
expire key 10 10秒钟:为给定的key设置过期时间
ttl key 查看还有多少秒过期,-1表示永不过期,-2表示已过期
select 命令切换数据库
dbsize 查看当前数据库的key的数量
flushdb 清空当前库
flushall 清空全部库

第二节 字符串类型(String)

2.1 简介

1 String是Redis最基本的类型,你可以理解成与Memcached一模一样的类型,一个key对应一个value。

2 String类型是二进制安全的。意味着Redis的string可以包含任何数据。比如jpg图片或者序列化的对象。

3 String类型是Redis最基本的数据类型,一个Redis中字符串value最多可以是512M

2.2 常用命令

语法 解释
set <key><value> 添加键值对
NX:当数据库中key不存在时,可以将key-value添加数据库
XX:当数据库中key存在时,可以将key-value添加数据库,与NX参数互斥
EX:key的超时秒数
PX:key的超时毫秒数,与EX互斥
get <key> 查询对应键值
append <key><value> 将给定的<value> 追加到原值的末尾
strlen <key> 获得值的长度
setnx <key><value> 只有在 key 不存在时 设置 key 的值
incr <key> 将 key 中储存的数字值增1,只能对数字值操作,如果为空,新增值为1
decr <key> 将 key 中储存的数字值减1,只能对数字值操作,如果为空,新增值为-1
incrby / decrby <key><步长> 将 key 中储存的数字值增减。自定义步长
mset <key1><value1><key2><value2> … 同时设置一个或多个 key-value对
mget <key1><key2><key3> … 同时获取一个或多个 value
msetnx <key1><value1><key2><value2> … 同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在。有一个失败则都失败(原子性)
getrange <key><起始位置><结束位置> 获得值的范围,类似java中的substring,前包,后包
setrange <key><起始位置><value> 用 <value> 覆写<key>所储存的字符串值,从<起始位置>开始(索引从0开始)。
setex <key> <过期时间> <value> 设置键值的同时,设置过期时间,单位秒。
getset <key><value> 以新换旧,设置了新值同时获得旧值。

redis指令运行的原子性

  • 所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。
    • (1)在单线程中, 能够在单条指令中完成的操作都可以认为是"原子操作",因为中断只能发生于指令之间。
    • (2)在多线程中,不能被其它进程(线程)打断的操作就叫原子操作。
    • (3)Redis单命令的原子性主要得益于的单线程。

问题 JAVA中的 a++ 是否具有原子性

原子性:即不可分割性。比如 a=0;(a非long和double类型) 这个操作是不可分割的,那么我们说这个操作是原子操作。再比如:a++; 这个操作实际是a = a + 1;是可分割的,所以他不是一个原子操作。非原子操作都会存在线程安全问题,需要使用同步技术(sychronized)或者锁(Lock)来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。

2.3 数据结构

String的数据结构为简单动态字符串(Simple Dynamic String,缩写SDS)。是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配.

如图中所示,内部为当前字符串实际分配的空间capacity一般要高于实际字符串长度len。当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次只会多扩1M的空间。需要注意的是字符串最大长度为512M。

第三节 Redis 列表(List)

3.1 简介

单键多值, 一个键下的value是一个List.Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。它的底层实际是个双向链表,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差。

3.2 常用命令

语法 功能
lpush/rpush <key><value1><value2><value3> … 从左边/右边插入一个或多个值。
lpop/rpop <key> 从左边/右边吐出一个值。值在键在,值光键亡。
rpoplpush <key1><key2> 从<key1>列表右边吐出一个值,插到<key2>列表左边
lrange <key><start><stop> 按照索引下标获得元素(从左到右)
0左边第一个,-1右边第一个,(0-1表示获取所有)
lindex <key><index> 按照索引下标获得元素(从左到右)
llen <key> 获得列表长度
linsert <key> before <value><newvalue> 在<value>的前面插入<newvalue>插入值
linsert <key> after <value><newvalue> 在<value>的后面插入<newvalue>插入值
lrem <key><n><value> 从左边删除n个value(从左到右)
lset<key><index><value> 将列表key下标为index的值替换成value

3.3 数据结构

List的数据结构为快速链表quickList。首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是ziplist,也即是压缩列表。它将所有的元素紧挨着一起存储,分配的是一块连续的内存。当数据量比较多的时候才会改成quicklist。因为普通的链表需要的附加指针空间太大,会比较浪费空间。比如这个列表里存的只是int类型的数据,结构上还需要两个额外的指针prev和next。

Redis将链表和ziplist结合起来组成了quicklist。也就是将多个ziplist使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。

第四节Redis 集合(Set)

4.1 简介

Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。

Redis的Set是string类型的无序集合。它底层其实是一个value为null的hash表,所以添加,删除,查找的复杂度都是O(1)。一个算法,随着数据的增加,执行时间的长短,如果是O(1),数据增加,查找数据的时间不变

4.2 常用命令

语法 功能
sadd <key><value1><value2> … 将一个或多个 member 元素加入到集合 key 中,已经存在的 member 元素将被忽略
smembers <key> 取出该集合的所有值。
sismember <key><value> 判断集合<key>是否为含有该<value>值,有1,没有0
scard<key> 返回该集合的元素个数。
srem <key><value1><value2> … 删除集合中的某个元素。
spop <key> 随机从该集合中吐出一个值
spop <key><N> 随机从该集合中吐出N个值。
srandmember <key><n> 随机从该集合中取出n个值。不会从集合中删除 。
smove <source><destination><value> 把集合中一个值从一个集合移动到另一个集合
sinter <key1><key2> 返回两个集合的交集元素。
sunion <key1><key2> 返回两个集合的并集元素。
sdiff <key1><key2> 返回两个集合的差集元素(key1中的,不包含key2中的)

4.2 数据结构

Set数据结构是dict字典,字典是用哈希表实现的。Java中HashSet的内部实现使用的是HashMap,只不过所有的value都指向同一个对象。Redis的set结构也是一样,它内部也使用hash结构,所有value都指向同一个内部值。

第五节 Redis 哈希(Hash)

5.1 简介

Redis hash 是一个键值对集合。Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。类似Java里面的Map<String,Object>用户ID为查找的key,存储的value用户对象包含姓名,年龄,生日等信息

  • 方式1 单key+序列化 .问题:每次修改用户的某个属性需要,先反序列化改好后再序列化回去。开销较大。

  • 方式2 多key-value .问题:用户ID数据冗余

  • 方式3 单key + 多(field+value)

  • 通过 key(用户ID) + field(属性标签) 就可以操作对应属性数据了,既不需要重复存储数据,也不会带来序列化和并发修改控制的问题

5.2 常用命令

语法 功能
hset <key><field><value> 给<key>集合中的 <field>键赋值<value>
hget <key1><field> 从<key1>集合<field>取出 value
hmset <key1><field1><value1><field2><value2>… 批量设置hash的值
hexists<key1><field> 查看哈希表 key 中,给定域 field 是否存在。
hkeys <key> 列出该hash集合的所有field
hvals <key> 列出该hash集合的所有value
hincrby <key><field><increment> 为哈希表 key 中的域 field 的值加上增量 1 -1
hsetnx <key><field><value> 将哈希表 key 中的域 field 的值设置为 value ,当且仅当域 field 不存在 .

5.3 数据结构

Hash类型对应的数据结构是两种:ziplist(压缩列表),hashtable(哈希表)。当field-value长度较短且个数较少时,使用ziplist,否则使用hashtable。

第六节 Redis 有序集合Zset

6.1 简介

Redis有序集合zset与普通集合set非常相似,是一个没有重复元素的字符串集合。不同之处是有序集合的每个成员都关联了一个评分(score),这个评分(score)被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复了 。因为元素是有序的, 所以你也可以很快的根据评分(score)或者次序(position)来获取一个范围的元素。访问有序集合的中间元素也是非常快的,因此你能够使用有序集合作为一个没有重复成员的智能列表。

6.2 常用命令

语法 功能
zadd <key><score1><value1><score2><value2>… 将一个或多个 member 元素及其 score 值加入到有序集 key 当中。
zrange<key><start><stop> [WITHSCORES] 升序返回有序集 key 中,下标在<start><stop>之间的元素,0代表第一个元素索引,-1代表最后一个元素索引.带WITHSCORES,可以让分数一起和值返回到结果集。
zrevrange <key><start><stop> [WITHSCORES] 降序返回有序集 key 中,下标在<start><stop>之间的元素,0代表第一个元素索引,-1代表最后一个元素索引.带WITHSCORES,可以让分数一起和值返回到结果集
zrangebyscore <key> <min> <max> [withscores] [limit offset count] 返回有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。有序集成员按 score 值递增(从小到大)次序排列。
zrevrangebyscore <key> <max> <min> [withscores] [limit offset count] 同上,改为从大到小排列。
zincrby <key><increment><value> 为元素的score加上增量
zrem <key><value> 删除该集合下,指定值的元素
zcount <key><min><max> 统计该集合,分数区间内的元素个数
zrank <key><value> 返回该值在集合中的排名,从0开始。

案例:如何利用zset实现一个文章访问量的排行榜?

6.3 数据结构

SortedSet(zset)是Redis提供的一个非常特别的数据结构,一方面它等价于Java的数据结构Map<String, Double>,可以给每一个元素value赋予一个权重score,另一方面它又类似于TreeSet,内部的元素会按照权重score进行排序,可以得到每个元素的名次,还可以通过score的范围来获取元素的列表。

面试题:

  1. redis对客户端发起的命令是单线程

    1. 内部有两大体现:
      1. 网络传输
      2. 执行到命令
  2. redis多线程的体现:异步删除、集群的数据同步、AOF和RDB持久化方式

  3. redis的底层数据结构:

    sds数据结构:{

    len,

    alloc

    flag

    char

    }

    1. 为什么不用c语言自己的chra数组
      1. 因为c语言的char数组以/0来判断是否结束,会导致不能存储任意的字符串。
      2. 在追加和计算长度时,c语言底层是变量目标char数组来实现的,效率低
    2. sds数据结构的优势
      1. 可变的大小,sds可以根据存储数据的大小,给数据结构分配合适的大小,以节省空间
      2. 采用紧密存储结构,c语言在数据不足8字节时,会直接分配8字节空间,导致空间浪费,而sds采用编译优化,占多少空间就分配多少,节省空间
      3. sds数据结构在计算长度和追加字符串时,不用遍历目标char数组,而是直接根据目标数组的长度直接在后面追加,效率高
  4. redis五种数据结构的使用场景

    1. 字符串 (Strings)
      • 定义:字符串是 Redis 最基本的数据类型,它可以存储二进制安全的字符串,最大长度为 512MB。
      • 使用场景:
        • 存储简单的键值对数据,例如用户的登录状态、网站计数器等。
        • 作为缓存,存储经常查询但不经常变化的数据,如商品详情页的缓存。
        • 存储消息队列中的消息,配合 Redis 的发布/订阅功能。
    2. 列表 (Lists)
      • 定义:列表是按照插入顺序排列的字符串集合,可以看作是一个双向链表。
      • 使用场景:
        • 实现消息队列,如任务队列、异步消息队列等。
        • 存储历史记录,如用户的浏览历史、聊天记录等。
        • 分布式锁实现,利用列表的原子操作来实现锁的获取和释放。
    3. 集合 (Sets)
      • 定义:集合是存储唯一字符串元素的无序集合。
      • 使用场景:
        • 存储不重复的元素集合,如用户好友列表、黑名单等。
        • 进行交集、并集和差集运算,比如统计共同兴趣的用户、不同广告渠道的用户重叠情况等。
    4. 有序集合 (Sorted Sets)
      • 定义:有序集合与集合类似,但每个元素都有一个分数,用于排序。
      • 使用场景:
        • 排行榜,如游戏排行榜、热门新闻排行等。
        • 时间线,按发布时间排序的消息流。
        • 优先级队列,如任务调度系统中的任务优先级。
    5. 哈希 (Hashes)
      • 定义:哈希是一个字符串字段和字符串值的映射表,类似于一个字典。
      • 使用场景:
        • 存储复杂对象,如用户资料、商品详情等,将对象的不同属性作为字段存储。
        • 统计指标,比如记录网站各个页面的访问次数。
        • 多个字段的批量读写操作,如购物车商品列表。

内存淘汰机制

说明:redis的淘汰机制主要分为三大类

  1. 针对设置ttk(过期时间)

    • volatile-lru:使用LRU算法移除key,只对设置了过期时间的键;(最近最少使用)
    • volatile-lfu:对设置了TTL的key基于LFU算法进行淘汰。(最不经常使用)
    • volatile-random:在过期集合中移除随机的key,只对设置了过期时间的键
    • volatile-ttl:移除那些TTL值最小的key,即那些最近要过期的key
  2. 没有设置ttk

    • allkeys-random:在所有集合key中,移除随机的key
    • allkeys-lru:在所有集合key中,使用LRU算法移除key
    • allkeys-lfu:对全体key给予LFRU算法进行淘汰
  3. 不淘汰

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

redis事务

命令 功能
multi 开始组队
exec 执行队列中的命令
discard 取消组队

三种情况

  1. 组队成功,提交成功
  2. 情况2,组队报错,提交失败:提交失败组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消
  3. 情况3, 组队成功,提交时有成功有失败。如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,其他的命令都会执行,不会回滚。

锁机制

  1. 悲观锁

    1
    悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
  2. 乐观锁

    1
    乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。
  3. 监视和取消监视key

    1
    在执行multi之前,先执行watch key1 [key2],可以监视一个(或多个) key ,如果在事务**执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。**

    取消 WATCH 命令对所有 key 的监视。如果在执行 WATCH 命令之后,EXEC 命令或DISCARD 命令先被执行了的话,那么就不需要再执行UNWATCH 了。

    warch 监听key,

三个特性

  • 单独的隔离操作
    事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
  • 没有隔离级别的概念
    队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行
  • 不保证原子性
    事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚

Lua脚本

redis的持久化

  1. 持久化机制:

    RDB、AOF

    DB(RedisDataBase)定时数据快照默认方式

    AOF(AppendOnly File)指令日志文件手动开启

    RDB文件未来存放是内存中某一时刻的所以数据,压缩存储二进制(看不懂)。“-省空间

    AOF文件未来存放的是所有客户端执行的写命令,AOF。RESP格式存储(可读)

RDB持久化

  1. 在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里.
  2. 持久化流程:
    1. —>RDB文件什么时候生成的。
      1.自动生成
      2.手动生成。
      手动执行save命令-Redis会给我们生成一个RDB文件
      手动执行bgsave命令,Redis也会给我们生成一个RDB文件。
      save or bgsave区别
      共同点:生成二进制压缩的文件,且未来可恢复数据
      不同点:性能:如果用save命令,将内存中的数据持久化到RDB文件使用的是Redis的(和执行客户端命令)相同的线程。(公用一个线程)
      性能:如果用rgsaVe命令,将内存中的数据持久化到RDB文件使用的是和执行客户端命的线程不是同一个。(另外启动一个新线程执行)

    2. 优势

      • 适合大规模的数据恢复
      • 对数据完整性和一致性要求不高更适合使用
      • 节省磁盘空间
      • 恢复速度快
    3. 劣势

      • Fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑
      • 虽然Redis在fork时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能。
      • 在备份周期在一定间隔时间做一次备份,所以如果Redis意外down掉的话,就会丢失最后一次快照后的所有修改。

AOF持久化

Append Only File 以日志的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(读操作不记录), 只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作

在重写时,使用的是子进程,没重写时,源aof文件是主进程修改的

持久化流程

(1)客户端的请求写命令会被append追加到AOF缓冲区内;

(2)AOF缓冲区根据AOF持久化策略[always,everysec,no]将操作sync同步到磁盘的AOF文件中;

(3)AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量;

(4)Redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的;

AOF同步策略
  • appendfsync always
    • 始终同步,每次Redis的写入都会立刻记入日志;性能较差但数据完整性比较好
  • appendfsync everysec
    • 每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失。
  • appendfsync no
    • redis不主动进行同步,把同步时机交给操作系统。

主从复制

  1. 读写分离
  2. 容灾

实现思路

  1. 一个redis服务作为主机,主要负责写操作
  2. 两个redis服务作为从机,主要负责读操作
  3. 从机自动从主机同步数据下来
  4. 从机主动找主机,而主机不会找从机

主从复制的三种模式

  1. 一主二仆
  2. 星火相传
  3. 反客为主 : master掉了之后,仆人通过slaveof no one 变成master

哨兵模式

反客为主的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库

  1. 设置一主多仆

    设置优先级:replica-priority 100

  2. 编辑哨兵配置 :配置master的 1 代表哨兵的数量

    1
    sentinel monitor mymaster 127.0.0.1 6379 1
  3. 启动哨兵

    1
    redis-sentinel /root/myredis/sentinel.conf

redis集群

  1. 什么是集群

    Redis 集群实现了对Redis的水平扩容,即启动N个redis节点,将整个数据库分布存储在这N个节点中,每个节点存储总数据的1/N。

    Redis 集群通过分区(partition)来提供一定程度的可用性(availability ):即使集群中有一部分节点失效或者无法进行通讯,集群也可以继续处理命令请求。

  2. 搭建集群

    1. 编写配置文件

      1
      2
      3
      4
      ....
      cluster-enabled yes // 开启集群
      cluster-config-file nodes-6379.conf
      cluster-node-timeout 15000
    2. 启动集群

      1
      redis-cli --cluster create --cluster-replicas 1 192.168.1.128:6379 192.168.1.128:6380 192.168.1.128:6381 192.168.1.128:6389 192.168.1.128:6390 192.168.1.128:6391
    3. 集群的登录

      redis-cli -c -p 6379

集群提供的好处

  • 实现扩容
  • 分摊压力
  • 无中心配置相对简单

集群的不足

  • 多键操作是不被支持的
  • 多键的Redis事务是不被支持的。lua脚本不被支持
  • 由于集群方案出现较晚,很多公司已经采用了其他的集群方案,而代理或者客户端分片的方案想要迁移至redis cluster,需要整体迁移而不是逐步过渡,复杂度较大。

1.2、缓存常见问题

缓存最常见的3个问题: 面试

  1. 缓存穿透
  2. 缓存雪崩
  3. 缓存击穿

缓存穿透: 是指查询一个不存在的数据,由于缓存无法命中,将去查询数据库,但是数据库也无此记录,并且出于容错考虑,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。

  • 解决1 :空结果也进行缓存,但它的过期时间会很短,最长不超过五分钟,但是不能防止随机穿透。

  • 解决2 :使用布隆过滤器来解决随机穿透问题。

缓存雪崩:是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。

  • 解决1:原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

  • 解决2:如果单节点宕机,可以采用集群部署方式防止雪崩

缓存击穿: 是指对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:如果这个key在大量请求同时进来之前正好失效,那么所有对这个key的数据查询都落到db,我们称为缓存击穿。

与缓存雪崩的区别:

  1. 击穿是一个热点key失效
  2. 雪崩是很多key集体失效
  • 解决:锁

缓存穿透:查询根本不存在的数据

​ 1.解决方案1:缓存null

​ 2…解决方案2:布隆过滤器

缓存雪崩: 同一时刻大量的key同时失效|缓存单节点故障问题造成的

解决方案1:设置随机的过期时间,

解决方案2:加redis集群

缓存击穿: 某个热点key过期,比如秒杀

解决方案:使用分布式锁

缓存一致性:canal解决