学会Redis缓存中间件,这一篇就够了

Redis

文章涉及的资源文件都可以在文末自行获取。

一、什么是Nosql

Nosql=Not only sql 泛指非关系型数据库

Nosql特点

解耦

  1. 方便扩展(数据间没有关联)
  2. 大数据量高性能(redis一秒写8万次,读取11万,细粒度缓存)
  3. 数据类型是多样型的(不需要事先设计数据库,随取随用)
  4. 传统的关系型数据库和Nosql

传统的关系型数据库

  • 结构化组织
  • sql
  • 数据和关系都存在单独的表中
  • 数据定义语言
  • 严格的一致性
  • 基础的事务

Nosql

  • 没有固定的查询语言
  • 键值对存储、列存储、文档存储、图形数据库
  • 最终一致性
  • CAP 和 BASE (异地多活)
  • 高性能、高可用、高扩展

二、Redis入门

概述

Redis(Remote Dictionary Server) 是一个使用 C 语言编写的,开源的(BSD许可)高性能非关系型(NoSQL)的键值对数据库。

Redis 可以存储键和五种不同类型的值之间的映射。的类型只能为字符串支持五种数据类型:字符串、列表、集合、散列表、有序集合

是一个缓存中间件,与传统数据库不同的是 Redis 的数据是存在内存中的,所以读写速度非常快,因此 redis 被广泛应用于缓存方向,每秒可以处理超过 10万次读写操作,是已知性能最快的Key-Value DB。另外,Redis 也经常用来做分布式锁。除此之外,Redis 支持事务 、持久化、LUA脚本、LRU驱动事件、多种集群方案。

Redis能干嘛?

  1. 内存存储、持久化,内存是断电即失
  2. 效率高,可以用于告诉缓存
  3. 发布订阅系统
  4. 地图信息分析
  5. 计时器,计数器(浏览量)

特性

  1. 多样的数据类型
  2. 持久化
  3. 集群
  4. 事务

Linux安装

  1. 下载安装包,redis官网:https://redis.io/download/

在这里插入图片描述

  1. 安装目录:/opt/

在这里插入图片描述

  1. 解压安装包:

    tar -zxvf redis-7.0.4.tar.gz

在这里插入图片描述

  1. 环境安装:

    进入解压后的文件夹,运行命令:yum install gcc-c++ 因为redis是基于c语言,需要安装c语言环境。

在这里插入图片描述

1.yum install gcc-c++ 环境安装

2.make加载文件

3.make install 加载文件到默认的安装目录下:/usr/local/bin

在这里插入图片描述

在这里插入图片描述

  1. Redis默认的安装路径:/usr/local/bin

在这里插入图片描述

  1. 将redis的配置文件redis.conf复制一份到安装目录下的myconfig下,保证安全,不破坏redis原生文件:

    cp /opt/redis-7.0.4/redis.conf myconfig

  2. Redis默认不是后台启动,需要修改配置文件redis.conf,建议后台启动:
    在这里插入图片描述

  3. 启动Redis服务

在bin目录启动服务,/usr/local/bin

redis-server myconfig/redis.conf

redis-cli -p 6379

ping

在这里插入图片描述

测试:

在这里插入图片描述

  1. 关闭Redis连接:shutdown

在这里插入图片描述

  1. 查看redis进程是否存在:ps -ef|grep redis
    在这里插入图片描述

测试性能

redis-benchmark是一个压力测试工具

在这里插入图片描述

在这里插入图片描述

三、基础知识

redis默认有16个数据库,默认使用的是第[0]个数据库。

使用select进行切换数据库:

在这里插入图片描述

127.0.0.1:6379> SELECT 3 #切换数据库
OK
127.0.0.1:6379[3]> DBSIZE #查看数据库大小
(integer) 0
127.0.0.1:6379[3]> 
127.0.0.1:6379[3]>  keys * #查看所有的key
(empty array)
127.0.0.1:6379[3]> 
127.0.0.1:6379[3]> FLUSHALL #清除所有数据库内容
OK
127.0.0.1:6379[3]> FLUShDB #清除当前数据库
OK
127.0.0.1:6379[3]> 

Redis是单线程的

Redis是基于内存操作,CPU不是Redis性能的瓶颈,Redis的性能瓶颈是机器的内存和网络带宽,既然可以使用单线程来实现,就使用单线程了。

Redis为什么单线程还这么快?

误区一:高性能的服务器一定是多线程的?

误区二:多线程一定比单线程高?(多线程会产生CPU上下文切换:这是一个耗时的操作)

核心:Redis是将所有数据全部存储到内存中的 ,所以说使用单线程去操作效率会高很多。

五大数据类型

Redis-key
127.0.0.1:6379> keys * #查询所有数据库信息
(empty array)
127.0.0.1:6379> set name messi #设置一个name值
OK
127.0.0.1:6379> set age 36 #设置一个age值
OK
127.0.0.1:6379> keys *
1) "age"
2) "name"
127.0.0.1:6379> EXISTS age #是否存在age
(integer) 1
127.0.0.1:6379> TYPE name #type查看key是什么类型的
string
127.0.0.1:6379> move age 1 #移除age,key=age,数据库db=1
(integer) 1
127.0.0.1:6379> EXPIRE name 10 #设置name过期事件 10秒后消失
(integer) 1
127.0.0.1:6379> ttl name #查看倒数
(integer) 4
127.0.0.1:6379> ttl name
(integer) 1
127.0.0.1:6379> keys *
(empty array)

String(字符串)

String是redis最基本的类型,一个key对应一个value。

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

一个Redis中字符串value最多可以是512M。

#追加字符串 APPEND
127.0.0.1:6379> set key1 v1
OK
127.0.0.1:6379> get key1
"v1"
127.0.0.1:6379> APPEND key1 "funny" #追加一个字符串类型
(integer) 7
127.0.0.1:6379> get key1
"v1funny"
127.0.0.1:6379> STRLEN key1 #截取键的长度
(integer) 7

#i++ incr ; i-- decr
#步长
127.0.0.1:6379> set view 0 #初始值为0
OK
127.0.0.1:6379> get view
"0"
127.0.0.1:6379> incr view #自增1
(integer) 1
127.0.0.1:6379> incr view
(integer) 2
127.0.0.1:6379> decr view #自减1
(integer) 1
127.0.0.1:6379> decr view
(integer) 0
127.0.0.1:6379> decr view
(integer) -1
127.0.0.1:6379> INCRBY view 10 #设置步长,指定增量
(integer) 9
127.0.0.1:6379> get view
"9"
127.0.0.1:6379> 
#字符串范围 range
127.0.0.1:6379> set name "hello,redis!"
OK
127.0.0.1:6379> GETRANGE name 0 4 #查看范围 第0个到第4个字符串
"hello"
127.0.0.1:6379> GETRANGE name 0 -1 #查看范围 查看所有的字符串
"hello,redis!"
127.0.0.1:6379> 

# 替换 SETRANGE
127.0.0.1:6379> set key2 "abcdefg123"
OK
127.0.0.1:6379> SETRANGE key2 1 123 #把第[1]位置的值替换成123
(integer) 10
127.0.0.1:6379> get key2
"a123efg123"
127.0.0.1:6379> 
# setex 设置过期时间
# setnx 不存在再设置 (在分布式锁中会常常使用)
127.0.0.1:6379> setex key3 10 "woshikey3" # 设置key3的值10秒后消失
OK
127.0.0.1:6379> ttl key3
(integer) -2
127.0.0.1:6379> keys *
1) "key2"
2) "name"
127.0.0.1:6379> setnx key3 "123" #此时已经不存在key3,不存在就设置值
(integer) 1 #成功返回1
127.0.0.1:6379> get key3
"123"
127.0.0.1:6379> setnx key3 "456" #此时已经存在有key3了,再设置不会覆盖原来的值
(integer) 0
127.0.0.1:6379> get key3
"123"
127.0.0.1:6379> 
# mset 批量设置值
# mget 批量获取值

