Redis教程

Redis详解

本文主要是介绍Redis详解,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

目录

一、Redis概述

1. Redis是什么

2.Redis可以干什么

3.特性

 二、安装

1.windows安装

2.Linux安装

三、redis-benchmark性能测试

语法

redis 性能测试工具可选参数

测试

四、Redis的基本知识说明

基础命令

Redis为什么使用单线程还是这么快 

五、Redis的数据类型

Redis-Key

String(字符串)

List(列表)

Set(集合)

Hash(哈希)

Zset(有序集合)

geospatial (地理位置)

hyperloglog(基数统计)

bitmap(位图)

六、Redis事务

redis基本的事务操作

Redis事务的本质

Redis事务的特性

Redis的事务步骤

 正常执行事务

放弃事务

编译异常(命令错误)

运行异常(语法错误) 

redis实现乐观锁(watch)

测试在事务执行过程中,另外一个线程去修改

Jedis

什么是Jedis

通过jedis操作redis

通过jedis操作事务

七、SpringBoot集成Redis

八、Redis配置文件详解

 include :支持引用多个配置文件

NETWORK:网络

 GENERAL:通用

 SNAPSHOTTING:快照

 SECURITY:安全

 CLIENTS:客户端限制

 MEMORY MANAGEMENT:内存管理

 APPEND ONLY MODE: AOF配置

九、Redis持久化

RDB操作

触发机制

恢复rdb文件

 优缺点

AOF操作

开启AOF模式

修复appendonly.aof文件

优缺点

十、Redis订阅发布

命令

测试

十一、Redis主从复制

概念

主从复制的作用

集群环境搭建

主从复制之复制原理

哨兵模式详解

概念

配置哨兵

优缺点

 哨兵模式的全部配置

 十二、缓存穿透、雪崩

缓存穿透

概念

解决方案

缓存击穿

概念

解决方案

缓存雪崩

概念

解决方案


一、Redis概述

1. Redis是什么

Redis(Remote Dictionary Server),远程字典服务。是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。

Redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。

2.Redis可以干什么

1.内存存储,持久化,内存中断点即失,所以说持久化很重要。持久化的方式:RDB、AOF。

2.效率高,可以用于高速缓存

3.发布订阅系统

4.地图信息分析

5.计时器,计数器

6.。。。

3.特性

1.支持多样的数据类型

2.支持持久化

3.支持集群

4.支持事务

5.。。。

官网:Redis官网

中文网:redis中文官方网站

Redis推荐都是在Linux服务器上搭建的。 我们以下的操作都会在linux服务器上进行操作。


 二、安装

1.windows安装

1.下载安装包:https://github.com/tporadowski/redis/releases

2. 解压

 3.双击redis-server.exe,启动

 4.使用redis客户端连接redis,双击redis-cli.exe。

2.Linux安装

1.下载安装包:Index of /releases/。这里使用5.0.8。

 2. 在home文件夹下创建redis目录

mkdir redis

3. 把redis安装包上传到redis目录下

 4.解压安装包

tar -zxvf redis-5.0.8.tar.gz

 

 5.进入解压后的文件,可以看到redis的配置文件

 6.基本的环境安装

yum install gcc-c++

 7.查看版本

gcc -v

 8.执行make命令,这里需要一些时间。

make

执行成功后,再次执行make命令。

 9.执行make install

make install

 10.redis的默认安装路劲是/usr/local/bin,进入/usr/local/bin目录查看

 11.将我们的redis的配置文件复制到当前目录下

#创建redis-conf目录
mkdir redis-conf

# 把redis的配置文件拷贝到redis-conf目录下
cp /home/redis/redis-5.0.8/redis.conf  redis-conf/

我们之后就使用这个配置文件进行启动。

12.redis默认不是后台启动的,修改配置文件。

 13.启动redis服务

 回到bin目录下,启动服务

redis-server redis-conf/redis.conf

 14.测试连接

 15.查看redis的进程是否开启

ps -ef|grep redis

 16.如何关闭redis服务

shutdown

 再次查看进程是否存在

 注:若需要远程连接,需操作以下步骤:

  修改配置文件 

bind 0.0.0.0
protected-mode no

  测试连接,若连接不上,报以下错误

Unable to connect to Redis; nested exception is io.lettuce.core.RedisConnect

  redis默认端口号6379是不允许进行远程连接的,所以在防火墙中设置6379开启远程服务;

/sbin/iptables -I INPUT -p tcp --dport 6379 -j ACCEPT

三、redis-benchmark性能测试

redis-benchmark是一个压力测试工具,是官方自带的。

语法

redis 性能测试的基本命令如下:

redis-benchmark [option] [option value]

 注意:该命令是在 redis 的目录下执行的,而不是 redis 客户端的内部指令。

redis 性能测试工具可选参数

序号选项描述默认值
1-h指定服务器主机名127.0.0.1
2-p指定服务器端口6379
3-s指定服务器 socket
4-c指定并发连接数50
5-n指定请求数10000
6-d以字节的形式指定 SET/GET 值的数据大小2
7-k1=keep alive 0=reconnect1
8-rSET/GET/INCR 使用随机 key, SADD 使用随机值
9-P通过管道传输 <numreq> 请求1
10-q强制退出 redis。仅显示 query/sec 值
11--csv以 CSV 格式输出
12-l(L 的小写字母)生成循环,永久执行测试
13-t仅运行以逗号分隔的测试命令列表。
14-I(i 的大写字母)Idle 模式。仅打开 N 个 idle 连接并等待。

测试

场景:100个并发连接,1000000个请求

redis-benchmark -h localhost -p 6379 -c 100 -n 1000000

 


四、Redis的基本知识说明

基础命令

redis默认有16个数据库,查看redis.conf配置文件

 默认使用的是第0个数据库。

切换数据库

127.0.0.1:6379> select 3 #使用select进行数据库的切换
OK

 查看数据库大小

