最近深入的学习了一下Redis ,在此记录一下 共同学习。


目录

  • 一、redis的数据结构和常用命令
  • 五种数据结构
  • 常用命令
  • 1.字符串string常用操作
  • 2.哈希hash常用操作
  • 3.列表list常用操作
  • 4.集合set常用操作
  • 5.有序集合zset常用命令
  • 其他命令
  • 二、redis的持久化
  • 1.RDB快照(snapshot)
  • 2.AOF(append-only file)
  • 3.混合持久化
  • 三、redis的主从架构、哨兵机制和集群搭建
  • 1. Redis的主从架构如何搭建:
  • 2.Redis哨兵高可用架构
  • 3.高可用集群搭建
  • 四、通过源码看底层结构
  • 1.string
  • 2.list
  • 3.hash
  • 4.set
  • 5.zset
  • 五、Redis常见问题及解决思路


一、redis的数据结构和常用命令

五种数据结构

字符串string
哈希hash
列表list
集合set
有序集合zset

常用命令

推荐一个Redis命令很全的中文参考网站,而且有详细的命令介绍和使用示例。

redis 源码分析 redis源码剖析与实战_redis 源码分析


下面,我列出一些常用的命令:

1.字符串string常用操作

SET key value //存入字符串键值对
GET key //获取一个字符串键值
应用场景: 单值缓存

MSET key value [key value …] //批量存储字符串键值对
MGET key [key …] //批量获取字符串键值
应用场景:对象缓存 如:MSET user:1:name xiaoming user:1:age 18

SETNX key value //存入一个不存在的字符串键值对,如果key已经存在,会不成功
应用场景:分布式锁 SETNX product:10001 true //返回1代表获取锁成功

INCR key //将key中储存的数字值加1 (有原子性)
DECR key //将key中储存的数字值减1(有原子性)
应用场景:计数器,如:微博点赞、文章浏览次数等。

DEL key [key …] //删除一个键
EXPIRE key seconds //设置一个键的过期时间(秒)
INCRBY key increment //将key所储存的值加上increment(有原子性)
DECRBY key decrement //将key所储存的值减去decrement(有原子性)

2.哈希hash常用操作

HSET key field value //存储一个哈希表key的键值
HSETNX key field value //存储一个不存在的哈希表key的键值,如果key已经存在,会不成功
HMSET key field value [field value …] //在一个哈希表key中存储多个键值对
HGET key field //获取哈希表key对应的field键值
HMGET key field [field …] //批量获取哈希表key中多个field键值
HDEL key field [field …] //删除哈希表key中的field键值
HLEN key //返回哈希表key中field的数量
HGETALL key //返回哈希表key中所有的键值
HINCRBY key field increment //为哈希表key中field键的值加上增量increment

应用场景:
对象缓存:

存储:HMSET user 1:name xiaoming 1:age 18
读取:HMGET user 1:name (可以只读取用户的名字信息,这是String数据结构做不到的)

如:购物车功能:key是购物车ID(cart:1001), field是商品id(88),商品数量为value
添加商品:hset cart:1001 88 1
增加数量:hincrby cart:1001 88 1
商品总数:hlen cart:1001
删除商品:hdel cart:1001 88
获取购物车所有商品:hgetall cart:1001

3.列表list常用操作

LPUSH key value [value …] //将一个或多个值value插入到key列表的表头(最左边)
RPUSH key value [value …] //将一个或多个值value插入到key列表的表尾(最右边)
LPOP key //移除并返回key列表的头元素
RPOP key //移除并返回key列表的尾元素
LRANGE key start stop //返回列表key中指定区间内的元素,区间以偏移量start和stop指定
BLPOP key [key …] timeout //从key列表表头弹出一个元素,若列表中没有元素,阻塞等待 timeout秒,如果timeout=0,一直阻塞等待
BRPOP key [key …] timeout //从key列表表尾弹出一个元素,若列表中没有元素,阻塞等待 timeout秒,如果timeout=0,一直阻塞等待

应用场景:
通过上面的命令,我们可以随意操作一个value在list中,左进、左出、右进、右出,通过这些方法的组合,我们可以想到什么数据结构? 栈结构:先进后出, 队列:先进先出,阻塞队列:用BLPOP取出就是阻塞队列。