127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3
OK
127.0.0.1:6379> keys *
1) "k2"
2) "k1"
3) "k3"
127.0.0.1:6379> mget k1 k2 k3
1) "v1"
2) "v2"
3) "v3"
127.0.0.1:6379> msetnx k1 12 k4 v4 #此时k1已经存在,k4不存在,msetnx是一个原子性操作,要么一起成功要么一起失败。
(integer) 0
127.0.0.1:6379> get k4
(nil)
# 创建对象 user:{id}:{filed}
127.0.0.1:6379> mset user:1:name luoxiang user:1:age 34 #把user:1:name看作key 用空格隔开后面跟着属性
OK
127.0.0.1:6379> mget user:1:name user:1:age 
1) "luoxiang"
2) "34"
127.0.0.1:6379> 
# getset 先get再set

127.0.0.1:6379> getset DB mysql #先获取DB,但DB不存在,返回空,再设置值
(nil)
127.0.0.1:6379> get DB #获取set的值
"mysql"
127.0.0.1:6379> getset DB reids #先获取DB的值,返回上一个值,再设置值
"mysql"
127.0.0.1:6379> get DB #获取新set的值
"reids"
List(列表)

所有的list命令都是l开头的;底层就是一个双向链表

# 添加,查看列表的值 LPUSH LRANGE 
127.0.0.1:6379> LPUSH list one #给list 设置值(头部,左)
(integer) 1
127.0.0.1:6379> LPUSH list two
(integer) 2
127.0.0.1:6379> LPUSH list three
(integer) 3
127.0.0.1:6379> LRANGE list 0 -1 #查看list中所有的值
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> RPUSH list four #给list 设置值(尾部,右)
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "one"
4) "four"
# 移除列表元素 
127.0.0.1:6379> LPOP list 1 # 移除列表一个元素(L:左)
1) "three"
127.0.0.1:6379> LPOP list 2 # 移除列表两个元素(L:左)
1) "two"
2) "one"
127.0.0.1:6379> rpop list  # 移除列表元素(R:右)
"four"
# 通过下标获取对应的值 Lindex
127.0.0.1:6379> lrange list 0 -1
1) "winter"
2) "autumn"
3) "summer"
4) "spring"
127.0.0.1:6379> LINDEX list 0 # 获取第[0]个列表的值
"winter"
# Llen 获取长度
127.0.0.1:6379> llen list
(integer) 4
# 移除指定的值 LREM
127.0.0.1:6379> LREM list 1 winter #移除list中的 一个元素 winter
(integer) 1
127.0.0.1:6379> LRANGE list 0 -1 
1) "autumn"
2) "summer"
3) "spring"
# ltrim 修剪 
127.0.0.1:6379> LRANGE list 0 -1
1) "winter"
2) "autumn"
3) "summer"
4) "spring"
127.0.0.1:6379> LTRIM list 1 2  #截取list中第[1]到[2]的元素,之保留[1,2]的值
OK
127.0.0.1:6379> LRANGE list 0 -1
1) "autumn"
2) "summer"
# 移除最后一个元素,将它移动到另一个集合 rpoplpush
127.0.0.1:6379> LRANGE list 0 -1
1) "winter"
2) "autumn"
3) "summer"
4) "spring"
127.0.0.1:6379> rpoplpush list otherlist # 移除list中的尾部元素,剪切到otherlist中
"spring"
127.0.0.1:6379> LRANGE list 0 -1
1) "winter"
2) "autumn"
3) "summer"
127.0.0.1:6379> LRANGE otherlist 0 -1
1) "spring"

# lset 相当于更新操作,替换列表指定下标的值
127.0.0.1:6379> lset list 0 season # 替换列表list指定下标[0]的值为season
OK
127.0.0.1:6379> LRANGE list 0 -1
1) "season"
2) "autumn"
3) "summer"
127.0.0.1:6379> 

# Linsert 在列表前面或后面插入指定的值
127.0.0.1:6379> LRANGE list 0 -1
1) "season"
2) "autumn"
3) "summer"
127.0.0.1:6379> LINSERT list before season dailog #在list列表中,在season元素 之前 插入元素 dailog 
(integer) 4
127.0.0.1:6379> LRANGE list 0 -1
1) "dailog"
2) "season"
3) "autumn"
4) "summer"
127.0.0.1:6379> LINSERT list after season afterdailog #在list列表中,在season元素 之后 插入元素 afterdailog 
(integer) 5
127.0.0.1:6379> LRANGE list 0 -1
1) "dailog"
2) "season"
3) "afterdailog"
4) "autumn"
5) "summer"
127.0.0.1:6379> 

理解:list类型相当于一个链表,可以在表头表尾进行插入、修改和删除等操作;对两端操作性很高,中间节点性能较差。

Set(集合)

Set最大的特点就是:可以自动排重;Set中的值是不能重复的;Set命令都是以S开头的。

# 添加、查看集合中的元素 sadd;SMEMBERS
127.0.0.1:6379> sadd myset hello redis set #在集合中添加元素:hello redis set,无序添加
(integer) 3
127.0.0.1:6379> SMEMBERS myset # 查看集合中的元素
1) "set"
2) "hello"
3) "redis"
127.0.0.1:6379> SISMEMBER myset hello #判断该元素是否存在于集合
(integer) 1 #返回1,表示正确
127.0.0.1:6379> SISMEMBER myset nihao
(integer) 0 #返回0,表示错误
127.0.0.1:6379> SADD myset hello # 添加重复的元素,返回失败
(integer) 0

# 获取集合元素的个数 scard
127.0.0.1:6379> SCARD myset # 获取集合元素的个数
(integer) 3
127.0.0.1:6379> 
# 移除集合中的元素 srem
127.0.0.1:6379> srem myset set #移除集合中的元素set
(integer) 1
127.0.0.1:6379> SMEMBERS myset
1) "hello"
2) "redis"
127.0.0.1:6379> 
# 获取随机数 SRANDMEMBER
127.0.0.1:6379> SMEMBERS myset
 1) "redis"
 2) "the"
 3) "first"
 4) "i"
 5) "hello"
 6) "is"
 7) "today"
 8) "redis!"
 9) "learn"
10) "day"
127.0.0.1:6379> SRANDMEMBER myset #获取随机数
"is"
127.0.0.1:6379> SRANDMEMBER myset #获取随机数
"today"
127.0.0.1:6379> SRANDMEMBER myset 2  #获取随机数 后面可以加上想要获取的个数
1) "is"
2) "learn"
127.0.0.1:6379> 
#删除指定的key srem
#随机删除key spop
127.0.0.1:6379> SPOP myset #随机移除一个key 
"is 
127.0.0.1:6379> SPOP myset 2 #随机移除多个key 
1) "today"
2) "redis!"
127.0.0.1:6379> SMEMBERS myset
1) "redis"
2) "first"
3) "i"
4) "learn"
5) "day"
6) "the"
7) "hello"
------------------------------------------------------------------------
127.0.0.1:6379> SMEMBERS myset 
1) "redis"
2) "first"
3) "i"
4) "day"
5) "the"
6) "hello"
127.0.0.1:6379> srem myset i #删除myset集合中指定的元素i
(integer) 1
127.0.0.1:6379> SMEMBERS myset
1) "redis"
2) "first"
3) "day"
4) "the"
5) "hello"
# 剪切元素 SMOVE,将一个集合的元素移动到另一个集合 (集合必须存在)
127.0.0.1:6379> SMEMBERS myset2 #必须存在集合myset2
1) "member2"
127.0.0.1:6379> SMEMBERS myset
1) "first"
2) "i"
3) "fly"
4) "can"
5) "believe"
6) "day"
7) "hello"
127.0.0.1:6379> SMOVE myset myset2 believe #将myset集合中的 believe 剪切到myset2中
(integer) 1
127.0.0.1:6379> SMEMBERS myset
1) "first"
2) "i"
3) "fly"
4) "can"
5) "day"
6) "hello"
127.0.0.1:6379> SMEMBERS myset2
1) "believe"
2) "member2"
# 交集 SINTER 查看共同元素
# 差集 SDIFF 
# 并集 SUNION
------------------------------------------------------------------------
# 交集
127.0.0.1:6379> SINTER set1 set2 #交集 以set1内的元素为参照,对照set2的元素,查找共同元素
1) "c"
------------------------------------------------------------------------
# 差集 
127.0.0.1:6379> sadd set1 a b c
(integer) 3
127.0.0.1:6379> SADD set2 c d e #set1 set2拥有共同的元素c
(integer) 3
127.0.0.1:6379> SDIFF set1 set2 #差集 以set1内的元素为参照,对照set2的元素,查找差集
1) "a"
2) "b"
------------------------------------------------------------------------
# 并集
127.0.0.1:6379> SUNION set1 set2
1) "d"
2) "c"
3) "a"
4) "b"
5) "e"
Hash(哈希)