127.0.0.1:6379[3]> DBSIZE  #使用DBSIZE查看数据的大小
(integer) 0

 查看数据库中所有的key


127.0.0.1:6379[3]> set name zzz
OK
127.0.0.1:6379[3]> get name
"zzz"
127.0.0.1:6379[3]> keys * #查看数据库中所有的key
1) "name"

清除当前数据库

127.0.0.1:6379[3]> flushdb
OK
127.0.0.1:6379[3]> keys *
(empty list or set)

清除所有数据库中的内容

127.0.0.1:6379[3]> flushall
OK

Redis是单线程,官方表示,Redis是基于内存操作的,CPU不是Redis的性能瓶颈,Redis的瓶颈是根据机器的内存和网络带宽决定的,所以就使用了单线程。

Redis为什么使用单线程还是这么快 

redis是将所有的数据全部存放在内存中的,所以使用单线程去操作效率是最高的,因为多线程会来回的上下文切换,上下文切换是一个耗时的操作,对于内存系统来说,如果没有上下文切换效率就是最高的。


五、Redis的数据类型

Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与 范围查询, bitmaps, hyperloglog 和 地理空间(geospatial)索引半径查询。 Redis 内置了复制(replication)、LUA脚本、LRU驱动事件(LRU eviction),事务(transactions)和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动分区(Cluster) 提供高可用性(high availability)。

Redis命令帮助文档:Redis命令帮助文档

Redis-Key

设置key

127.0.0.1:6379> set name zzz
OK

判断key是否存在

127.0.0.1:6379> exists name
(integer) 0
127.0.0.1:6379> set name zzz
OK
127.0.0.1:6379> exists name
(integer) 1

移除某个key

127.0.0.1:6379> move name 1
(integer) 1

设置过期时间

127.0.0.1:6379> set name zzz
OK
127.0.0.1:6379> expire name 10  #单位是秒
(integer) 1

查看key剩余过期时间

127.0.0.1:6379> ttl name
(integer) 8
127.0.0.1:6379> ttl name
(integer) 2

查看key的类型

127.0.0.1:6379> type name
string

String(字符串)

追加字符:append

127.0.0.1:6379> append name '123'
(integer) 6   #这里返回的是值的长度
127.0.0.1:6379> get name
"zzz123"

获取字符长度:strlen

127.0.0.1:6379> strlen name
(integer) 6

自增+1:incr

127.0.0.1:6379> set views 0
OK
127.0.0.1:6379> get views
"0"
127.0.0.1:6379> incr views  #自增+1
(integer) 1
127.0.0.1:6379> incr views
(integer) 2
127.0.0.1:6379> get views
"2"

自减-1:decr

127.0.0.1:6379> decr views
(integer) 1
127.0.0.1:6379> 
127.0.0.1:6379> decr views
(integer) 0
127.0.0.1:6379> get views
"0"

增加多少:incrby

127.0.0.1:6379> incrby views 10
(integer) 10
127.0.0.1:6379> get views
"10"

减小多少:decrby

127.0.0.1:6379> decrby views 6
(integer) 4
127.0.0.1:6379> get views
"4"

截取字符:getrange

127.0.0.1:6379> set name 'hello,redis'
OK
127.0.0.1:6379> get name
"hello,redis"
127.0.0.1:6379> getrange name 0 2
"hel"
127.0.0.1:6379> getrange name 0 -1 #获取全部字符
"hello,redis"

替换字符:setrange

127.0.0.1:6379> get name
"hello,redis"
127.0.0.1:6379> setrange name 5 -
(integer) 11
127.0.0.1:6379> get name
"hello-redis"

设置过期时间:setex 

127.0.0.1:6379> setex name 30 'hello,redis'
OK
127.0.0.1:6379> ttl name
(integer) 26

不存在设置:setnx,在分布式锁中常常使用

127.0.0.1:6379> setnx name 'redis'  #如果不存在,则创建
(integer) 1
127.0.0.1:6379> setnx name 'mongdb' #如果存在,创建失败
(integer) 0

批量设置值:mset

127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3
OK
127.0.0.1:6379> keys *
1) "k3"
2) "k1"
3) "k2"
127.0.0.1:6379> msetnx k1 v1 k4 v4  #msetnx 是一个原子性操作,要么一起成功,要么一起失败
(integer) 0

批量获取值:mget

127.0.0.1:6379> mget k1 k2 k3
1) "v1"
2) "v2"
3) "v3"

设置对象

127.0.0.1:6379> mset user:name zzz user:age 1
OK
127.0.0.1:6379> mget user:name user:age
1) "zzz"
2) "1"

getset:先get然后在set

127.0.0.1:6379> getset name redis #如果不存在,则返回nil
(nil)
127.0.0.1:6379> get name
"redis"
127.0.0.1:6379> getset name mongodb #如果存在值,获取原来的值,并设置新的值
"redis"
127.0.0.1:6379> get name
"mongodb"

List(列表)

在Redis里面,我们可以把list玩成,栈、队列、阻塞队列。

所有的list命令都是以L开头的。

将一个值或者多个值插入到列表的头部(左)

127.0.0.1:6379> lpush names one two three  #将一个值或者多个值插入到列表的头部(左)
(integer) 3
127.0.0.1:6379> lrange names 0 -1
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> lrange names 0 1
1) "three"
2) "two"

 将一个值或者多个值插入到列表的尾部(右)

127.0.0.1:6379> rpush names four #将一个值或者多个值插入到列表的尾部(右)
(integer) 4
127.0.0.1:6379> lrange names 0 -1
1) "three"
2) "two"
3) "one"
4) "four"

获取list中的值

127.0.0.1:6379> lrange names 0 -1 #获取list中全部的值
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> lrange names 0 1 #获取list中第0个到第1个的值
1) "three"
2) "two"

移除元素(左)

127.0.0.1:6379> lrange names 0 -1
1) "three"
2) "two"
3) "one"
4) "four"
127.0.0.1:6379> lpop names
"three"
127.0.0.1:6379> lrange names 0 -1
1) "two"
2) "one"
3) "four"