4.集合set常用操作

SADD key member [member …] //往集合key中存入元素,元素存在则忽略,若key不存在则新建
SREM key member [member …] //从集合key中删除元素
SMEMBERS key //获取集合key中所有元素
SCARD key //获取集合key的元素个数
SISMEMBER key member //判断member元素是否存在于集合key中
SRANDMEMBER key [count] //从集合key中选出count个元素,元素不从key中删除
SPOP key [count] //从集合key中选出count个元素,元素从key中删除

运算操作
SINTER key [key …] //交集运算
SINTERSTORE destination key [key …] //将交集结果存入新集合destination中
SUNION key [key …] //并集运算
SUNIONSTORE destination key [key …] //将并集结果存入新集合destination中
SDIFF key [key …] //差集运算
SDIFFSTORE destination key [key …] //将差集结果存入新集合destination中

应用场景:
在社交APP中,如知乎或者微博,有一个功能,“共同关注”,就是求两个集合的交集。

5.有序集合zset常用命令

ZADD key score member [[score member]…] //往有序集合key中加入带分值元素
ZREM key member [member …] //从有序集合key中删除元素
ZSCORE key member //返回有序集合key中元素member的分值
ZINCRBY key increment member //为有序集合key中元素member的分值加上increment
ZCARD key //返回有序集合key中元素个数
ZRANGE key start stop [WITHSCORES] //正序获取有序集合key从start下标到stop下标的元素
ZREVRANGE key start stop [WITHSCORES] //倒序获取有序集合key从start下标到stop下标的元素
ZUNIONSTORE destkey numkeys key [key …] //并集计算
ZINTERSTORE destkey numkeys key [key …] //交集计算

应用场景:
热门新闻top10排行:
点击新闻加1分值 : ZINCRBY hotNews:20211204 1 新闻名字
展示当日排行前十: ZREVRANGE hotNews:20211204 0 9 WITHSCORES

其他命令

keys [MATCH pattern] 全量遍历键
用来列出所有满足特定正则字符串规则的key,当redis数据量比较大时, 性能比较差,要避免使用。

SCAN cursor [MATCH pattern] [COUNT count] 渐进式遍历键 是一个基于游标的迭代器
scan 参数提供了三个参数,第一个是 cursor 整数值(hash桶的索引值),第二个是 key 的正则模式, 第三个是一次遍历的key的数量(参考值,底层遍历的数量不一定),并不是符合条件的结果数量。第 一次遍历时,cursor 值为 0,然后将返回结果中第一个整数值作为下一次遍历的 cursor。一直遍历 到返回的 cursor 值为 0 时结束。

Info:查看redis服务运行信息

二、redis的持久化

Redis 启动服务: src/redis‐server redis.conf
redis.conf文件是 启动redis的配置文件,我们持久化相关的配置也都在这个文件里。

Redis的持久化配置目前有3中:
1、RDB快照(snapshot) 这是Redis默认的持久化方式。
2、AOF(append-only file)
3、混合持久化

1.RDB快照(snapshot)

Redis 将内存数据库快照保存在名字为 dump.rdb 的二进制文件中。

配置解释:

save 60 1000

60 秒内有至少有 1000 个键被改动”这一条件时, 自动保存一次rdb文件。

redis 源码分析 redis源码剖析与实战_缓存_02


还可以手动执行命令生成RDB快照,进入redis客户端执行命令save或bgsave可以生成dump.rdb文件, 每次命令执行都会将所有redis内存快照到一个新的rdb文件里,并覆盖原有rdb快照文件。

redis 源码分析 redis源码剖析与实战_Redis_03


配置自动生成rdb文件后台使用的是bgsave方式。

2.AOF(append-only file)

AOF 持久化,将修改的每一条指令记录进文件appendonly.aof中(先写入os cache,每隔一段时间 fsync到磁盘);
可以通过修改配置文件来打开 AOF 功能:

appendonly yes #打开AOF持久化

比如执行命令“set test 666”,aof文件里会记录如下数据:

*3
$3  
set 
$5
test  
$3 
666