Redis-Hash是一个键值对集合。是一个string类型的fieldvalue的映射表,适用于存储对象。

类似java中的Map<String,Object>

同样,hash命令都是以H开头的。

## 添加、获取map中的元素 Hset Hget Hgetall
127.0.0.1:6379> hset myhash name zhangsan age 33 #在myhash中设置了name:zhangsan age:33的对象
(integer) 1
127.0.0.1:6379> hget myhash name #通过hget取出对应的值
"zhangsan"
127.0.0.1:6379> hget myhash age
"33"
127.0.0.1:6379> HGETALL myhash #Hgetall将key中的所有哈希值获取出来 以k-v键值对的方式获取
1) "name"
2) "zhangsan"
3) "age"
4) "33"
# 删除hash指定的key字段
127.0.0.1:6379> HDEL myhash name #删除指定的name字段 对应的value值也随之删除
(integer) 1
127.0.0.1:6379> hgetall myhash
1) "age"
2) "33"
# 查看hash长度 hlen
127.0.0.1:6379> hgetall myhash
1) "age"
2) "33"
3) "name"
4) "fawaikuangtu"
127.0.0.1:6379> hlen myhash
(integer) 2 #代表存在两个键值对
# 判断hash指定字段是否存在(可和其他类型类比)HEXISTS
127.0.0.1:6379> hgetall myhash
1) "age"
2) "33"
3) "name"
4) "fawaikuangtu"
127.0.0.1:6379> HEXISTS myhash name
(integer) 1
127.0.0.1:6379> HEXISTS myhash age
(integer) 1
127.0.0.1:6379> HEXISTS myhash address 
(integer) 0
# 只获取所有的filed hkeys
# 只获取所有的value HVALS
127.0.0.1:6379> hkeys myhash
1) "age"
2) "name"
127.0.0.1:6379> HVALS myhash
1) "33"
2) "fawaikuangtu"
# 自增 hincr
# 自减 
127.0.0.1:6379> hget myhash age
"33"
127.0.0.1:6379> HINCRBY myhash age 3 # 自增3
(integer) 36
127.0.0.1:6379> HINCRBY myhash age -6 # 自减6
(integer) 30
# 存在设置值 hsetnx(类比string类型)
127.0.0.1:6379> hsetnx myhash address beijing #如果不存在address,可以设置
(integer) 1
127.0.0.1:6379> hsetnx myhash address tianjin #如果存在address,设置失败
(integer) 0
Zset(有序集合)

和通用集合set非常相似,是一个没有重复元素的字符串集合,不同之处是Zset的每个成员关联了一个评分(score),这个score被用来从低分到最高分的方式排序集合中的成员。

集合的成员是唯一的,但是评分可以是重复的。

# 添加元素,查看元素 zadd; ZRANGE
127.0.0.1:6379> zadd myzset 1 chinese
(integer) 1
127.0.0.1:6379> zadd myzset 2 english
(integer) 1
127.0.0.1:6379> zadd myzset 2 math #此处的math 和 english 评分是同级的
(integer) 1
127.0.0.1:6379> ZADD myzset 3 Chemistry 4 Geography #添加多个元素
(integer) 2
127.0.0.1:6379> ZRANGE myzset 0 -1 # 按照设置的评分降序
1) "chinese"
2) "english"
3) "math"
4) "Chemistry"
5) "Geography"
# 有序排序 ZRANGEBYSCORE 从小到大
# 有序排序 ZREVRANGE 从大到小
127.0.0.1:6379> ZADD salary 3000 zhangsan #先设置三个对象
(integer) 1
127.0.0.1:6379> ZADD salary 3500 lisi
(integer) 1
127.0.0.1:6379> ZADD salary 9000 mest
(integer) 1
127.0.0.1:6379> ZRANGEBYSCORE salary -inf +inf #按照salary 从最小值到最大值排序
1) "zhangsan"
2) "lisi"
3) "mest"
127.0.0.1:6379> ZREVRANGE salary 0 -1 #按照salary 从大到小排序
1) "mest"
2) "zhangsan"
127.0.0.1:6379> ZRANGEBYSCORE salary -inf +inf withscores #withscores 排序时 将携带对应的scores
1) "zhangsan"
2) "3000"
3) "lisi"
4) "3500"
5) "mest"
6) "9000"
127.0.0.1:6379> ZRANGEBYSCORE salary 3500 +inf withscores # 排序条件: [3500,+无穷),过滤掉3500以下的值
1) "lisi"
2) "3500"
3) "mest"
4) "9000"
#移除元素 Zrem
127.0.0.1:6379> ZRANGE salary 0 -1
1) "zhangsan"
2) "lisi"
3) "mest"
127.0.0.1:6379> ZREM salary lisi #移除lisi
(integer) 1
127.0.0.1:6379> ZRANGE salary 0 -1
1) "zhangsan"
2) "mest"
# 获取指定区间的成员数量 zcount
127.0.0.1:6379> zrange myzset 0 -1
1) "chinese" #1
2) "english" #2
3) "math" #2
4) "Chemistry" #3
5) "Geography" #4
127.0.0.1:6379> zcount myzset 1 3 # (1,3)之间的成员个数
(integer) 4
#返回给定元素对应的score ZSCORE
127.0.0.1:6379> ZSCORE myzset chinese
"1"
# 返回集合中元素的个数 zcard
127.0.0.1:6379> zcard myzset
(integer) 5

工作实践中需要可以查询官方文档

三种特殊数据类型

geospatial 地理位置

运用:定位、附近的人、打车

相关命令:


geoadd 添加地理位置

# geoadd 添加地理位置 
# key 值(经度、纬度、名称)
# 有效的经度-180度到180度
# 有效的维度-85.05112878度到85.05112878度
127.0.0.1:6379> geoadd china:city 104.06 30.65 chengdu
(integer) 1
127.0.0.1:6379> geoadd china:city 108.94 34.26 xian
(integer) 1
127.0.0.1:6379> geoadd china:city 106.50 29.53 chongqin
(integer) 1
127.0.0.1:6379> geoadd china:city 114.29 30.58 wuhan
(integer) 1
127.0.0.1:6379> geoadd china:city 102.71 25.04 kunmin
(integer) 1

geodis 返回两地间的距离

# geodis 返回两地间的距离
127.0.0.1:6379> GEODIST china:city chengdu wuhan #计算成都和武汉两地的 直线 距离
"978932.5162"
127.0.0.1:6379> GEODIST china:city chengdu wuhan km #设置距离单位km
"978.9325"

geohash 表示的并不是一个点,而是一个矩形区域。

GeoHash是一种对地理坐标进行编码的方法,它将二维坐标映射为一个字符串。每个字符串代表一个特定的矩形,在该矩形范围内的所有坐标都共用这个字符串。字符串越长精度越高,对应的矩形范围越小。使用者可以发布地址编码,既能表明自己大致位置,又不至于暴露自己的精确坐标,有助于隐私保护。