移除元素(右)

127.0.0.1:6379> lrange names 0 -1
1) "two"
2) "one"
3) "four"
127.0.0.1:6379> rpop names
"four"
127.0.0.1:6379> lrange names 0 -1
1) "two"
2) "one"

获取list中某个下标的元素

127.0.0.1:6379> lrange names 0 -1
1) "two"
2) "one"
127.0.0.1:6379> lindex names 0
"two"

获取list的长度

127.0.0.1:6379> lrange names 0 -1
1) "two"
2) "one"
127.0.0.1:6379> llen names
(integer) 2

移除list中指定个数的值,精确匹配

127.0.0.1:6379> lrange names 0 -1
1) "three"
2) "three"
3) "two"
4) "one"
127.0.0.1:6379> lrem names 1 one  #移除一个
(integer) 1
127.0.0.1:6379> lrange names 0 -1
1) "three"
2) "three"
3) "two"
127.0.0.1:6379> lrem names 2 three  #移除两个
(integer) 2
127.0.0.1:6379> lrange names 0 -1
1) "two"

截取list中的值

127.0.0.1:6379> lpush names one two three four five
(integer) 5
127.0.0.1:6379> lrange names 0 -1
1) "five"
2) "four"
3) "three"
4) "two"
5) "one"
127.0.0.1:6379> ltrim names 1 2 #通过下标截取指定的长度
OK
127.0.0.1:6379> lrange names 0 -1
1) "four"
2) "three"

移除列表中最后一个元素,并移动到新的列表中

127.0.0.1:6379> lpush mylist one two three four
(integer) 4
127.0.0.1:6379> 
127.0.0.1:6379> lrange mylist 0 -1
1) "four"
2) "three"
3) "two"
4) "one"
127.0.0.1:6379> rpoplpush mylist myotherlist
"one"
127.0.0.1:6379> lrange myotherlist 0 -1
1) "one"
127.0.0.1:6379> lrange mylist 0 -1
1) "four"
2) "three"
3) "two"

将list列表中指定下标的值替换为另一个值

127.0.0.1:6379> exists names  # 判断列表是否存在
(integer) 0
127.0.0.1:6379> lset names 0 two #若列表不存在,则报错
(error) ERR no such key
127.0.0.1:6379> lpush names one 
(integer) 1
127.0.0.1:6379> lrange names 0 -1
1) "one"
127.0.0.1:6379> lset names 0 two #将列表中下标为0的值替换为two
OK
127.0.0.1:6379> lrange names 0 -1
1) "two"
127.0.0.1:6379> lset names 1 three #若列表中下标不存在,则报错
(error) ERR index out of range

 在列表的某个值前边或者后边插入一个值

127.0.0.1:6379> lpush names onw three
(integer) 2
127.0.0.1:6379> lrange names 0 -1
1) "three"
2) "onw"
127.0.0.1:6379> linsert names before three two #在three这个值的前边插入一个值
(integer) 3
127.0.0.1:6379> lrange names 0 -1
1) "two"
2) "three"
3) "onw"
127.0.0.1:6379> linsert names after three two #在three这个值的后边插入一个值
(integer) 4
127.0.0.1:6379> lrange names 0 -1
1) "two"
2) "three"
3) "two"
4) "onw"

Set(集合)

set是无序不重复集合。所有的set命令都是以S开头的。

插入:sadd

127.0.0.1:6379> sadd names one two three
(integer) 3

获取:smembers

127.0.0.1:6379> smembers names
1) "two"
2) "one"
3) "three"

查看元素是否存在:sismember

127.0.0.1:6379> sismember names one
(integer) 1
127.0.0.1:6379> sismember names four
(integer) 0

获取Set集合中的个数:scard

127.0.0.1:6379> scard names
(integer) 3

删除指定元素:srem

127.0.0.1:6379> srem names one
(integer) 1
127.0.0.1:6379> scard names
(integer) 2
127.0.0.1:6379> smembers names
1) "two"
2) "three"

随机获取元素:srandmember

127.0.0.1:6379> srandmember names #随机获取一个元素
"three"
127.0.0.1:6379> srandmember names
"two"
127.0.0.1:6379> srandmember names 2 #随机获取指定个数元素
1) "two"
2) "three"

随机删除元素:spop

127.0.0.1:6379> spop names #随机删除一个元素
"one"
127.0.0.1:6379> spop names 2 #随机删除指定个数元素
1) "four"
2) "three"
127.0.0.1:6379> smembers names
1) "two"

将一个指定的元素移动到另外一个set集合中:smove

127.0.0.1:6379> sadd names one two three
(integer) 3
127.0.0.1:6379> smembers names
1) "two"
2) "one"
3) "three"
127.0.0.1:6379> sadd names2 one
(integer) 1
127.0.0.1:6379> smembers names2
1) "one"
127.0.0.1:6379> smove names names2 two #删除某个指定的元素并移动到另一个set集合中
(integer) 1
127.0.0.1:6379> smembers names
1) "one"
2) "three"
127.0.0.1:6379> smembers names2
1) "two"
2) "one"

查看多个集合之间的差集:sdiff

127.0.0.1:6379> sadd key1 a b c
(integer) 3
127.0.0.1:6379> sadd key2 c d e
(integer) 3
127.0.0.1:6379> sdiff key1 key2
1) "a"
2) "b"

查看多个集合之间的交集:sinter

127.0.0.1:6379> sadd key1 a b c
(integer) 3
127.0.0.1:6379> sadd key2 c d e
(integer) 3
127.0.0.1:6379> sinter key1 key2
1) "c"

查看多个集合之间的并集:sunion

127.0.0.1:6379> sadd key1 a b c
(integer) 3
127.0.0.1:6379> sadd key2 c d e
(integer) 3
127.0.0.1:6379> sunion key1 key2
1) "a"
2) "b"
3) "c"
4) "d"
5) "e"