这是一种resp协议格式数据,星号后面的数字代表命令有多少个参数,$号后面的数字代表这个参数有几 个字符

也可以配置 Redis 多久才将数据 fsync 到磁盘一次:

appendfsync always:每次有新命令追加到 AOF 文件时就执行一次 fsync ,非常慢,也非常安全。
 appendfsync everysec:每秒 fsync 一次,足够快,并且在故障时只会丢失 1 秒钟的数据。 (默认的,且推荐这个)
 appendfsync no:从不 fsync ,将数据交给操作系统来处理。更快,也更不安全的选择。

AOF重写:
AOF文件里可能有太多没用指令,所以AOF会定期根据内存的最新数据生成整理后的aof文件。
配置可以控制AOF自动重写频率:

auto‐aof‐rewrite‐min‐size 64mb //aof文件至少要达到64M才会自动重写,文件太小恢复速度本来就 很快,重写的意义不大 
 auto‐aof‐rewrite‐percentage 100 //aof文件自上一次重写后文件大小增长了100%则再次触发重写

AOF还可以通过命令去重写,进入redis客户端执行命令bgrewriteaof重写AOF。重写也是新的线程去处理的,不会影响客户端发送Redis的请求。

RDB 和 AOF 对比:
因为RDB文件是二进制的,所以它占用空间小、恢复速度快。但是相比数据安全性,AOF的数据会更全更安全。如果你同时启用了这两个持久化方式,Redis在启动恢复的时候,会优先选择AOF的数据,因为它一般情况下数据会更完整。

3.混合持久化

什么事混合持久化,其实就是AOF的一种文件, 在重写的时候,不再是单纯将内存数据转换为RESP命令写入AOF文件,而是将写数据这一刻之前的内存做RDB快照处理,并且将RDB快照内容和增量的AOF修改内存数据的命令存在一 起,都写入新的AOF文件。

所以混合持久化AOF文件appendonly.aof的结构:RDB格式+AOF格式。
因为RDB是二进制的 空间小、读取速度很快。所以 Redis 重启的时候,可以先加载 RDB 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,因此重启效率大幅得到提升。

通过如下配置可以开启混合持久化(必须先开启aof):

aof‐use‐rdb‐preamble yes

三、redis的主从架构、哨兵机制和集群搭建

1. Redis的主从架构如何搭建:

redis 源码分析 redis源码剖析与实战_缓存_04

一个Redis主节点master和多个Redis从节点slave,组合到一起,slave节点从master节点复制数据保持数据同步,master节点可以为客户端提供读写,slaver只能提供读的服务。
如何操作:
首先正常启动主节点,然后再启动从节点之前在redis.conf文件中加一些配置,让从节点的数据从主节点里复制,所以要配置主节点的IP和端口。
从节点要修改配置 redis.conf文件:

// 配置主从复制 
 replicaof 192.168.0.60 6379    # 从本机6379的redis实例复制数据,Redis 5.0之前使用slaveof 
 replica‐read‐only yes # 配置从节点只读

启动从节点,就OK了

redis‐server redis.conf

可以看到,配置启动主从架构很简单,只需要从节点中配置一个主节点的地址就OK了。

2.Redis哨兵高可用架构

redis 源码分析 redis源码剖析与实战_redis_05


sentinel哨兵是特殊的redis服务,不提供读写服务,主要用来监控redis实例节点,master节点挂了,就选举slave节点作为master节点。

哨兵架构下client端第一次从哨兵找出redis的主节点,后续就直接访问redis的主节点。当redis的主节点发生变化,哨兵会第一时间感知到,并且将新的redis 主节点通知给client端。

搭建步骤:
在sentinel.conf文件中修改配置(注意这个文件名字叫sentinel 翻译成中文就是哨兵,所以我们都说Redis的哨兵架构)

将相关配置修改为如下值:
port 26379
daemonize yes
pidfile "/var/run/redis-sentinel-26379.pid"
logfile "26379.log"
dir "/usr/local/redis-5.0.3/data"
# sentinel monitor <master-redis-name> <master-redis-ip> <master-redis-port> <quorum>
# quorum是一个数字,指明当有多少个sentinel认为一个master失效时(值一般为:sentinel总数/2 + 1),master才算真正失效
sentinel monitor mymaster 192.168.0.60 6379 2   # mymaster这个名字随便取,客户端访问时会用到