# geohash
127.0.0.1:6379> geohash china:city chengdu #比如chengdu所在的经纬度映射成编码为: wm3yrgwjjt0
1) "wm3yrgwjjt0"

geopos 查询经纬度

# geopos 查询经纬度
127.0.0.1:6379> GEOPOS china:city chengdu wuhan # 获取指定值的经纬度
1) 1) "104.05999749898910522"
   2) "30.6499990746355806"
   
2) 1) "114.29000169038772583"
   2) "30.58000021509926825"

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

# georadius 附近的人
127.0.0.1:6379> GEORADIUS china:city 114 29 1000 km #以114 29的位置为圆心,作1000km的半径搜寻
1) "wuhan"
2) "chengdu"
3) "chongqin"
4) "xian"
127.0.0.1:6379> GEORADIUS china:city 114 29 300 km
1) "wuhan"
127.0.0.1:6379> GEORADIUS china:city 114 29 1000 km count 2 # count 限制个数
1) "wuhan"
2) "chongqin"

GEORADIUSBYMEMBER 找出位于指定范围内的元素,中心点是由给定的额位置决定

# GEORADIUSBYMEMBER 找出指定位置周围的其他元素
127.0.0.1:6379> GEORADIUSBYMEMBER china:city chengdu 500 km # 以chengdu为中心,查找500昆明范围内的元素
1) "chengdu"
2) "chongqin"

geo的 底层实现原理就是Zset ,我们可以使用Zset命令来操作geo。

# 通过Zset命令查询和删除 zrange;zrem
127.0.0.1:6379> zrange china:city 0 -1 #查询所有china:city中的元素
1) "kunmin"
2) "chengdu"
3) "chongqin"
4) "xian"
5) "wuhan"
127.0.0.1:6379> zrem china:city kunmin # 移除china:city中的kunmin元素
(integer) 1
127.0.0.1:6379> zrange china:city 0 -1
1) "chengdu"
2) "chongqin"
3) "xian"
4) "wuhan"
Hyperloglog

什么是基数?

A={1,3,5,7,9,7}

基数(不重复的元素个数)A集合中不重复的元素为:{1,3,5,7,9},基数就得5。

简介

Hyperloglog 基数统计的算法,优点:占用内存是固定的,2^64不同元素的基数,只需要占用12kb的内存。

应用场景:网页UV(网页访问量,一个人多次访问同一个网站,计数仍旧记作一人)基数统计

0.81%错误率,统计UV任务 ,可以忽略不计

# 添加元素 查看元素 合并两个集合中的元素
# PFADD   PFCOUNT  PFMERGE
127.0.0.1:6379> PFADD myset a b c d e f g h i j k 
(integer) 1
127.0.0.1:6379> PFCOUNT myset
(integer) 11
127.0.0.1:6379> pfadd myset2 i j k l m n o p q
(integer) 1
127.0.0.1:6379> PFCOUNT myset2
(integer) 9
127.0.0.1:6379> PFMERGE myset3 myset myset2 # 把myset myset2中的元素合并到myset3
OK
127.0.0.1:6379> PFCOUNT myset3 #myset3自动去除了两个集合中重复的元素
(integer) 17

如果允许容错,就使用Hyperloglog

如果不允许容错,就使用set类型

Bitmap

位存储

应用场景:统计用户信息,活跃,不活跃;登录,未登录;打卡,未打卡;两个状态的都可以使用Bitmap。

Bitmap位图,都是操作二进制位来进行记录的,就只要0和1两种状态。

# 记录一周内的打卡情况 SETBIT GETBIT BITCOUNT
127.0.0.1:6379> SETBIT sign 1 1 #设置周一到周七的打卡情况 1为打卡,0为未打卡
(integer) 0
127.0.0.1:6379> SETBIT sign 2 1
(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 0
(integer) 0
127.0.0.1:6379> SETBIT sign 6 1
(integer) 0
127.0.0.1:6379> SETBIT sign 7 1
(integer) 0
127.0.0.1:6379> GETBIT sign 3 # 获取周三的打卡情况
(integer) 1
127.0.0.1:6379> GETBIT sign 5
(integer) 0
127.0.0.1:6379> BITCOUNT sign # 计数一周打卡了多少次
(integer) 5

四、事务

关系型Mysql事务:原子性(A)、一致性©、隔离性(I)、持久性(D)

Reids事务的本质:一组命令的集合,一个事务中的所有命令都会被序列化,在事务执行过程中按照顺序执行,一次性,排他性执行一系命令。

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

Redis事务没有隔离级别的概念

redis的事务:

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

正常执行事务

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

事务执行后,事务就会消失。

# 放弃事务 discard
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> DISCARD # 放弃事务
OK

编译型异常(代码有错,命令有错),事务中的所有命令都不会被执行。

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

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

127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> INCR k1 # 对k1自增1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> exec
1) (error) ERR value is not an integer or out of range # 因为k1类型是字符串,运行时出错
2) OK
3) OK
127.0.0.1:6379> get k3 # 其他命令正常被执行
"v3"

监控 watch

悲观锁:

很悲观,认为什么时候都会出问题,无论做什么都会加上锁。(影响性能)

乐观锁:

  • 很乐观,任务任何时候都不会出问题,所有不会上锁。更新数据的时候去判断一下,在此期间是否有人修改过这个数据。
  • 获取version
  • 更新时比较version

redis测试

正常执行:

127.0.0.1:6379> set money 100  #设置初始值
OK
127.0.0.1:6379> set out 0
OK
127.0.0.1:6379> watch money #监视money
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> DECRBY money 20 # 命令入队,对money减20
QUEUED
127.0.0.1:6379(TX)> INCRBY out 20 #对out加20
QUEUED
127.0.0.1:6379(TX)> exec #执行,返回结果
1) (integer) 80 # money的值
2) (integer) 20 # out的值

测试多线程修改值,使用watch可以当作redis的乐观锁

在这里插入图片描述

127.0.0.1:6379> watch money #监视money
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> DECRBY money 10
QUEUED
127.0.0.1:6379(TX)> INCRBY out 10
QUEUED
127.0.0.1:6379(TX)> exec #执行失败,执行之前,另一个线程修改了money的值
(nil)

如果发现事务执行失败,就先解锁,获取最新的值,再次监视

127.0.0.1:6379> UNWATCH #如果发现事务执行失败,就先解锁
OK
127.0.0.1:6379> WATCH money # 获取最新的值,再次监视,select version
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> DECRBY money 30
QUEUED
127.0.0.1:6379(TX)> INCRBY out 30
QUEUED
127.0.0.1:6379(TX)> exec # 对比监视的值是否发生变化,如果没有变化,那么执行成功,如果发生变化,执行失败
1) (integer) 170
2) (integer) 50

Jedis

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

  1. 创建一个java项目

  2. 导入jedis依赖

    <!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>4.2.3</version>
    </dependency>
    <!--        fastjson-->
    <dependency>
         <groupId>com.alibaba</groupId>
         <artifactId>fastjson</artifactId>
         <version>1.2.62</version>
    </dependency>
    
  3. 编码测试

    • 连接数据库

在这里插入图片描述

  • 操作命令

    package com.mest;
    import redis.clients.jedis.Jedis;
    
    public class TestPing {
        public static void main(String[] args) {
            //new jedis 对象
            Jedis jedis = new Jedis("127.0.0.1", 6379);
            String ping = jedis.ping();
            System.out.println(ping);
        }
    }
    

    输出:

在这里插入图片描述

  • 断开连接

    jedis.close();
    
常用API

在这里插入图片描述

所有的api命令就是上面的指令,可以类比。

事务API
package com.mest;

import com.alibaba.fastjson.JSONObject;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;

public class TXping {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("127.0.0.1", 6379);//开启连接
        
        jedis.flushDB();
        
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("name","luoxiang");
        jsonObject.put("age","30");
        //操作事务
        Transaction multi = jedis.multi(); //开启事务
        String s = jsonObject.toJSONString();