Hash(哈希)

可以想象成一个map,就相当于是key-map的结构。本质和string类型没有太大区别。

所有的hash命令都是以H开头的。

插入键值对:hset

127.0.0.1:6379> hset myhash name zzz
(integer) 1

获取:hget

127.0.0.1:6379> hget myhash name
"zzz"

插入多个键值对:hmset

127.0.0.1:6379> hmset myhash age 1 date 2022-01
OK

获取多个值:hmget

127.0.0.1:6379> hmget myhash age date
1) "1"
2) "2022-01"

获取全部键值对:hgetall

127.0.0.1:6379> hgetall myhash
1) "name"
2) "zzz"
3) "age"
4) "1"
5) "date"
6) "2022-01"

移除指定的键值对:hdel

127.0.0.1:6379> hdel myhash date age
(integer) 2
127.0.0.1:6379> hgetall myhash
1) "name"
2) "zzz"

获取键值对的个数:hlen

127.0.0.1:6379> hlen myhash
(integer) 1

判断键值对是否存在:hexists

127.0.0.1:6379> hexists myhash name
(integer) 1

获取所有的键:hkeys

127.0.0.1:6379> hkeys myhash
1) "name"

获取所有键对应的值:hvals

127.0.0.1:6379> hvals myhash
1) "zzz"

指定增量:hincrby

127.0.0.1:6379> hincrby myhash age 1
(integer) 1
127.0.0.1:6379> hget myhash age
"1"
127.0.0.1:6379> hincrby myhash age 5
(integer) 6
127.0.0.1:6379> hget myhash age
"6"

判断某个键值对是否存在:hsetnx

127.0.0.1:6379> hsetnx myhash date 2022-01 #如果键值对不存在,则插入
(integer) 1
127.0.0.1:6379> hsetnx myhash date 2022-02 #如果键值对存在,则失败
(integer) 0

Zset(有序集合)

插入:zadd

127.0.0.1:6379> zadd subject 1 zhangsan
(integer) 1

获取:zrange

127.0.0.1:6379> zrange subject 0 -1
1) "zhangsan"

排序(升序):zrangescore

127.0.0.1:6379> zrangebyscore score -inf +inf
1) "xiaohong"
2) "zhangsan"
3) "lisi"
127.0.0.1:6379> zrangebyscore score -inf +inf withscores 
1) "xiaohong"
2) "50"
3) "zhangsan"
4) "99"
5) "lisi"
6) "100"
127.0.0.1:6379> zrangebyscore score -inf 99 #获取分数小于99的
1) "xiaohong"
2) "zhangsan"
127.0.0.1:6379> zrangebyscore score -inf 99 withscores
1) "xiaohong"
2) "50"
3) "zhangsan"
4) "99"

移除指定元素:zrem

127.0.0.1:6379> zrem score xiaohong
(integer) 1
127.0.0.1:6379> zrange score 0 -1
1) "zhangsan"
2) "lisi"

排序(降序):zrevrange

127.0.0.1:6379> zrevrange score 0 -1
1) "lisi"
2) "zhangsan"

获取集合中的个数:zcard

127.0.0.1:6379> zcard score
(integer) 2

获取指定区间的成员数量:zcount

127.0.0.1:6379> zcount score 1 100
(integer) 2

geospatial (地理位置)

使用场景:朋友定位,附近的人,打车距离计算

使用这个功能可以推算地理位置的信息,两地之间的距离,附近的人。

GEO底层的实现原理其实就是Zset !

准备测试数据:经纬度查询工具

相关命令:

  • GEOADD
  • GEODIST
  • GEOHASH
  • GEOPOS
  • GEORADIUS
  • GEORADIUSBYMEMBER

GEOADD:添加地理位置

# 规则: 两极(南极,北极)无法直接插入,一般情况下我们会使用java程序直接导入
# 参数: key 值(纬度 经度 名称)
127.0.0.1:6379> geoadd china:city 116.405285 39.904989 beijing 121.472644 31.231706 shanghai 113.280637 23.125178 guangzhou 114.085947 22.547 shenzhen 113.665412 34.757975 zhengzhou 120.153576 30.287459 hangzhou
(integer) 6

GEODIST:获取两个给定位置之间的距离

指定单位的参数 unit 必须是以下单位的其中一个:

  • m 表示单位为米。
  • km 表示单位为千米。
  • mi 表示单位为英里。
  • ft 表示单位为英尺。

如果用户没有显式地指定单位参数, 那么 GEODIST 默认使用米作为单位。

127.0.0.1:6379> geodist china:city shanghai zhengzhou #查看上海到郑州的直线距离
"826835.6078"
127.0.0.1:6379> geodist china:city shanghai zhengzhou km
"826.8356"

GEOHASH:获取一个或多个位置元素的hash字符串

127.0.0.1:6379> geohash china:city beijing
1) "wx4g0b7xrt0"

GEOPOS:获取指定城市的纬度和经度

127.0.0.1:6379> geopos china:city beijing
1) 1) "116.40528291463851929"
   2) "39.9049884229125027"
127.0.0.1:6379> geopos china:city beijing shanghai
1) 1) "116.40528291463851929"
   2) "39.9049884229125027"
2) 1) "121.47264629602432251"
   2) "31.23170490709807012"

GEORADIUS:以给定的经纬度为中心,找出某一半径内的元素

127.0.0.1:6379> georadius china:city 121.5447 31.22249 1000 km #获取以纬度121.5447,经度31.22249为中心,1000km为半径的城市
1) "hangzhou"
2) "shanghai"
3) "zhengzhou"
127.0.0.1:6379> georadius china:city 121.5447 31.22249 1000 km withcoord withdist count 2
1) 1) "shanghai"
   2) "6.9294"
   3) 1) "121.47264629602432251"
      2) "31.23170490709807012"
2) 1) "hangzhou"
   2) "168.8068"
   3) 1) "120.15357345342636108"
      2) "30.28745790721532671"