启动sentinel哨兵实例

src/redis-sentinel sentinel-26379.conf`

查看sentinel的info信息

src/redis-cli -p 26379
 127.0.0.1:26379>info

sentinel集群都启动完毕后,会将哨兵集群的元数据信息写入所有sentinel的配置文件里去(追加在文件的最下面)。当redis主节点如果挂了,哨兵集群会重新选举出新的redis主节点,
同时会修改所有sentinel节点配置文件的集群元数据信息,
同时还会修改sentinel文件里之前配置的mymaster对应的6379端口。
所以我们可以在sentinel配置文件中查看历史的节点选举情况。

哨兵的Jedis连接代码:

public class JedisSentinelTest {
    public static void main(String[] args) throws IOException {

        JedisPoolConfig config = new JedisPoolConfig();
        config.setMaxTotal(20);
        config.setMaxIdle(10);
        config.setMinIdle(5);

        String masterName = "mymaster";
        Set<String> sentinels = new HashSet<String>();
        sentinels.add(new HostAndPort("192.168.0.60",26379).toString());
        sentinels.add(new HostAndPort("192.168.0.60",26380).toString());
        sentinels.add(new HostAndPort("192.168.0.60",26381).toString());
        //JedisSentinelPool其实本质跟JedisPool类似,都是与redis主节点建立的连接池
        //JedisSentinelPool并不是说与sentinel建立的连接池,而是通过sentinel发现redis主节点并与其建立连接
        JedisSentinelPool jedisSentinelPool = new JedisSentinelPool(masterName, sentinels, config, 3000, null);
        Jedis jedis = null;
        try {
            jedis = jedisSentinelPool.getResource();
            System.out.println(jedis.set("test", "testvalue"));
            System.out.println(jedis.get("test"));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //注意这里不是关闭连接,在JedisPool模式下,Jedis会被归还给资源池。
            if (jedis != null)
                jedis.close();
        }
    }
}

注意,以上哨兵机制, 性能和高可用性等各方面表现 一般,特别是在主从切换的瞬间存在访问瞬断的情况,而且哨兵模式只有一个主节点对外提供服务,没法支持 很高的并发,且单个主节点内存也不宜设置得过大。 所以就有了下面的高可用的集群架构。

3.高可用集群搭建

redis 源码分析 redis源码剖析与实战_数据库_06


这是一个由多个主从节点群组成的分布式服务器群,它具有复制、高可用和分片特性。Redis集群不需要sentinel哨兵也能完成节点移除和故障转移的功能。需要将每个节点设置成集群模式,这种集群模式没有中 心节点,可水平扩展。

redis集群需要至少三个master节点。
这里搭建三个master节点,并且给每个master再搭建一个slave节 点,总共6个redis节点,这里用三台机器部署6个redis实例,每台机器一主一从,搭建集群的步骤如下:
在redis.conf 文件中修改

把之前的redis.conf配置文件copy到8001下,修改如下内容:
(1)daemonize yes
(2)port 8001(分别对每个机器的端口号进行设置)
(3)pidfile /var/run/redis_8001.pid  # 把pid进程号写入pidfile配置的文件
(4)dir /usr/local/redis-cluster/8001/(指定数据文件存放位置,必须要指定不同的目录位置,不然会丢失数据)
(5)cluster-enabled yes(启动集群模式)
(6)cluster-config-file nodes-8001.conf(集群节点信息文件,这里800x最好和port对应上)
(7)cluster-node-timeout 10000
 (8)# bind 127.0.0.1(bind绑定的是自己机器网卡的ip,如果有多块网卡可以配多个ip,代表允许客户端通过机器的哪些网卡ip去访问,内网一般可以不配置bind,注释掉即可)
 (9)protected-mode  no   (关闭保护模式)
 (10)appendonly yes
如果要设置密码需要增加如下配置:
 (11)requirepass 123456     (设置redis访问密码)
 (12)masterauth 123456      (设置集群节点间访问密码,跟上面一致)

把6台Redis都修改好这样的配置之后,分别启动6个redis实例,然后检查是否启动成功。

用redis-cli创建整个redis集群:
执行这条命令需要确认三台机器之间的redis实例要能相互访问,可以先简单把所有机器防火墙关掉,如果不关闭防火墙则需要打开redis服务端口和集群节点gossip通信端口16379(默认是在redis端口号上加1W)
关闭防火墙
systemctl stop firewalld # 临时关闭防火墙
systemctl disable firewalld # 禁止开机启动

用这个命令启动集群:
 /usr/local/redis-5.0.3/src/redis-cli -a 123456 --cluster create --cluster-replicas 1 192.168.0.61:8001 192.168.0.62:8002 192.168.0.63:8003 192.168.0.61:8004 192.168.0.62:8005 192.168.0.63:8006

最后查看集群信息
连接任意一个客户端即可:./redis-cli -c -h -p (-a访问服务端密码,-c表示集群模式,指定ip地址和端口号)

cluster info(查看集群信息)
 cluster nodes(查看节点列表)

关闭集群则需要逐个进行关闭,使用命令:

/usr/local/redis-5.0.3/src/redis-cli -a 123456 -c -h 192.168.0.60 -p 800* shutdown

集群分析:
Redis Cluster 将所有数据划分为 16384 个 slots(槽位),每个节点负责其中一部分槽位。槽位的信息存储于每 个节点中。
槽位定位算法: 会对 key 值使用 crc16 算法进行 hash 得到一个整数值,然后用这个整数值对 16384 进行取模 来得到具体槽位。
所以我们要尽量避免bigkey,也就是一个key对应的value非常大,这个key存在某一个槽位中,必然会导致数据倾斜。

四、通过源码看底层结构

redis的源码使用C语言编写的。这里简单的记录一下它底层的数据结构 共同深入学习。

1.string

string数据结构底层存储是: 简单动态字符串(SDS,simple dynamic string)
详细讲解可以看这个博客:javascript:void(0) SDS数据结构的主要优点是:
1、二进制的安全的数据结构
2、提供了内存预分配机制、避免频繁的内存分配
3、兼容C语言的函数库

2.list

List是一个有序(按加入的时序排序)的数据结构,Redis采用quicklist(双端链表) 和 ziplist 作为List的底层实现。

3.hash

Hash 数据结构底层实现为一个字典( dict )来存储K-V的数据结构,当数据量比较小,或者单个元素比较小时,底层用ziplist存储。

4.set

Set 为无序的,自动去重的集合数据类型,Set 数据结构底层实现为一个value 为 null 的 字典( dict ),当数据可以用整形表示时,Set集合将被编码为intset数据结构。两个条件任意满足时
Set将用hashtable存储数据。

5.zset

ZSet 为有序的,自动去重的集合数据类型,ZSet 数据结构底层实现为 字典(dict) + 跳表(skiplist) ,当数据比较少时,用ziplist编码结构存储。


五、Redis常见问题及解决思路

缓存穿透: 在缓存上没有匹配到,就去查数据库了。
方案1: 给无效ID设置缓存且加上过期时间。

缓存失效(击穿): 缓存突然失效,但是大量请求还在。
方案1:商品失效时间,不要统一设置,要在一个范围设置随机失效时间

缓存雪崩: 如果Redis宕机,所有请求都到数据库,导致数据库挂了,所有网站崩溃。
方案1: 可以做限流,防止雪崩,可能会有些体验不好,但是至少可以保证服务器不宕机。

热点缓存key重建优化: 冷数据突然变成热点数据,但是我们还没有缓存。
方案:用分布式锁,重建缓存。

缓存数据库双写不一致: 非常高并发情况,才有可能出现这个问题。
方案: 用队列 串行化执行, 延时删除, 推荐加分布式锁,用读写锁。
canal是阿里云的开源中间件,会监听binlog去更新缓存

常用优化小提示:
拒绝bigkey: value值很大。 解决:拆分,取模拆分。
用缓存尽量加上超时时间。
禁用危险的命令 如keys
Redis有16个DB,不同的业务不要用不同的DB,这样对并发没有效果,可以用不同的Redis
Redis的事务功能较弱,如果用到事务就用lua脚本