        try {
            multi.set("user1",s);
            multi.set("user2",s);
            multi.exec(); //执行事务
        } catch (Exception e) {
            multi.discard();//放弃事务
            e.printStackTrace();
        } finally {
            System.out.println(jedis.get("user2"));
            jedis.close();//关闭客户端
        }
    }
}

五、SpringBoot整合


在springboot 2.x以后,原来使用的jedis被替换成了lettuce。

jedis:采用的直连,多个线程操作的话,是不安全的,如果想避免不安全,使用jedis pool连接池,更像BIO模式。

lettuce:采用netty,实例可以再多个线程中进行共享,不存在线程不安全的情况,可以减少线程数据,更像NIO模式。

  1. 创建一个springboot项目,引入nosql中的redis依赖

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

    springboot所有的配置类,都有一个自动配置类,RedisAutoconfiguration

​ 自动配置类都会绑定一个properties配置文件 ,RedisProperties

源码:

    @Bean
    @ConditionalOnMissingBean(name = {"redisTemplate"})
	//如果不存在才会生效,我们可以自定义一个redisTemplate来替换这个默认的
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) 
        throws UnknownHostException {
        //默认的redisTemplate没有过多的设置,redis对象都需要序列化
        //两个泛型的都是<Object, Object>,我们后续使用需要强制转换<String, Object>
        RedisTemplate<Object, Object> template = new RedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

    @Bean
    @ConditionalOnMissingBean //String是redis中最常用的类型,所以单独使用一个bean
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) 
        throws UnknownHostException {
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }
  1. 配置连接

    #配置application.properties
    spring.redis.host=127.0.0.1
    spring.redis.port=6379
    
  2. 测试

    测试前,需要开启redis客户端

package com.mest;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisTemplate;

@SpringBootTest
class RedisSpringbootApplicationTests {

    @Autowired
    private RedisTemplate redisTemplate;//注入redisTemplate

    @Test
    void contextLoads() {
        
        //opsForValue() 操作字符串,类似String
        //opsForList() 操作list,类似list
        //opsForSet() 操作集合,类似set
        //...每一个操作都对应redis的数据类型

        //常用的方法比如:discard(),exec()等,可以通过redisTemplate直接调用

        //获取redis连接:redisTemplate.getConnectionFactory().getConnection()
//        RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
//        connection.flushDb();//对数据库进行操作

        redisTemplate.opsForValue().set("name","zhangsan");
        System.err.println(redisTemplate.opsForValue().get("name"));
    }
}

在这里插入图片描述

在这里插入图片描述

自定义redisTemplate


  1. 创建一个实体类

    可以通过implements Serializable实现序列化,但为了测试这里先不实现Serializable。

    在企业开发中,都需要我们在实体类中实现序列化implements Serializable

  2. 编写测试类,实现序列化

在这里插入图片描述

自定义序列化:

固定RedisTemplate模板,可以直接使用:

@Configuration
public class RedisConfig {
//自己定义的一个RedisTemplate
    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> myredisTemplate(RedisConnectionFactory factory) {
        //1.一般使用<String, Object>类型
        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
        //2.连接工厂
        template.setConnectionFactory(factory);
        //3.序列化配置
        //3.1 Jackson序列化
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new 					Jackson2JsonRedisSerializer(Object.class);//使用Jackson解析任意的对象
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        //3.2 String序列化
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        // key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value序列化方式采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的value序列化方式采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();

        return template;
    }
}
  1. 使用自定义的myredisTemplate

  2. 清除数据flushdb

  3. 重新测试

    @SpringBootTest
    class RedisSpringbootApplicationTests {
    
        @Autowired
        @Qualifier("myredisTemplate")//指定使用自己自定义的redisTemplate
        private RedisTemplate redisTemplate;
    
        @Test
        void test() throws JsonProcessingException {
            //真实开发一般使用json来传递对象
            User user = new User("mest",21);
            redisTemplate.opsForValue().set("user",valueAsString);
            System.err.println(redisTemplate.opsForValue().get("user"));
        }
    }
    

    控制台 输出:

在这里插入图片描述

​ 客户端查询结果:

在这里插入图片描述

RedisUtils工具类

封装RedisUtils工具类,避免使用原生api的方式编写代码,直接使用:

package com.mest.utils;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