GEORADIUSBYMEMBER:获取位于指定范围内的元素

127.0.0.1:6379> georadiusbymember china:city shanghai 1000 km withcoord withdist 
1) 1) "hangzhou"
   2) "164.0867"
   3) 1) "120.15357345342636108"
      2) "30.28745790721532671"
2) 1) "shanghai"
   2) "0.0000"
   3) 1) "121.47264629602432251"
      2) "31.23170490709807012"
3) 1) "zhengzhou"
   2) "826.8356"
   3) 1) "113.66541177034378052"
      2) "34.75797603259534441"

hyperloglog(基数统计)

使用场景:统计网页的访问uv(一个人访问一个网站多次,还是算作一个人)。

添加:pfadd

127.0.0.1:6379> pfadd key1 a b c d e f g
(integer) 1

获取基数数量:pfcount

127.0.0.1:6379> pfcount key1
(integer) 7

获取并集数量:pfmerge

127.0.0.1:6379> pfadd key1 a b c d e f g
(integer) 1
127.0.0.1:6379> pfcount key1
(integer) 7
127.0.0.1:6379> pfadd key2 e f g h
(integer) 1
127.0.0.1:6379> pfcount key2
(integer) 4
127.0.0.1:6379> pfmerge key3 key1 key2  #将key1 key2合并
OK
127.0.0.1:6379> pfcount key3
(integer) 8

bitmap(位图)

使用场景:打卡,统计用户登录状态。。。

以下就以打卡来举例:

周一到周日:

周一:打卡 周二:未打卡  周三:未打卡 周四:打卡 周五:未打卡  周六:打卡 周日:未打卡

 插入:setbit

127.0.0.1:6379> setbit sign 0 1 
(integer) 0
127.0.0.1:6379> setbit sign 1 0 
(integer) 0
127.0.0.1:6379> setbit sign 2 0 
(integer) 0
127.0.0.1:6379> setbit sign 3 1 
(integer) 0
127.0.0.1:6379> setbit sign 4 0 
(integer) 0
127.0.0.1:6379> setbit sign 5 1 
(integer) 0
127.0.0.1:6379> setbit sign 6 0 
(integer) 0

 获取某一条记录:getbit

127.0.0.1:6379> getbit sign 3
(integer) 1

 统计数量:bitcount

127.0.0.1:6379> bitcount sign
(integer) 3

六、Redis事务

redis基本的事务操作

Redis事务的本质

一组命令的集合。一个事务中的所有命令都会被序列化,在事务执行的过程中,会按照顺序执行。

Redis事务的特性

一次性、顺序性、排他性。

Redis事务不具备隔离性。所有的命令在事务中,并没有直接被执行,只有发起执行命令的时候才会被执行。

Redis的单条命令是保证原子性的,但是Redis的事务是不保证原子性的!

Redis的事务步骤

  • 开启事务(multi)
  • 命令入队(正常命令)
  • 执行事务(exec)

 正常执行事务

127.0.0.1:6379> multi     #开启事务
OK
127.0.0.1:6379> set k1 v1  #命令入队
QUEUED
127.0.0.1:6379> set k2 v2   #命令入队
QUEUED
127.0.0.1:6379> set k3 v3    #命令入队
QUEUED
127.0.0.1:6379> get k3      #命令入队
QUEUED
127.0.0.1:6379> exec       #执行事务
1) OK
2) OK
3) OK
4) "v3"

每一组事务执行完成以后,事务就结束了,如果想要执行新的事务,需要重新开启事务。

放弃事务

127.0.0.1:6379> multi   #开启事务
OK
127.0.0.1:6379> set k1 v1   #命令入队
QUEUED
127.0.0.1:6379> set k2 v2   #命令入队
QUEUED
127.0.0.1:6379> set k4 v4   #命令入队
QUEUED
127.0.0.1:6379> discard     #取消事务
OK
127.0.0.1:6379> get 4       #事务队列中的命令都不会被执行
(nil)

编译异常(命令错误)

事务中所有的命令都不会被执行。

127.0.0.1:6379> multi     #开启事务
OK
127.0.0.1:6379> set k1 v1  #命令入队
QUEUED 
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3   #错误命令
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:6379> set k4 v4
QUEUED
127.0.0.1:6379> exec    #执行事务报错
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k4  #所有的命令都不会被执行
(nil)

运行异常(语法错误) 

如果事务队列中存在语法行错误,那么执行命令的时候,其它命令是可以正常执行的,错误命令抛出异常。

127.0.0.1:6379> set k1 'v1'  #设置一个字符串
OK
127.0.0.1:6379> multi        #开启事务
OK
127.0.0.1:6379> incr k1      #命令入队,自增+1,命令是没问题的,但是字符串是不能自增的,在执行的时候会失败
QUEUED
127.0.0.1:6379> set k2 v2    #命令入队
QUEUED
127.0.0.1:6379> set k3 v3    #命令入队
QUEUED
127.0.0.1:6379> exec         #执行事务
1) (error) ERR value is not an integer or out of range  #虽然第一条命令报错了,但是事务中的其它命令依然正常执行成功了
2) OK
3) OK
127.0.0.1:6379> get k2
"v2"
127.0.0.1:6379> get k3
"v3"

redis实现乐观锁(watch)

正常执行成功

127.0.0.1:6379> set money 100  #设置金额为100元
OK
127.0.0.1:6379> set out 0      #设置划出金额为0元
OK
127.0.0.1:6379> watch money    #监听money
OK
127.0.0.1:6379> multi          #开启事务
OK
127.0.0.1:6379> decrby money 10  #金额减10
QUEUED
127.0.0.1:6379> incrby out 10    #划出金额加10
QUEUED
127.0.0.1:6379> exec           #执行事务,事务执行成功。
1) (integer) 90
2) (integer) 10

事务在执行完成后,如果想要再次监听,需要重新设置。 

测试在事务执行过程中,另外一个线程去修改