@Component
public final class RedisUtil {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // =============================common============================
    /**
     * 指定缓存失效时间
     * @param key  键
     * @param time 时间(秒)
     */
    public boolean expire(String key, long time) {
        try {
            if (time > 0) {
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 根据key 获取过期时间
     * @param key 键 不能为null
     * @return 时间(秒) 返回0代表为永久有效
     */
    public long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }


    /**
     * 判断key是否存在
     * @param key 键
     * @return true 存在 false不存在
     */
    public boolean hasKey(String key) {
        try {
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 删除缓存
     * @param key 可以传一个值 或多个
     */
    @SuppressWarnings("unchecked")
    public void del(String... key) {
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                redisTemplate.delete(key[0]);
            } else {
                redisTemplate.delete(CollectionUtils.arrayToList(key));
            }
        }
    }


    // ============================String=============================

    /**
     * 普通缓存获取
     * @param key 键
     * @return 值
     */
    public Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }

    /**
     * 普通缓存放入
     * @param key   键
     * @param value 值
     * @return true成功 false失败
     */

    public boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 普通缓存放入并设置时间
     * @param key   键
     * @param value 值
     * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */

    public boolean set(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 递增
     * @param key   键
     * @param delta 要增加几(大于0)
     */
    public long incr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递增因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }


    /**
     * 递减
     * @param key   键
     * @param delta 要减少几(小于0)
     */
    public long decr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递减因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, -delta);
    }


    // ================================Map=================================

    /**
     * HashGet
     * @param key  键 不能为null
     * @param item 项 不能为null
     */
    public Object hget(String key, String item) {
        return redisTemplate.opsForHash().get(key, item);
    }

    /**
     * 获取hashKey对应的所有键值
     * @param key 键
     * @return 对应的多个键值
     */
    public Map<Object, Object> hmget(String key) {
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * HashSet
     * @param key 键
     * @param map 对应多个键值
     */
    public boolean hmset(String key, Map<String, Object> map) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * HashSet 并设置时间
     * @param key  键
     * @param map  对应多个键值
     * @param time 时间(秒)
     * @return true成功 false失败
     */
    public boolean hmset(String key, Map<String, Object> map, long time) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 向一张hash表中放入数据,如果不存在将创建
     *
     * @param key   键
     * @param item  项
     * @param value 值
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 向一张hash表中放入数据,如果不存在将创建
     *
     * @param key   键
     * @param item  项
     * @param value 值
     * @param time  时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value, long time) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 删除hash表中的值
     *
     * @param key  键 不能为null
     * @param item 项 可以使多个 不能为null
     */
    public void hdel(String key, Object... item) {
        redisTemplate.opsForHash().delete(key, item);
    }


    /**
     * 判断hash表中是否有该项的值
     *
     * @param key  键 不能为null
     * @param item 项 不能为null
     * @return true 存在 false不存在
     */
    public boolean hHasKey(String key, String item) {
        return redisTemplate.opsForHash().hasKey(key, item);
    }


    /**
     * hash递增 如果不存在,就会创建一个 并把新增后的值返回
     *
     * @param key  键
     * @param item 项
     * @param by   要增加几(大于0)
     */
    public double hincr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, by);
    }


    /**
     * hash递减
     *
     * @param key  键
     * @param item 项
     * @param by   要减少记(小于0)
     */
    public double hdecr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, -by);
    }


    // ============================set=============================

    /**
     * 根据key获取Set中的所有值
     * @param key 键
     */
    public Set<Object> sGet(String key) {
        try {
            return redisTemplate.opsForSet().members(key);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }


    /**
     * 根据value从一个set中查询,是否存在
     *
     * @param key   键
     * @param value 值
     * @return true 存在 false不存在
     */
    public boolean sHasKey(String key, Object value) {
        try {
            return redisTemplate.opsForSet().isMember(key, value);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 将数据放入set缓存
     *
     * @param key    键
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSet(String key, Object... values) {
        try {
            return redisTemplate.opsForSet().add(key, values);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 将set数据放入缓存
     *
     * @param key    键
     * @param time   时间(秒)
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSetAndTime(String key, long time, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().add(key, values);
            if (time > 0)
                expire(key, time);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 获取set缓存的长度
     *
     * @param key 键
     */
    public long sGetSetSize(String key) {
        try {
            return redisTemplate.opsForSet().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 移除值为value的
     *
     * @param key    键
     * @param values 值 可以是多个
     * @return 移除的个数
     */

    public long setRemove(String key, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().remove(key, values);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    // ===============================list=================================

    /**
     * 获取list缓存的内容
     *
     * @param key   键
     * @param start 开始
     * @param end   结束 0 到 -1代表所有值
     */
    public List<Object> lGet(String key, long start, long end) {
        try {
            return redisTemplate.opsForList().range(key, start, end);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }


    /**
     * 获取list缓存的长度
     *
     * @param key 键
     */
    public long lGetListSize(String key) {
        try {
            return redisTemplate.opsForList().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 通过索引 获取list中的值
     *
     * @param key   键
     * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
     */
    public Object lGetIndex(String key, long index) {
        try {
            return redisTemplate.opsForList().index(key, index);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }


    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     */
    public boolean lSet(String key, Object value) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 将list放入缓存
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     */
    public boolean lSet(String key, Object value, long time) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            if (time > 0)
                expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

    }


    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @return
     */
    public boolean lSet(String key, List<Object> value) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

    }


    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     * @return
     */
    public boolean lSet(String key, List<Object> value, long time) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            if (time > 0)
                expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 根据索引修改list中的某条数据
     *
     * @param key   键
     * @param index 索引
     * @param value 值
     * @return
     */

    public boolean lUpdateIndex(String key, long index, Object value) {
        try {
            redisTemplate.opsForList().set(key, index, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 移除N个值为value
     *
     * @param key   键
     * @param count 移除多少个
     * @param value 值
     * @return 移除的个数
     */

    public long lRemove(String key, long count, Object value) {
        try {
            Long remove = redisTemplate.opsForList().remove(key, count, value);
            return remove;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }

    }

}

RedisUtils工具类的使用

  1. 在测试类中注入RedisUtils工具类

        @Autowired
        private RedisUtil redisUtil;
    
  2. 编写测试类

  //通过调用RedisUtil使用api
    @Test
    void redisUtil(){
        redisUtil.set("CDUT","Chengdu University of Technology");
        redisUtil.set("SWPU","southwest petroleum university");
        System.err.println(redisUtil.get("CDUT"));
    }
  1. 输出结果:

在这里插入图片描述

六、Redis.conf详解


Redis.conf作为配置文件,用于启动redis服务。

网络

bind 127.0.0.1 #绑定的ip 连接远程服务器的时候,注释掉这行(推荐)或者指定ip
protected-mode no #保护模式 默认开启yes,连接远程服务器的时候需要修改为no 
port 6379 

GENERAL通用

daemonize yes #以守护进程方式开启,后台运行,默认是no

快照

持久化,在规定的时间内,执行了多少次,则会持久化到文件.rdb .aof

redis是内存数据库,如果没有持久化,那么数据断电即失

# Unless specified otherwise, by default Redis will save the DB:
#   * After 3600 seconds (an hour) if at least 1 change was performed
#   * After 300 seconds (5 minutes) if at least 100 changes were performed
#   * After 60 seconds if at least 10000 changes were performed

# 	save 3600 1 如果3600秒内,如果至少有1个key进行了修改,我们就进行持久化操作
		 300 100 #如果300秒内,如果至少有100个key进行了修改,我们就进行持久化操作
		 60 10000 #如果60秒内,如果至少有10000个key进行了修改,我们就进行持久化操作
		 
dir./ #rdb 文件保存目录(当前文件下)

SECURITY安全

默认是没有设置密码的

# 设置密码123456

# 在配置文件下设置
requirepass 123456 
# 命令行设置密码
config set requirepass 123456
#设置密码后操作指令会失去权限,需要输入密码
# 输入密码
auth 123456
# 获取密码
config get requirepass

限制 clients

 MAXMEMORY-POLICY noeviction #内存达到上限之后的处理策略
     1.volatile-lru: 只对设置过期时间的key进行lru(默认值)
     2.allkeys-lru: 删除lru算法的key
     3.volatile-random: 随机删除即将过期的key
     4.allkeys-random: 随机删除
     5.volatile-ttl: 删除即将过期的
     6.noeviction: 永不过期,返回错误

APPEND ONLY MODE AOF模式

appendonly no #默认不开启aof模式,默认使用rdb持久化,在大部分情况下,rdb完全够用。

Redis持久化


Redis是内存数据库,如果不将内存中的数据库状态保存到磁盘,那么一旦服务器进程退出,服务器中的数据状态也会消失,所以redis提供了持久化功能。

RDB (Redis DataBase)

什么是RDB

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

在这里插入图片描述

rdb保存的是dump.rdb文件


触发机制

  1. save的规则满足情况下,会自动触发rdb规则
  2. 执行flushall命令,会触发rdb规则
  3. 退出redis时,会触发rdb规则

触发后备份会生成一个dump.rdb文件在当前目录下


如何恢复rdb文件?

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

查看需要存在的位置

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

AOF(Append Only File)

以日志的形式记录每个写操作,将redis执行过的所有命令记录下来(读操作不记录),恢复的时候就把这个文件全部执行一遍。AOF 持久化功能则提供了一种更为可靠的持久化方式。 每当 Redis 接受到会修改数据集的命令时,就会把命令追加到 AOF 文件里,当你重启 Redis 时,AOF 文件里的命令会被重新执行一次,重建数据。换言之,redis重启的话就根据日志内容将写指令从头到尾执行一次以完成数据的恢复工作AOF保存的是appendonly.aof文件。

appendonly 默认是不开启的,需要手动配置,将配置文件里的appendonly 改为yes,就开启了aof。

重启后,redis就可以生效了。如果aof这个文件内部存在错误,redis启动就会失败,就需要我们修复这个文件。

redis提供了一个工具:redis-check-aof --fix,运行命令后,此时aof文件就会被自动修复,重启服务就可以生效了。

优点和缺点:

优点:

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

缺点:

  1. 相对于数据文件来说,aof的数据远远大于rdb,所以修复的速度也比rdb慢

  2. AOF运行效率也要比rdb慢,所以redis默认的持久化配置就是rdb

七、Redis发布订阅


Redis发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。Redis客户端可以订阅任意数量的频道。

订阅/发布消息图:

在这里插入图片描述

下图展示了频道 channel1 , 以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 之间的关系:

在这里插入图片描述

当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端:

在这里插入图片描述

下表列出了 redis 发布订阅常用命令:

序号命令及描述
1PSUBSCRIBE pattern 订阅一个或多个符合给定模式的频道。
2PUBSUB subcommand 查看订阅与发布系统状态。
3PUBLISH channel message 将信息发送到指定的频道。
4PUNSUBSCRIBE 退订所有给定模式的频道。
5SUBSCRIBE channel 订阅给定的一个或多个频道的信息。
6UNSUBSCRIBE 指退订给定的频道。

测试:
在这里插入图片描述

Redis主从复制


概念:

主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master/leader),后者称为从节点(slave/follower) ; 数据的复制是单向的,只能由主节点到从节点。Master以写为主,Slave以读为主。默认情况下,每台Redis服务器都是主节点 ;且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点

目的:

主从复制是为了达成高可用

  • 为了避免单点Redis服务器故障,准备多台服务器,互相连通。将数据复制多个副本保存在不同的服务器上,连接在一起,并保证数据是同步的。
  • 即使有其中一台服务器宕机,其他服务器依然可以继续提供服务,实现Redis的高可用,同时实现数据冗余备份。

需要解决的问题:数据同步

核心工作:master的数据复制到slave中

主从复制的作用:

  1. 读写分离:主节点写,从节点读,提高服务器的读写负载能力。

  2. 数据冗余︰主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。

  3. 故障恢复︰当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复 ; 实际上是一种服务的冗余。

  4. 负载均衡︰在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载 ; 尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。

  5. 高可用(集群)基石︰除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。

环境配置

只配从库,不用配置主库。

127.0.0.1:6379> info replication #查看当前库信息
# Replication
role:master #默认本机为master
connected_slaves:0 #从机数量为0
master_failover_state:no-failover
master_replid:db862742e131a16bfbbecfe2262468d41557e393
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. 打开三台终端用于测试主从

  2. 分别给三台从机分配 配置文件redis.conf

    • [root@localhost myconfig]# cp redis.conf redis80.conf # slaves1的配置文件
      [root@localhost myconfig]# cp redis.conf redis81.conf # slaves2的配置文件
      [root@localhost myconfig]# cp redis.conf redis82.conf # slaves3的配置文件
      [root@localhost myconfig]# ll
      总用量 540
      -rw-r--r--. 1 root root 106548 731 02:49 redis80.conf
      -rw-r--r--. 1 root root 106548 731 02:49 redis81.conf
      -rw-r--r--. 1 root root 106548 731 02:50 redis82.conf
      -rw-r--r--. 1 root root 106548 729 00:09 redis.conf # 主机的配置文件
      
    • 修改从机的配置文件,包含:

      • 端口:port 6380 port 6381 port 63782
      • 后台运行:daemonize yes
      • pidfile:/var/run/redis_6380.pid /var/run/redis_6381.pid /var/run/redis_6382.pid
      • logfile:"6380.log" "6381.log" "6382.log"
      • dbfilename:dump6380.rdb dump6381.rdb dump6382.rdb
  3. 分别开启服务

在这里插入图片描述

# 设置从机slaves
# SLAVEOF 127.0.0.1 
127.0.0.1:6380> SLAVEOF 127.0.0.1 6379 # SLAVEOF host 6379 找本地6379这台机器当老大(主机)
OK
127.0.0.1:6380> info replication
# Replication
role:slave
master_host:127.0.0.1 
master_port:6379 #可以看到自己的老大是谁
master_link_status:down
master_last_io_seconds_ago:-1
master_sync_in_progress:0
slave_read_repl_offset:0
slave_repl_offset:0
master_link_down_since_seconds:-1
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:b2b556042b9ec6650553178e801c043ae716e574
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
----------------------------------------------------------------------------------------------
# 在主机中查看信息
127.0.0.1:6379> INFO replication
# Replication
role:master
connected_slaves:3 #在主机下有三台从机
slave0:ip=127.0.0.1,port=6380,state=online,offset=28,lag=1 #从机的具体信息
slave1:ip=127.0.0.1,port=6381,state=online,offset=28,lag=0
slave2:ip=127.0.0.1,port=6382,state=online,offset=28,lag=1
master_failover_state:no-failover
master_replid:eeec78252a0a7b3701a240e5db61db69936e251c
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:28
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:28

真实的主从配置应该在配置文件中配置,这样的话是永久生效的,上面采用的是命令行配置,是可变的。

在这里插入图片描述

在主机中设置一个key:

127.0.0.1:6379> set k1 master-slaves
OK

在从机中获取这个key:

127.0.0.1:6380> get k1
"master-slaves"

从机只负责读取,不能写入:

127.0.0.1:6380> set k2 slaves-master
(error) READONLY You can't write against a read only replica.

注意:如果使用的命令行来设置的主从关系时,当从机断开后,会自动变回主机,想要继续获取主机的数据,需要重新绑定主从关系

复制原理

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

  • 全量复制:slave在接收到数据库文件数据后,将其存盘并加载到内存中。
  • 增量复制:master继续将新的所有收集的修改命令依次传给slave,完成同步。

只要连接到主机,就会自动执行一次完全同步!

宕机后手动配置主机

如果主机断掉后,从机可以使用slaveof no one来设置自己为主机,其他节点就可以手动去连接这个新的主机。

# SLAVEOF no one
127.0.0.1:6380> SLAVEOF no one
OK
127.0.0.1:6380> info replication
# Replication
role:master
connected_slaves:0
master_failover_state:no-failover
master_replid:07d79f122e06e987c443584c8ba201eca924cced
master_replid2:eeec78252a0a7b3701a240e5db61db69936e251c
master_repl_offset:2738
second_repl_offset:2739
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:15
repl_backlog_histlen:2724
哨兵模式

自动选取老大,解决手动设置主从关系。

概述

Sentinel(哨兵)是用于监控Redis集群中Master状态的工具,是Redis高可用解决方案,哨兵可以监视一个或者多个redis master服务,以及这些master服务的所有从服务。 某个master服务宕机后,会把这个master下的某个从服务升级为master替代已宕机的master继续工作。(即使后来之前的master重启服务,也不会变回master了,而是作为slave从服务)

工作原理

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

这里的哨兵有两个作用:

  1. 通过发送命令,让 Redis 服务器返回监控其运行状态,包括主节点和从节点;
  2. 当哨兵检测到 Master 宕机,会自动将 Slave 切换成 Master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。

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

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

开启哨兵模式

  1. 在bin目录下新建一个sentinel.conf文件,配置内容:

在这里插入图片描述

sentinel monitor mymaster 127.0.0.1 6379 1
# 其中mymaster为监控对象起的服务器名称, 1 为至少有多少个哨兵同意迁移的数量
  1. 启动哨兵服务redis-sentinel myconfig/sentinel.conf 使用bin目录下的redis-sentinel开启哨兵

    [root@localhost bin]# redis-sentinel myconfig/sentinel.conf 
    10106:X 31 Jul 2022 05:32:39.911 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
    10106:X 31 Jul 2022 05:32:39.911 # Redis version=7.0.4, bits=64, commit=00000000, modified=0, pid=10106, just started
    10106:X 31 Jul 2022 05:32:39.911 # Configuration loaded
    10106:X 31 Jul 2022 05:32:39.911 * Increased maximum number of open files to 10032 (it was originally set to 1024).
    10106:X 31 Jul 2022 05:32:39.911 * monotonic clock: POSIX clock_gettime
                    _._                                                  
               _.-``__ ''-._                                             
          _.-``    `.  `_.  ''-._           Redis 7.0.4 (00000000/0) 64 bit
      .-`` .-```.  ```\/    _.,_ ''-._                                  
     (    '      ,       .-`  | `,    )     Running in sentinel mode
     |`-._`-...-` __...-.``-._|'` _.-'|     Port: 26379
     |    `-._   `._    /     _.-'    |     PID: 10106
      `-._    `-._  `-./  _.-'    _.-'                                   
     |`-._`-._    `-.__.-'    _.-'_.-'|                                  
     |    `-._`-._        _.-'_.-'    |           https://redis.io       
      `-._    `-._`-.__.-'_.-'    _.-'                                   
     |`-._`-._    `-.__.-'    _.-'_.-'|                                  
     |    `-._`-._        _.-'_.-'    |                                  
      `-._    `-._`-.__.-'_.-'    _.-'                                   
          `-._    `-.__.-'    _.-'                                       
              `-._        _.-'                                           
                  `-.__.-'                                               
    
    10106:X 31 Jul 2022 05:32:39.912 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
    10106:X 31 Jul 2022 05:32:39.912 # Sentinel ID is fe41eed5c84c4daba13646f2000a77dc03d82793
    10106:X 31 Jul 2022 05:32:39.912 # +monitor master mymaster 127.0.0.1 6379 quorum 1
    10106:X 31 Jul 2022 05:32:39.924 * +slave slave 127.0.0.1:6382 127.0.0.1 6382 @ mymaster 127.0.0.1 6379
    10106:X 31 Jul 2022 05:32:39.939 * Sentinel new configuration saved on disk
    10106:X 31 Jul 2022 05:32:39.939 * +slave slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6379
    10106:X 31 Jul 2022 05:32:39.957 * Sentinel new configuration saved on disk
    10106:X 31 Jul 2022 05:32:39.957 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6379
    10106:X 31 Jul 2022 05:32:39.970 * Sentinel new configuration saved on disk
    

    如果主机断开了,这个时候就会在从机中随机选出一个主机。

    哨兵的优缺点

    优点:

    1. 哨兵模式基于主从复制,所有主从配置的优点和哨兵模式同等;
    2. 主从可以切换,故障转移,系统的可用性更好;
    3. 哨兵模式就是主从模式的升级,从手动到自动,更加健壮

    缺点:

    1. Redis 集群不好在线扩容,集群容量一旦到达上限,在线扩容很麻烦;
    2. 实现哨兵模式的配置很麻烦。

    哨兵模式的全部配置

    # 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> <port> <quorum>
    sentinel monitor mymaster 127.0.0.1 6379 2
    
    #当在redis实例中开启了requirepass 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>
    setinel down-after-milliseconds mymaster 30000
    
    # 指定在发生failover主备切换时最多可以有多少个slave同时对新的master进行同步,这个数字越小,完成failover所需的时间越长
    # 这个数字越大,就意味着越多的slave因为replication而不可用
    # 可以通过将这个值设为 1 来保证每次只有一个slave处于不能处理命令请求的状态
    # sentinel parallel-syncs <master-name> <numslaves>
    sentinel parallel-syncs mymaster 1
    
    # 故障转移的超时时间 failover-timeout 可以用在以下这些方面:
    # 1. 同一个sentine1对同一个master两次failover之间的间隔时间
    # 2.当一个slave从一个错误的master那里同步数据开始计算时间。直到slave被纠正为向正确的master那里同步数据时
    # 3. 当想要取消一个正在进行的failover所需要的时间
    # 4. 当进行failover时,配置所有slaves指向新的master所需的最大时间。不过,即使过了这个超时,slaves依然会被正确配置为指向master,但是就不按parallel-syncs所配置的规则来了
    #默认三分钟
    # sentinel failover-timeout <master-name> <milliseconds>
    sentinel failover-timeout mymaster 180000
    
    # SCRIPTS EXECUTION
    
    # 配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知相关人员。
    # 对于脚本的运行结果有以下规则:
    # 若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10
    # 若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行
    # 如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同
    # 一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个SIGKILL信号终止,之后重新执行。
    
    # 通知型脚本:当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等),将会去调用这个脚本,这时这个脚本应该通过邮件,SMS等方式去通知系统管理员关于系统不正常运行的信息。调用该脚本时,将传给脚本两个参数,一个是事件的类型,一个是事件的描述。如果sentinel.conf配置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则sentinel无法正常启动成功。
    # 通知脚本
    #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>是“7eader”或者“observer”中的一个
    # 参数 from-ip,from-port,to-ip,to-port是用来和旧的master和新的master(即旧的s1ave)通信的
    # 这个脚本应该是通用的,能被多次调用,不是针对性的
    # sentinel client-reconfig-script <master-name> <script-path>
    sentinel client-reconfig-script mymaster /var/redis/reconfig.sh
    

八、Redis缓存穿透和雪崩

(高频)

缓存穿透(查不到)

缓存穿透

缓存穿透是指当用户在查询一条数据的时候,而此时数据库和缓存却没有关于这条数据的任何记录,而这条数据在缓存中没找到就会向数据库请求获取数据。用户拿不到数据时,就会一直发请求,查询数据库,这样会对数据库的访问造成很大的压力。

如:用户查询一个 id = -1 的商品信息,一般数据库 id 值都是从 1 开始自增,很明显这条信息是不在数据库中,当没有信息返回时,会一直向数据库查询,给当前数据库的造成很大的访问压力。缓存穿透的发生一般是受到 “黑客攻击” 所导致的,所以应该进行监控,如果真的是黑客攻击,及时添加黑名单。

解决方案:

  1. 布隆过滤器

Redis 提供的一种解决方案,特点是代码维护比较复杂,但效果不错。

布隆过滤器是用来过滤东西的,它是一种基于概率的数据结构,主要判断当前某个元素是否在该集合中,运行速度快。

布隆过滤器的巨大用处就是,能够迅速判断一个元素是否在一个集合中。因此他有如下三个使用场景:

  • 网页爬虫对URL的去重,避免爬取相同的URL地址
  • 反垃圾邮件,从数十亿个垃圾邮件列表中判断某邮箱是否垃圾邮箱
  • 缓存穿透,将所有可能存在的数据缓存放到布隆过滤器中,当黑客访问不存在的缓存时迅速返回避免缓存及DB挂掉。

  1. 缓存空对象

从缓存出发,如果当前MySQL数据库不存在的信息,在redis中把它缓存成一个空对象,返回给用户。(代码维护简单,但是效果不是很好)

缓存空对象就是指一个请求发送过来,如果此时缓存中和数据库都不存在这个请求所要查询的相关信息,那么数据库就会返回一个空对象,并将这个空对象和请求关联起来存到缓存中,当下次还是这个请求过来的时候,这时缓存就会命中,就直接从缓存中返回这个空对象,这样可以减少访问数据库的压力提高当前数据库的访问性能

在这里插入图片描述

存在的问题:

  1. 如果空值被缓存起来,意味着缓存需要更多的空间存储更多的键,因为这当中存在许多空的键。
  2. 即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口不一致,这对于需要保持一致的业务会有影响。
缓存击穿(量太大,缓存过期)

​ 造成缓存击穿的原因有两个:

  1. 一个“冷门”key,突然被大量用户请求访问。
  2. 一个“热门”key,在缓存中时间恰好过期,这时有大量用户来进行访问。

这样会导致大并发请求直接穿透缓存,请求数据库,瞬间对数据库的访问压力增大。

在这里插入图片描述

解决方案

1、常用的解决方案是加锁

对于key过期的时候,当key要查询数据库的时候加上一把锁,这时只能让第一个请求进行查询数据库,然后把从数据库中查询到的值存储到缓存中,对于剩下的相同的key,可以直接从缓存中获取即可。

  • 单机环境下:直接使用常用的锁即可(如:Lock、Synchronized等);
  • 分布式环境下可以使用分布式锁,如:基于数据库、基于Redis或者zookeeper 的分布式锁。

2、设置热点数据永不过期

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

雪崩

缓存雪崩是指在某一个时间段内,缓存集中过期失效,如果这个时间段内有大量请求,而查询数据量巨大,所有的请求都会达到存储层,存储层的调用量会暴增,引起数据库压力过大甚至宕机。

原因:

  • Redis突然宕机
  • 大部分数据失效

解决方案

  1. redis高可用
    redis有可能挂掉,多增加几台redis实例,(一主多从或者多主多从),这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。
  2. 限流降级
    在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量,对某个key只允许一个线程查询数据和写缓存,其他线程等待。
  3. 数据预热
    数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key。
  4. 不同的过期时间
    设置不同的过期时间,让缓存失效的时间点尽量均匀。

视频资源:https://www.bilibili.com/video/BV1S54y1R7SB?p=36&spm_id_from=333.1007.top_right_bar_window_history.content.click
笔记&Redis资源:
链接:https://pan.baidu.com/s/1bskp7rYC6r4LUOU3prAI1g
提取码:zaq1
觉得文章排版不美观可以下载我的个人笔记文档,建议使用Typora打开,需要Typora破解版可留言。
觉得有帮助可以点点👍 收藏不迷路🌟

完结撒花 🎉 2022.07.31

  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值