127.0.0.1:6379> watch money          #监听money
OK
127.0.0.1:6379> multi                #开启事务
OK
127.0.0.1:6379> decrby money 10
QUEUED
127.0.0.1:6379> incrby out 10
QUEUED
127.0.0.1:6379> exec                 #执行之前,另外一个线程,修改了money,这个时候,就会导致事务执行失败
(nil)

 如果修改失败,解锁,并重新加锁,开启事务,执行事务即可

127.0.0.1:6379> unwatch         #先解锁
OK
127.0.0.1:6379> watch money     #在重新加速
OK
127.0.0.1:6379> multi           #重新开启事务
OK
127.0.0.1:6379> decrby money 10
QUEUED
127.0.0.1:6379> incrby out 10
QUEUED
127.0.0.1:6379> exec            #重新执行事务
1) (integer) 990
2) (integer) 20

Jedis

什么是Jedis

Jedis是官方推荐的java连接开发工具,使用Java操作redis的中间件。

通过jedis操作redis

1.导入依赖

<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>4.0.1</version>
        </dependency>

2. 连接redis

package com.zjb;

import redis.clients.jedis.Jedis;

public class TestPing {

    public static void main(String[] args) {
        Jedis jedis = new Jedis("127.0.0.1",6379);

        System.out.println(jedis.ping());
    }
}

 3.连接成功 

 

通过jedis操作事务

package com.zjb;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;

public class TestTX {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("127.0.0.1",6379);

        jedis.flushDB();
        //开启事务
        Transaction multi = jedis.multi();

        try {
            multi.set("k1","v1");
            multi.set("k2","v2");
            multi.exec(); //执行事务
        }catch (Exception e){
            multi.discard(); //取消事务
            e.printStackTrace();
        }finally {
            System.out.println(jedis.get("k1"));
            System.out.println(jedis.get("k2"));
            jedis.close(); //关闭连接
        }

    }
}

 


七、SpringBoot集成Redis

  1.引入依赖

        <dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>

 2.添加redis配置

spring.redis.host=127.0.0.1
spring.redis.port=6379

3.添加自定义配置类(这是一个固定的配置类,可以直接在企业的项目里边去使用)

package com.zjb.config;


import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * Redis配置类
 */
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);

        //Jackson序列化
        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();

        template.setKeySerializer(stringRedisSerializer);
        template.setHashKeySerializer(stringRedisSerializer);
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.setValueSerializer(jackson2JsonRedisSerializer);

        template.afterPropertiesSet();
        return template;
    }
}

4.测试 

package com.zjb.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

/**
* 必须序列化
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable{

    private String name;
    private int age;
}
package com.zjb;

import com.zjb.pojo.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;

@SpringBootTest
class Redis03SpringbootApplicationTests {

	@Autowired
	@Qualifier("redisTemplate")
	private RedisTemplate redisTemplate;

	@Test
	void contextLoads() {
		redisTemplate.opsForValue().set("user",new User("xiaohong",3));
		System.out.println(redisTemplate.opsForValue().get("user"));
	}

}

八、Redis配置文件详解

 include :支持引用多个配置文件

NETWORK:网络

bind 0.0.0.0  # 绑定的ip  远程连接时修改
protected-mode yes #保护模式
port 6379 #端口号

 GENERAL:通用

daemonize yes  #守护进程 默认no
pidfile /var/run/redis_6379.pid  #如果以后台的方式运行,我们就需要指定一个pid文件
loglevel notice   #日志级别 debug 、verbose 、notice(适用生产环境)、warning
logfile ""        #日志的文件位置名
databases 16      #数据库的数量 默认是16个数据库
always-show-logo yes  #是否总是显示logo

 SNAPSHOTTING:快照

#如果900s内,至少有一个key进行了修改,就进行持久化操作
save 900 1
#如果300s内,至少有10个key进行了修改,就进行持久化操作
save 300 10
#如果60s内,至少有10000个key进行了修改,就进行持久化操作
save 60 10000
#持久化如果出错redis是否继续工作
stop-writes-on-bgsave-error yes
#是否压缩rdb文件,需要消耗一些cpu资源
rdbcompression yes
#保存rdb文件时,进行错误检查校验
rdbchecksum yes
#rdb文件保存的目录
dir ./

 SECURITY:安全

# redis的密码 默认是空的
requirepass

 CLIENTS:客户端限制

#最大客户端连接数
maxclients 10000

 MEMORY MANAGEMENT:内存管理

#配置redis最大的内存容量
maxmemory <bytes>
#内存到达上限之后的处理策略,六种策略如下:
#1、volatile-lru:只对设置了过期时间的key进行LRU(默认值) 
#2、allkeys-lru : 删除lru算法的key   
#3、volatile-random:随机删除即将过期key   
#4、allkeys-random:随机删除   
#5、volatile-ttl : 删除即将过期的   
#6、noeviction : 永不过期,返回错误
maxmemory-policy noeviction

 APPEND ONLY MODE: AOF配置

#默认不开启aof模式,默认使用rdb方式进行持久化
appendonly no
#aof持久化文件的名字
appendfilename "appendonly.aof"
# 每次修改都会同步,消耗性能 
# appendfsync always
#每秒同步一次,可能会丢失这一秒的数据
appendfsync everysec
#不执行同步
# appendfsync no

九、Redis持久化

Redis是内存数据库,如果不将内存中的数据保存到磁盘中,那么一旦服务器进程退出,服务器中的数据也会消失。

RDB操作

 在指定的时间间隔内将内存中的数据集快照写入磁盘,恢复时将快照文件直接读取到内存里。

Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件,待持久化过程都结束了,在用这个临时文件替换上次持久化好的文件,整个过程中,主进程是不进行任何IO操作的。这就确保了极高的性能,如果需要进行大规模数据恢复,且对于数据恢复的完整性不是非常敏感,那么RDB方式要比AOF方式更加高效。RDB的缺点是最后一次持久化后的数据可能丢失。默认使用RDB方式。

RDB保存的文件是dump.rdb

触发机制

  • save规则满足的情况下,会自动触发
  • 执行flushall命令时,会触发
  • 退出redis时,也会触发

恢复rdb文件

   1.只需将rdb文件放在redis启动目录下就可以,redis启动的时候会自动检查dump.rdb文件恢复其中的数据

  2. 查看需要存放的位置

127.0.0.1:6379> config get dir
1) "dir"
2) "/usr/local/bin"  如果在这个目录下存在dump.rdb文件,启动时就会自动恢复文件中的数据

 优缺点

    优点:

  • 适合大规模的数据恢复
  • 对数据的完整性要求不高        

    缺点:

  • 需要一定的时间间隔,如果redis意外宕机了,最后一次修改的数据就没有了
  • fork进程的时候,会占用一定的内存空间   

AOF操作

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

AOF保存的是appendonly.aof文件

开启AOF模式

默认是不开启的,只需要修改配置文件即可

1.修改配置文件

#将no改为yes即可
appendonly no  

 2.重启服务

修复appendonly.aof文件

如果appendonly.aof被破坏了,在启动redis服务时就会报错,这时我们应该怎样修复appendonly.aof文件呢?

redis给我们提供了redis-check-aof工具,用来修复appendonly.aof文件。

redis-check-aof --fix appendonly.aof

 如果文件正常,重启就可以恢复了。

优缺点

  优点:

  • 每一次修改都同步,文件的完整性更加好。
  • 每秒同步一次,可能会丢失一秒的数据
  • 从不同步,效率最高

  缺点:

  • 相对于数据文件来说,aof远远大于rdb,修复的速度也比rdb慢。
  • aof运行效率也比rdb慢,因为aof是有io操作

十、Redis订阅发布

Redis发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。

Redis客户端可以订阅任意数量的频道。

订阅、发布消息图:

命令

  • PSUBSCRIBE :订阅一个或多个给定模式的频道
  • PUBLISH:将消息发送到指定的频道
  • PUBSUB:查看订阅与发布系统状态
  • PUNSUBSCRIBE:退订所有给定模式的频道
  • SUBSCRIBE:订阅一个或多个频道
  • UNSUBSCRIBE:退订指定的频道

测试

  订阅端

#订阅一个频道
127.0.0.1:6379> subscribe channel
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "channel"
3) (integer) 1
1) "message"
2) "channel"
3) "hello,redis pub/sub"

 发布端

#向频道中发送消息
127.0.0.1:6379> publish channel 'hello,redis pub/sub'
(integer) 1

十一、Redis主从复制

概念

主从复制,是指将一台redis服务器的数据,复制到其他的redis服务器。前者称为主节点(master/leader),后者被称为从节点(slave/follwer);数据的复制都是单向的,只能由主节点到从节点。master以写为主,Slave以读为主。

默认情况下,每台redis服务器都是主节点,且一个节点可以有多个从节点(或者没有从节点),但一个从节点只能有一个主节点。

主从复制的作用

  • 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
  • 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复,实际上一种服务的冗余。
  • 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务,分担服务器负载,尤其是在写少多读的场景下,通过多个节点分担读负载,可以大大提高redis服务器的并发量。
  • 高可用基石:除了上述作用之外,主从复制还是哨兵和集群能够实施的基础,因此主从复制是redis高可用的基础。

集群环境搭建

 只配置从节点,不用配置主节点。主机可以写,从机只能读不能写。

查看当前redis信息

127.0.0.1:6379> info replication  #当前redis信息
# Replication
role:master   #角色名称
connected_slaves:0  #从节点数量
master_replid:dc74c05af5dfd774beb611e085f0e31f6898bf03
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0

  配置从节点有两种方式

  1.命令:这种方式是暂时的

# 语法: slaveof host port
# 参数说明:
# host: 主节点的IP
# port: 主节点的端口号
slaveof 127.0.0.1 6379

 2.修改配置文件

 打开以下两个配的注释,并编辑主节点ip、端口号。

#配置主节点 ip 端口号
replicaof <masterip> <masterport>
# 当主节点设置的有密码时,配置主节点密码
masterauth <master-password>

 服务启动后该节点就是从节点。

主从复制之复制原理

Slave启动成功连接到master后会发送一个sync命令。

master接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,master将传送整个数据文件到slave,并完成一次完全同步。

全量复制:slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。

增量复制:master继续将新的所有收集到的修改命令依次传给slave,完成同步。

但只要是重新连接master,一次完全同步(全量复制)将会自动执行。

哨兵模式详解

概念

主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费时费力,还会造成一段时间内服务不可用。这不是种推荐的方式,更多时候,我们优先考虑哨兵模式。Redis从2.8开始正式提供了Sentinel(哨兵)架构来解决这个问题。

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

 哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,他会独立运行。其原理是哨兵通过发送命令,等待redis服务响应,从而监控运行的多个redis实例。

这里的哨兵有两个作用:

  • 通过发送命令,让redis服务器返回监控其运行状态,包括主服务器和从服务器。
  • 当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让他们切换主机。 

然而一个哨兵进程对redis服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。 

 假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上failover过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象称为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover[故障转移]操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线

配置哨兵

1.配置哨兵配置文件,sentinel.conf

#语法:sentinel monitor 被监控的主机名称 host port 1
# 后边这个数字1,代表主机挂了,slave投票看让谁接替成为主机,票数最多的,就会成为主机 
sentinel monitor master-redis 127.0.0.1 6379 1

 哨兵配置文件不止有这一个配置,但这个配置是最基本的。

2.启动哨兵

redis-sentinel redis-conf/sentinel.conf

如果主机挂掉,查看sentinel的日志,发现已经自动选取从节点为新的主节点。

 如果主机恢复了,只能归到新的主节点下,当做从机,这就是哨兵模式的规则。

优缺点

优点

  • 哨兵集群,基于主从复制模式,所有的主从配置优点,它都有
  • 主从可以切换,故障可以转移,系统的可用性就会更好
  • 哨兵模式就是主从模式的升级,手动到自动,更加健壮

缺点

  • redis不好在线扩容,集群容量一旦到达上限,在线扩容就十分麻烦。
  • 实现哨兵模式的配置很麻烦     

 哨兵模式的全部配置

#Example sentinel.conf
#哨兵sentinel实例运行的端口  默认是26379
port 26379

#哨兵sentinel的工作目录
dir /tmp
#哨兵sentinel监控的redis主节点的ip port
#master-name 可以自己命名主节点的名字 只能由字母A-z,数字0-9、这三个字符".-_"组成
#quorum配置多少个sentinel哨兵统一认为master主节点失联,那么这时客观上认为主节点失联了
#sentinel monitor <master-name> <ip> <redis-port> <quorum>
sentinel monitor mymaster 127.0.0.1 6379 2

#当在redis实例中开启了requiress foobared 授权密码 这样所有连接redis实例的客户端都要提供密码
#设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码
#sentinel auth-pass <master-name> <password>
sentinel auth-pass mymaster MySUPER--secret-0123passwOrd

#指定多少毫秒之后 主节点没有应答哨兵sentinel此时 哨兵主观上认为主节点下线 默认30秒
#sentinel down-after-milliseconds <master-name> <milliseconds>
sentinel down-after-milliseconds mymaster 30000

#这个配置指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行同步
#这个数字越小,完成failover所需的时间就越长
#如果这个数字越大,就意味着越多slave因为replication而不可用
#可以通过将这个值设为1   来保证每次只有一个slave处于不能处理命令请求的状态
#sentinel paraller-syncs <master-name> <numslaves>
sentinel paraller-syncs mymaster 1

#故障转移的超时时间 failover-timeout 可以用在以下这些方面
#1.同一个sentinel对同一个master两次failover之间的间隔时间
#2.当一个slave从一个错误的master那里同步数据开始计算时间。直到slave被纠正为向正确的master那里同步数据时。
#3.当想要取消一个正在进行的failover所需要的时间
#4.当进行failover时,配置所有slaves指向新的master所需的最大时间。不过,即使过了这个超时,slaves依然会被正确配置为指向master,但是就不按paraller-syncs所配置的规则来了
#默认三分钟
#sentinel failover-timeout <master-name> <milliseconds>
sentinel failover-timeout mymaster 180000


#配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知相关人员
#对于脚本的运行结果有以下规则:
#若脚本执行后返回1 ,那么该脚本稍后将会被再次执行,重复次数目前默认为10
#若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行
#如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同
#一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个sigkill信号终止,之后重新执行
#通知脚本
#sentinel notification-script <master-name> <script-path>
sentinel notification-script mymaster /var/redis/notify.sh



#客户端重新配置主节点参数脚本
#当一个master由于failover而发生改变时,这个脚本将会被调用,通知相关的客户端关于master地址已经发生改变的信息
#以下参数将会在调用脚本时传给脚本:
#<master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port>
#目前<state>总是failover
#<role>是leader或者observer中的一个
#参数from-ip from-port to-ip to-port 是用来和旧的master和新的master通信的
#这个脚本应该是通用的,能被多次调用,不是针对性的。
#sentinel client-reconfig-script <master-name> <script-path>
sentinel client-reconfig-script mymaster /var/redis/reconfig.sh

 十二、缓存穿透、雪崩

Redis缓存的使用,极大的提升了应用程序的性能和效率,特别是数据库方面。但同时,他也带来了一些问题。其中,最要害的问题就是数据的一致性问题 。从严格意义上讲,这个问题无解。如果对数据的一致性要求很高,那么就不能使用缓存。

缓存穿透

概念

缓存穿透的概念很简单,用户想要查询一个数据,发现redis内存数据数据库没有,也就是缓存未命中,于是向持久层数据库查询,发现也没有,于是本次查询失败,当用户很多的时候,缓存都没有命中,于是都去请求持久层数据库,这会给持久层数据库造成很大的压力,这时候相当于出现了缓存穿透。

解决方案

布隆过滤器

布隆过滤器是一种数据结构,对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃,从而避免了对底层数据库的查询压力。

缓存空对象 

当存储层未命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后在访问这个数据会从缓存中获取,保护了后端数据源。

但是这种方法会存在两个问题:

  • 如果空值能够被缓存起来,这就意味着缓存需要更多的空间,存储更多的键,因为这当中可能会有很多空值的键
  • 即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响

缓存击穿

概念

这里需要注意和缓存穿透的区别,缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像一个屏障凿开了一个大洞。

当某个key在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来查询最新数据,并且返回写缓存,会导致数据库瞬间压力过大。

解决方案

设置热点数据永不过期

从缓存层面看,没有设置过期时间,所以不会出现热点key过期后产生的问题。

加互斥锁

分布式锁:使用分布式锁,保证对于每个key同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可。这种方式将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大。

缓存雪崩

概念

缓存雪崩是指在某一个时间段,缓存集中过期失效。或者Redis集群宕机。

产生雪崩的原因之一,比如在写文本的时候,马上到双十二零点,很快就会迎来一波抢购,这波商品时间比较集中的放入了缓存,假设缓存一个小时,那么到凌晨一点钟的时候,这批商品的缓存都快过期了,而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。于是所有请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。

 其实集中过期,倒不是非常致命,比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网,因为自然形成的缓存雪崩 ,一定是在某个时间段集中创建缓存,这个时候,数据库也是可以顶住压力的,无非就是对数据库产生周期性的压力而已。而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知的,很有可能瞬间就把数据库压垮。

解决方案

redis高可用

这个思想的含义是,既然redis有可能挂掉,那我就多设几台,这样一台挂掉之后其它的还可以继续工作,其实就是搭建的集群。

限流降级

这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其它线程等待。

数据预热

数据加热的含义就是在正式部署之前,先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中,在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。

这篇关于Redis详解的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!