redis笔记

Redis

前言

Redis(Remote Dictionary Server)即远程字典服务

什么是redis?

Redis 是C语言开发的一个开源高性能键值对的内存数据库,可以用来做数据库、缓存、消息中间件等场景,是一种NoSQL(not-only sql,非关系型数据库)的数据库,也被人称为结构性数据库。

Redis能干嘛?

  1. 内存存储、持久化,内存断电即失,其持久化很重要(rdb、aof)

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

  3. 发布订阅系统(简单的消息队列)

  4. 地图信息分析

  5. 计数器、计时器(浏览量)

  6. 。。。

Redis特性

  1. 多样的数据类型

  2. 持久化

  3. 集群

  4. 事务

  5. 。。。

学习中需要用到的东西

 

linux安装redis

  1. 下载安装包到Linux中

  2. 把压缩包移动到/opt中,解压安装包 tar -zxvf redis-7.2-rc3.tar.gz

  3. 进入解压后的文件可以看见配置文件,里面有redis.config

  4. redis是c++编写的,所以Linux需安装c++环境,以下是安装环境

    1. yum install gcc-c++

    2. make,make 是用来编译的,它从Makefile中读取指令,然后编译。

    3. make install这条命令来进行安装(有些软件要先运行 make check 或 make test 来进行测试)

    4. cd /usr/local/bin进入默认安装位置

    5. mkdir myconfig创建一个个人的配置文件在redis的默认目录

    6. cp /opt/redis-7.2-rc3/redis.conf myconfig复制本来的redis.conf到个人配置文件

    7. redis默认不是后台启动的,这里得修改配置文件,daemonize no中no改成yes即可

    8. 在/usr/local/bin目录启动redis服务,redis-server myconfig/redis.conf

    9. 执行redis-cli -p 6379进行连接测试,连上就ping

    10. ps -ef|grep redis查看开启的redis服务

    11. 关闭redis服务(关闭的是cli和server)操作,在客户端输入shutdown

 

基础知识

数据库控制

  1. redis默认有16个数据库,默认使用的是第0个,可以使用select进行切换

    127.0.0.1:6379> select 3    ##切换数据库
    OK
    127.0.0.1:6379[3]> set name haohao2036 ##set操作
    OK
    127.0.0.1:6379> get name ##get操作
    "haohao2036"
    127.0.0.1:6379[3]> DBSIZE ##查看DB大小
    (integer) 0
    127.0.0.1:6379[3]> flushdb ##清空当前数据库
    OK
    127.0.0.1:6379[3]> flushall ##清空所有数据库
    OK
  2. redis为什么是单线程的

    redis是基于内存操作的,cpu不是redis的性能瓶颈所以就使用单线程了,其瓶颈是机器内存和网络带宽

  3. redis为什么是单线程还这么快?

    误区:

    1. 高性能的服务器一定是多线程?

    2. 多线程(CPU上下文切换会消耗资源)一定比单线程效率高?

    redis是将所有数据全部放在内存中,所以说单线程效率是最高的,多线程cpu上下文切换反而会浪费资源

 

性能测试

官方给的redis-benchmark就是一个压力测试工具。

 

我们来简单地测试一下:100个并发连接,100000个请求

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

 

 

 

数据类型

Key的基本操作

keys * 查看所有key

set key value 创建key、value

get key 取得key的value值

dbsize 查看数据库大小

exists KEY 可以查看这个key存在多少个值

move KEY DB 移动key及其对应的value值到某个数据库

expire KEY SECONDS expire name 10 表示把让key为name的数据在10秒后过期(移除)

ttl KEY 查看当前key还有多少秒过期

type key 查看key的类型

五大基础数据类型

String(字符串)

String除了可以是字符串,还可以是数字、对象。

操作字符串

append KEY VALUE

和StringBuilder语法一样,追加字符串,就是往原先字符串后面再添加值

如果当前key不存在,就相当于set操作

例子如下:

127.0.0.1:6379[1]> get name
"hao2036"
127.0.0.1:6379[1]> append name 666
(integer) 10
127.0.0.1:6379[1]> get name
"hao2036666"

strlen KEY 获取key所在value的长度

getrange KEY 0 5 截取key的value索引从0到5的字符

setrange key 7 value 替换索引从7开始的字符

 

 

自增自减操作

(要求value是integer类型)

incr key 自增1

decr key 自减1

incrby key number key的value增加number

decrby key number key的value减少number

 

重要的set

getset

getset key value 组合操作先get再set

setex(set with expire)

setex KEY seconds value 创建一个 seconds秒过期的key-value

setnx(set if not exist)

setnx KEY VALUE 如果key不存在则创建一个key-value,如果存在就不覆盖

mset

mset KEY1 VALUE1 KEY2 VALUE2 KEY3 VALUE3 ... 批量创建key-value

mget

mget KEY1 KEY2 KEY3 ... 批量查询key-value

msetnx 批量执行setnx

msetnx k1 v666 k4 v4 如果key不存在则批量创建一个key-value,如果有一个存在就都不操作

 

对象的操作

常规操作:

set user:1 {name:zhangsan,age:23} 这里以json格式存储一个对象

get user:1 ##取出对象

另一种思路:

mset user:2:name lisi user:2:age 24 ##这里换种思路存储一个对象

mget user:2:name user:2:age ##取出对象

 

 

全部操作例子如下:

127.0.0.1:6379[1]> set num 0
OK
127.0.0.1:6379[1]> incr num ##自增1
(integer) 1
127.0.0.1:6379[1]> incr num
(integer) 2
127.0.0.1:6379[1]> get num
"2"
127.0.0.1:6379[1]> decr num ##自减1
(integer) 1
127.0.0.1:6379[1]> get num
"1"
127.0.0.1:6379[1]> incrby num 100 ##key的value增加100
(integer) 101
127.0.0.1:6379[1]> decrby num 50 ## key的value减少50
(integer) 51
127.0.0.1:6379[1]> get name
"hao2036666"
127.0.0.1:6379[1]> getrange name 0 5  ## 截取key的value索引从0到5的字符
"hao203"
127.0.0.1:6379[1]> getrange name 0 -1 ##截取全部字符(0到-1)
"hao2036666"
127.0.0.1:6379[1]> set name hao2036223666
OK
127.0.0.1:6379[1]> setrange name 7 hao ##替换索引从7开始的字符串
(integer) 13
127.0.0.1:6379[1]> get name
"hao2036hao666"
127.0.0.1:6379> select 0
OK
127.0.0.1:6379> setex abc 30 'hello'  ##创建一个30秒过期的key-value
OK
127.0.0.1:6379> ttl abc
(integer) 25
127.0.0.1:6379> setnx name1 111 ##setnx KEY VALUE   如果key不存在则创建一个key-value
(integer) 1
127.0.0.1:6379> get name1
"111"
127.0.0.1:6379> setnx name1 222 ##如果存在就不覆盖
(integer) 0
127.0.0.1:6379> get name1
"111"
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3
OK
127.0.0.1:6379> mget k1 k2 k3
1) "v1"
2) "v2"
3) "v3"
127.0.0.1:6379> msetnx k1 v666 k4 v4 ##批量使用setnx操作,它是原子性的,要么全部创建,要么都不创建
(integer) 0
127.0.0.1:6379> get k4
(nil)
127.0.0.1:6379> set user:1 {name:zhangsan,age:23}##这里以json格式存储一个对象
OK
127.0.0.1:6379> get user:1 ##取出对象
"{name:zhangsan,age:23}"
127.0.0.1:6379> mset user:2:name lisi user:2:age 24##这里换种思路存储一个对象
OK
127.0.0.1:6379> mget user:2:name user:2:age ##取出对象
1) "lisi"
2) "24"

 

 

List(列表)

在redis里面可以把list完成栈(一个入口,先进后出)、队列、阻塞队列(两端都可以取)

所有的list命令都是L开头的,虽然redis列表的尾部显式是1)开始,但索引从0开始,尾部为0

查元素及长度

LRANGE key start stop 查看索引从start到stop的list元素,当stop为-1时表示取到最先加入的元素为止

Lindex key Num 取索引为Num的元素

Llen key 返回列表的长度

 

添加和移除

LPUSH key value 往list加元素(加在尾部)

RPUSH key value 往list头部加元素

LPOP key 从尾部移除1个元素,如果key后接数字表示移除元素的个数

RPOP key 从头部移除1个元素

Lrem key Num Value 从尾部开始,去除num个值为value的元素

Linsert key before Value Value2 往列表key中元素value前(前表示靠近尾部)插入一个元素value2

Linsert key after Value Value2 往列表key中元素value后面插入一个元素value2

 

覆盖与修改

Ltrim key start stop 在key列表中,从尾部开始截取索引为start到stop的元素来覆盖该列表

Lset key Num value 修改key列表中索引是Num的元素,其结果改成value(Num处没有值则会报错)

 

多列表操作

RpopLpush key1 key2 移除列表key1头部一个元素并添加到列表key2尾部

 

全部操作例子如下:

127.0.0.1:6379> lpush list one
(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
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> lrange list 0 1  #可以发现先取出来的是后放进去的元素
1) "three"
2) "two"
127.0.0.1:6379> Rpush list zero
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "one"
4) "zero"
127.0.0.1:6379> LPOP list ##移除操作
"three"
127.0.0.1:6379> lrange list 0 -1
1) "two"
2) "one"
3) "zero"
127.0.0.1:6379> rpop list
"zero"
127.0.0.1:6379> lrange list 0 -1
1) "two"
2) "one"
#######################下面为重新的list
127.0.0.1:6379> lrange list 0 -1
1) "four"
2) "three"
3) "two"
4) "one"
127.0.0.1:6379> lindex list 0   ##利用Lindex取元素
"four"
127.0.0.1:6379> lindex list -1 ##-1为头部
"one"
127.0.0.1:6379> Llen list    ##返回列表的长度
(integer) 4
#########################
127.0.0.1:6379> lpush list one
(integer) 5
127.0.0.1:6379> lrange list  0 -1
1) "one"
2) "four"
3) "three"
4) "two"
5) "one"
127.0.0.1:6379> lrem list 1 one ##移除列表中1个值为one的元素
(integer) 1
127.0.0.1:6379> lrange list  0 -1  ##可以发现Lrem是从尾部移除
1) "four"
2) "three"
3) "two"
4) "one"
127.0.0.1:6379> lrem list 2 three  ##移除数目超过存在的数目则会全部移除
(integer) 1
127.0.0.1:6379> lrange list  0 -1
1) "four"
2) "two"
3) "one"
####################################
127.0.0.1:6379> ltrim list 0 1   ##在key为list的列表中截取索引为0到1的元素来覆盖list
OK
127.0.0.1:6379> lrange list 0 -1   ##可以看出截取也是从尾部开始的
1) "four"
2) "two"
########################
127.0.0.1:6379> RpopLpush list otherlist   #移除list头部一个元素并添加到otherlist尾部
"one"
127.0.0.1:6379> lrange list 0 -1
1) "four"
2) "two"
127.0.0.1:6379> lrange otherlist 0 -1
1) "one"
############################
127.0.0.1:6379> lset list 0 six ##修改索引是0的元素为six
OK
127.0.0.1:6379> lrange list 0 -1
1) "six"
2) "four"
3) "two"
###########################
127.0.0.1:6379> linsert list before four before ##在four前插入一个元素before
(integer) 4
127.0.0.1:6379> linsert list after four after ##在four后插入一个元素after
(integer) 5
127.0.0.1:6379> lrange list 0 -1
1) "ok"
2) "before"
3) "four"
4) "after"
5) "two"

 

Set(集合)

set中的值不能重复,所有的set命令都是S开头的

添加与移除元素

Sadd key value 添加元素value

Srem key value 移除元素value

Spop key [Num] 随机移除[Num个]元素

 

查看操作

Smembers key 查看全部的数组元素

Sismember key value 判断value元素是否存在

Scard key 获取数组元素数量

 

获取与移动元素

Srandmember key [Num] 随机获取[Num个]元素

Smove key key2 value 把value元素从key集合移动到key2集合

 

数字集合类
  1. 交集:SINTER key1 key2 [key...] 获取交集

  2. 差集:SDIFF key1 key2 [key...] 获取key1除去所有key集合交集的集合,即key1全部不包括交集

  3. 并集:SUNION key1 key2 [key...] 获取所有集合元素组成的集合

127.0.0.1:6379> sadd set one       ##添加元素
(integer) 1
127.0.0.1:6379> sadd set two
(integer) 1
127.0.0.1:6379> sadd set three
(integer) 1
127.0.0.1:6379> smembers set ##查看元素
1) "one"
2) "two"
3) "three"
127.0.0.1:6379> sismember set one ##判断set中有无one元素
(integer) 1
127.0.0.1:6379> scard set ##获取元素个数
(integer) 3
127.0.0.1:6379> srem set one ##移除元素
(integer) 1
127.0.0.1:6379> smembers set
1) "two"
2) "three"
127.0.0.1:6379> srandmember set ##随机取出元素
"two"
127.0.0.1:6379> SRANDMEMBER set 2
1) "two"
2) "three"
127.0.0.1:6379> spop set ##随机移除元素
"three"
127.0.0.1:6379> smembers set
1) "two"
127.0.0.1:6379> sadd set one
(integer) 1
127.0.0.1:6379> smove set set2 one ##移动元素到另一个集合
(integer) 1
127.0.0.1:6379> smembers set
1) "two"
127.0.0.1:6379> smembers set2
1) "one"
###################################################以下为新例子
##应用场景:B站微博的共同关注(就是交集)
127.0.0.1:6379> smembers set1
1) "a"
2) "b"
3) "c"
127.0.0.1:6379> smembers set2
1) "c"
2) "d"
3) "e"
127.0.0.1:6379> SDIFF set1 set2 ##去取差集
1) "a"
2) "b"
127.0.0.1:6379> SDIFF set2 set1 ##去取差集
1) "d"
2) "e"
127.0.0.1:6379> SINTER set1 set2 ##取交集
1) "c"
127.0.0.1:6379> SUNION set1 set2 ##取并集
1) "a"
2) "b"
3) "c"
4) "d"
5) "e"

 

Hash(哈希)

所有的Hash命令都是H开头的,key-map(key-value)格式,它的值是个map集合!

新增与删除

Hset key [Field Value...] 在key哈希表中新增map集合(可以为多个)

Hdel key [Field...] 删除指定key哈希的Field字段(可以为多个),其对应value也就删除了

 

获取操作

HgetAll key 获取key哈希表的全部内容,先打印map的field再map的value依次循环

Hget key field 获取一个字段值

Hlen key 获取哈希存储的map集合数,也就是获取哈希表的字段量

HEXISTS key field 查看key哈希表中field字段是否存在

HKEYS key 获取key哈希表中所有的map集合的key值

HVALS key 获取key哈希表中所有的map集合的value值

 

哈希内集合操作

有一些和String的操作差不多,只不过加了H开头

如:

Hincrby key field number 使field的value增加number(数字的字段才行)

Hsetnx key field value 如果存在就不覆盖(set if not exist)

 

哈希可以存储对象(例子)

Hset user:1 name zhangsan age 22 ##可以以此格式创建对象

hget user:1 name ##获取参数

 

127.0.0.1:6379> Hset myhash field1 one      ##创建
(integer) 1
127.0.0.1:6379> Hset myhash field2 two
(integer) 1
127.0.0.1:6379> HGET myhash field1 ##获取一个字段值
"one"
127.0.0.1:6379> HSET myhash field3 three field4 four ##一次创建多个map集合
(integer) 2
127.0.0.1:6379> HGETALL myhash ##查看全部内容
1) "field1"
2) "one"
3) "field2"
4) "two"
5) "field3"
6) "three"
7) "field4"
8) "four"
127.0.0.1:6379> HDEL myhash field4 ##删除指定的key字段,其对应value也就删除了
(integer) 1
127.0.0.1:6379> HGETALL myhash
1) "field1"
2) "one"
3) "field2"
4) "two"
5) "field3"
6) "three"
127.0.0.1:6379> Hlen myhash ##获取哈希存储的map集合数
(integer) 3
127.0.0.1:6379> HEXISTS myhash field3 ##查看字段是否存在
(integer) 1
127.0.0.1:6379> HKEYS myhash #获取所有的map集合的key值
1) "field1"
2) "field2"
3) "field3"
127.0.0.1:6379> HVALS myhash #获取所有的map集合的value值
1) "one"
2) "two"
3) "three"
#########################################################################
127.0.0.1:6379> Hset user:1 name zhangsan age 22 ##对象的操作
(integer) 2
127.0.0.1:6379> hget user:1 name
"zhangsan"
127.0.0.1:6379> hget user:1 age
"22"
########可以看出哈希更适合对象的存储

 

Zset(有序集合)

所有的zset命令都是Z开头的,它在set基础上增加了一个值,set k1 v1——— zset k1 score1 v1

新增和删除

Zadd key [score value...] 往有序集合key中添加score分的value值,这里score就是排序集合的关键

Zrem key value 移除有序集合key中为value的值

 

 

查询操作

Zrange key start stop 这里是通过索引查询有序集合的元素,默认是按score升序排的

Zrevrange key start stop 同上,按score降序排

Zrangebyscore key min max 这里通过score值按升序范围取min~max进行排序查询

min max可为-inf +inf 意为范围取负无穷到正无穷

Zrevrangebyscore salary max min 功能同上一个,不过是降序,且范围得从大到小

maxre值在min-max区间的元素个数

Zcount key min max 获取有序集合key中score值在min-max区间的元素个数

 

127.0.0.1:6379> Zadd myzset 1 one 2 two 3 three     ##增
(integer) 3
127.0.0.1:6379> zrange myzset 0 -1 ##查
1) "one"
2) "two"
3) "three"
#######################################################
127.0.0.1:6379> zadd salary 5000 zhangsan 4500 lisi 6430 wangwu ##直接将salary当有序集合名字
(integer) 3
127.0.0.1:6379> zrangebyscore salary -inf +inf  ##score值按升序范围取负无穷到正无穷进行排序查询
1) "lisi"
2) "zhangsan"
3) "wangwu"
127.0.0.1:6379> zrangebyscore salary -inf +inf withscores ##同时给出score分
1) "lisi"
2) "4500"
3) "zhangsan"
4) "5000"
5) "wangwu"
6) "6430"
127.0.0.1:6379> zrangebyscore salary (4500 6430 ##小细节:(号表示开区间,没有则默认闭区间
1) "zhangsan"
2) "wangwu"
127.0.0.1:6379> zrem salary zhangsan ##移除操作
(integer) 1
127.0.0.1:6379> zrange salary 0 -1
1) "lisi"
2) "wangwu"
127.0.0.1:6379> Zrevrange salary 0 -1    ##反过来排序
1) "wangwu"
2) "lisi"
127.0.0.1:6379> ZREVRANGEBYSCORE salary +inf -inf##score值按降序范围取负无穷到正无穷进行排序查询
1) "wangwu"
2) "lisi"
127.0.0.1:6379> zcard salary ##获取有序集合元素的数目
(integer) 2
##################判断有序集合的zrange排序规则
127.0.0.1:6379> zadd salary 3000 zhangsan
(integer) 1
127.0.0.1:6379> zrange salary 0 -1 withscores
1) "zhangsan"
2) "3000"
3) "lisi"
4) "4500"
5) "wangwu"
6) "6430"
127.0.0.1:6379> zadd salary 8000 zhaoliu
(integer) 1
127.0.0.1:6379> zrange salary 0 -1 withscores ##不难看出默认是按score升序排的
1) "zhangsan"
2) "3000"
3) "lisi"
4) "4500"
5) "wangwu"
6) "6430"
7) "zhaoliu"
8) "8000"
127.0.0.1:6379> zcount salary 5000 10000 ##获取score在5000到10000区间的元素个数
(integer) 2

案例思路:set能做到的都可以做到,而且是有序的,适合做排行榜value值绑定id即可,还有订单中可以保证安全性,value绑定订单号就避免了list中订单重复多次执行的安全问题!!!

 

这些就是常用的API,其他的API可以去官方文档查看

 

三大特殊数据类型

geospatial(坐标)

其指令以geo开头

定位,附近的人,打车距离计算都可以使用它,geo在redis3.2就推出了,它只有六个命令

 

geoadd

添加地理位置:geoadd key [longitude(经度) latitude(纬度) member(名称)...]

规则:有效经纬度(-180~180,-85~85)两极无法直接添加,一般会下载测试数据,直接通过java程序一次性导入!

127.0.0.1:6379> geoadd china:city 116.24 39.55 beijin 121.29 31.14 shanghai 106.33 29.35 chongqing 111.51 29.02 changde   ##添加城市数据
(integer) 4

 

geopos

获得其定位:GEOPOS key member

127.0.0.1:6379> GEOPOS china:city changde
1) 1) "111.50999933481216431"
  2) "29.02000100814179717"

 

geodist

获得两位置之间距离:geodist key member1 member2 [M|KM|FT|MI]

127.0.0.1:6379> GEODIST china:city beijin shanghai
"1041114.3719"
127.0.0.1:6379> GEODIST china:city beijin shanghai km
"1041.1144"

 

georadius

查询指令,可以获得一个中心点圆形辐射范围内的结果集

GEORADIUS key(结果集的取值地) longitude(中心点经度) latitude(中心点纬度) radius(距离的数字) M|KM|FT|MI(距离单位) [WITHCOORD(结果集显示具体经纬度)] [WITHDIST(结果集带有于中心点与其的距离)] [WITHHASH(带上元素哈希值)] [COUNT(获取元素的数量) count [ANY]] [ASC|DESC(确定排序方式)] [STORE key|STOREDIST key]

127.0.0.1:6379> GEORADIUS china:city 116 33 600 km withcoord count 4    ##带有经纬度信息
1) 1) "shanghai"
  2) 1) "121.28999859094619751"
     2) "31.14000123027434341"
127.0.0.1:6379> GEORADIUS china:city 116 33 600 km withdist count 4 ##带有距离
1) 1) "shanghai"
  2) "539.7253"
127.0.0.1:6379> GEORADIUS china:city 116 33 600 km withdist withcoord count 4 ##都带有
1) 1) "shanghai"
  2) "539.7253"
  3) 1) "121.28999859094619751"
     2) "31.14000123027434341"
georadiusbymember

用元素作为中心点实现辐射范围内获取结果集,指令和georadius几乎一模一样,唯一区别就是中心点换成了结果集取值地的元素

georadiusbymember key member radius M|KM|FT|MI [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC|DESC] [STORE key|STOREDIST key]

127.0.0.1:6379> georadiusbymember china:city shanghai 1700 km withcoord withdist count 5
1) 1) "shanghai"
  2) "0.0000"
  3) 1) "121.28999859094619751"
     2) "31.14000123027434341"
2) 1) "changde"
  2) "969.9977"
  3) 1) "111.50999933481216431"
     2) "29.02000100814179717"
3) 1) "beijin"
  2) "1041.1144"
  3) 1) "116.23999983072280884"
     2) "39.5500007245470826"
4) 1) "chongqing"
  2) "1450.0223"
  3) 1) "106.32999926805496216"
     2) "29.34999889059729838"

 

geohash

返回表示经纬度的一个11字符长度的geohash字符串:geohash key [member [member ...]]

如果两个字符串越接近,那么它们的距离也越接近

127.0.0.1:6379> GEOHASH china:city beijin
1) "wx48vpg7kq0"
127.0.0.1:6379> GEOHASH china:city beijin changde ##若其字符串越接近,那么其距离也越接近
1) "wx48vpg7kq0"
2) "wmpm16zjg20"

 

 

hyperloglog(数据结构)

其指令以pf开头

hyperloglog是用来做基数统计的算法。

什么是基数?

去重后的元素的数量,比如{1,3,5,5,7,7,8,9}其基数是6。

使用场景:统计网页的用户访问量(一个用户无论访问多少次都视为一次),存在可以接受的错误率(0.81%),反正不是需要存储精确信息的应用环境。

传统方式:使用set集合来储存用户id实现这一功能,缺点:如果保存大量用户信息就会非常占内存,我们的目的是计数而不是保存用户id,这违背我们的目的。

hyperloglog优点:占用内存是固定的,能够储存2^64不同元素,差不多12kb!如果仅从内存角度看,hyperloglog就是首选。

 

pfadd key [element [element ...]] hyperloglog中添加元素

pfcount key 统计名为key的hyperloglog中元素数量。

pfmerge key key1 key2 ##合并多条hyperloglog并命名为key(会自动去重)

127.0.0.1:6379> pfadd myhplog a b c d e f g h i j   ##hyperloglog中添加元素
(integer) 1
127.0.0.1:6379> pfcount myhplog ##统计数量
(integer) 10
127.0.0.1:6379> pfadd myhplog2 1 2 3 4 5 6 7 8 9
(integer) 1
127.0.0.1:6379> pfcount myhplog2
(integer) 9
127.0.0.1:6379> PFMERGE myhplog3 myhplog myhplog2 ##合并多条hyperloglog
OK
127.0.0.1:6379> pfcount myhplog3
(integer) 19
127.0.0.1:6379> pfadd myhplog4 a b i j k l m ##合并时重复的元素会进行去重
(integer) 1
127.0.0.1:6379> PFMERGE myhplog5 myhplog3 myhplog4
OK
127.0.0.1:6379> pfcount myhplog5
(integer) 22

 

bitmaps

位存储

用于互补状态的统计如:是否打卡,是否登录,是否活跃...都可以使用bitmaps。

bitmaps位图也是一种数据结构,都是操作二进制位来进行记录,只有0和1两个状态。

就拿一个人365天打卡记录举例子,365天用0和1位存储只需要365bit,1B(字节)=8bit,一个人一年的打卡记录只需要46个字节

 

setbit key num(只能为数字) 0/1 在名为key的bitmaps中插入数据num及其状态0/1

getbit key num 取出名为key的bitmaps中num数据的状态0/1

bitcount key [start end] 统计名为key的bitmaps中1的数量,可以设置起点终点

127.0.0.1:6379> setbit status 0 1       #插入数据及其状态
(integer) 0
127.0.0.1:6379> setbit status 1 2 ##超过0和1范围了
(error) ERR bit is not an integer or out of range
127.0.0.1:6379> setbit status 1 1
(integer) 0
127.0.0.1:6379> setbit status 1 0 ##可以覆盖原数据
(integer) 1
127.0.0.1:6379> setbit status 2 1
(integer) 0
127.0.0.1:6379> setbit status 3 0
(integer) 0
127.0.0.1:6379> setbit status 4 1
(integer) 0
127.0.0.1:6379> setbit status 5 0
(integer) 0
127.0.0.1:6379> setbit status 6 0
(integer) 0
127.0.0.1:6379> getbit status 1 ##取出数据
(integer) 0
127.0.0.1:6379> getbit status 3
(integer) 0
127.0.0.1:6379> getbit status 4
(integer) 1
127.0.0.1:6379> bitcount status ##统计1的个数
(integer) 3
127.0.0.1:6379> bitcount key [start end [BYTE|BIT]]

 

 

事务

事务原则

关系型数据库事务

要么同时成功,要么同时失败

  1. 原子性

  2. 一致性

  3. 隔离性

  4. 持久性

redis事务的本质

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

  1. 一次性

  2. 顺序性

  3. 排他性

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

redis事务没有隔离级别的概念!也就没有关系型数据库幻读,脏读,不可重复读的问题!

 

redis事务执行

在redis事务中所有命令并没有被立即执行,只有在它们进行执行操作后才能被视为执行

过程:

  1. 开启事务(multi)

  2. 命令入队(其他命令即可)

  3. 执行事务(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)> get k1
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> EXEC ##执行事务
1) OK
2) OK
3) "v1"
4) OK

放弃事务

127.0.0.1:6379> multi       ##开启事务
OK
127.0.0.1:6379(TX)> set k4 v4
QUEUED
127.0.0.1:6379(TX)> set k5 v5
QUEUED
127.0.0.1:6379(TX)> DISCARD ##取消事务
OK
127.0.0.1:6379> get k4 ##事务未执行
(nil)

编译型异常

也就是命令格式有错,所有命令都不会执行

127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k4 v4
QUEUED
127.0.0.1:6379(TX)> set k5 v5
QUEUED
127.0.0.1:6379(TX)> getset k3 ##此处命令错误
(error) ERR wrong number of arguments for 'getset' command ##直接报错
127.0.0.1:6379(TX)> getset k3 v3 ##事务并没有停止排队
QUEUED
127.0.0.1:6379(TX)> exec ##执行后也会报错
(error) EXECABORT Transaction discarded because of previous errors.

 

运行时异常

如果队列中存在语法性错误,如(1÷0),那么其他命令可以被执行(redis无法在执行前辨识语法逻辑错误)

127.0.0.1:6379> set k1 "v1"             ##将k1的值设为字符串
OK
127.0.0.1:6379> incr k1 ##这里出现了运行时异常
(error) ERR value is not an integer or out of range
127.0.0.1:6379> multi ##事务中验证
OK
127.0.0.1:6379(TX)> set k2 1
QUEUED
127.0.0.1:6379(TX)> incr k1 ##不影响后续执行
QUEUED
127.0.0.1:6379(TX)> incr k2 ##体现了顺序性
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) (error) ERR value is not an integer or out of range
3) (integer) 2

 

监控(乐观锁)

锁机制

悲观锁
  • 很悲观,认为什么时候都会出问题,无论干啥都会加锁!synchronized

乐观锁
  • 很乐观,认为什么时候都会出现问题,所以不会加锁!更新数据需要去判断一下在此期间是否有人修改过这个数据。mysql就是通过比较version实现乐观锁的。

    原理:乐观锁通常与版本号(例如时间戳)一起使用。在每次更新数据记录时,都会更新版本号。在执行更新操作时,事务会检查当前数据记录的版本号是否与预期版本号相同。如果版本号不同,则表示其他事务已经修改了该记录,因此事务会回滚并重试。

测试redis监控

监控(watch)

解锁(unwatch):如果发现事务执行失败就解锁获取最新值后再监控执行事务

正常操作
127.0.0.1:6379> mset money 100 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
QUEUED
127.0.0.1:6379(TX)> incrby out 20
QUEUED
127.0.0.1:6379(TX)> exec ##正常执行
1) (integer) 80
2) (integer) 20
触发监控
############################客户端1
127.0.0.1:6379> mset money 100 out 0
OK
127.0.0.1:6379> watch money ##加监控
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> decrby money 20
QUEUED
127.0.0.1:6379(TX)> incrby out 20
QUEUED
127.0.0.1:6379(TX)>

############################客户端2
127.0.0.1:6379> get money
"100"
127.0.0.1:6379> set money 1000 ##修改money值
OK

############################客户端1再执行
127.0.0.1:6379(TX)> exec ##执行失败
(nil)
############################如何完成事务呢
127.0.0.1:6379> UNWATCH ##解锁
OK
127.0.0.1:6379> watch money ##再加锁,相当于mysql的getversion
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> decrby money 20
QUEUED
127.0.0.1:6379(TX)> incrby out 20
QUEUED
127.0.0.1:6379(TX)> exec
1) (integer) 980
2) (integer) 20


######################
127.0.0.1:6379> get out
"0"
127.0.0.1:6379> get money
"1000"
127.0.0.1:6379> watch money
OK
127.0.0.1:6379> decr money
(integer) 999
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> decrby money 20
QUEUED
127.0.0.1:6379(TX)> incrby out 20
QUEUED
127.0.0.1:6379(TX)> exec ##证明相同客户端也能使用,所以watch后最好直接加事务,不要修改监控对象
(nil)

 

Jedis

使用java来操作redis

什么是jedis?

是redis官方推荐的Java连接开发工具,使用java操作redis的中间件,需要十分熟悉!!!

idea操作

  1. 导入对应的依赖

    <!--        导入jedis的包-->
           <dependency>
               <groupId>redis.clients</groupId>
               <artifactId>jedis</artifactId>
               <version>3.2.0</version>
           </dependency>
    <!--       存数据的fastjson-->
           <dependency>
               <groupId>com.alibaba</groupId>
               <artifactId>fastjson</artifactId>
               <version>2.0.21</version>
           </dependency>
  2. 编码测试

    • 连接数据库

    public static void main(String[] args) {
           //这里就启动本地redis算了
           Jedis jedis = new Jedis("127.0.0.1",6379);
           //redis所有指令jedis都有对应的方法
           String ping = jedis.ping();
           System.out.println(ping);
      }
    • 操作命令

    //使用jedis对象对应的方法即可
    • 断开连接

 

jedis事务

      public static void main(String[] args) {
       //这里就启动本地redis算了
       Jedis jedis = new Jedis("127.0.0.1",6379);
       jedis.flushDB();  //清空redis数据库
       JSONObject jsonObject=new JSONObject();
       jsonObject.put("hello","world");
       jsonObject.put("name","hao");
       String result = jsonObject.toJSONString();
//       jedis.watch("user1"); //监视得在事务前面
       //开启事务
       Transaction multi = jedis.multi();
       try {               //idea编译器中ctrl+alt+t可以快捷使用环绕方法
           multi.set("user1", result);
           multi.set("user2", result);
//           int i=1/0;   //制造异常
           multi.exec();       //执行事务
      } catch (Exception e) {
           System.out.println("放弃事务");
           multi.discard();//放弃事务
      } finally {
           System.out.println(jedis.get("user1"));
           System.out.println(jedis.get("user2"));
           jedis.close();//关闭连接
      }
  }

 

Springboot整合

导入前须知

springboot操作数据:spring-data、jpa、jdbc、mongodb、redis

SpringData是和springboot齐名的项目!

springboot自动配置

#springboot所有的配置类都有一个自动配置类可在外部库中spring-boot-autoconfigure中查看,里面可以找到自动配置的中间件文件位置
#自动配置类都会绑定一个properties配置文件
#例如redis就是RedisAutoConfiguration和RedisProperties

为什么不用jedis?

因为springboot2.x之后所有的jedis被替换成了letture

jedis:采用的是直连,多线程操作是不安全的,如果想要避免这个问题需要使用redis的pool连接池,BIO模式。

letture:采用netty,实例可以在多个线程中共享,不存在线程不安全情况,NIO模式

源码分析:

    @Bean
   @ConditionalOnMissingBean(
       name = {"redisTemplate"}
  )//ConditionalOnMissingBean表示如果不存在这个bean则生效,可以重写redisTemplate替换这个默认的
   @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
   public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
       //默认的RedisTemplate没有过多的设置,redis对象都是需要序列化的,所以后续需要自己序列化
       //两个泛型都是object类型,后续使用需要强制转换成<String,Object>
       RedisTemplate<Object, Object> template = new RedisTemplate();
       template.setConnectionFactory(redisConnectionFactory);
       return template;
  }

   @Bean
   @ConditionalOnMissingBean //由于string类型最常用所以单独提出来了这个bean
   @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
   public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
       return new StringRedisTemplate(redisConnectionFactory);
  }

整合过程

导入依赖

<!--可以通过springboot直接导入,在nosql里面的redis-->

配置连接

spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.database=0
#连接池建议使用spring.redis.lettuce.pool,因为springboot自动导入的redis不会和jedis一起导入,而是和lettuce一起导入的
#先配点简单的

测试

@SpringBootTest
class Redis02SpringbootApplicationTests {
   @Autowired
   private RedisTemplate redisTemplate;
   @Test
   void contextLoads() {
       //opsFor:操作...
       //RedisTemplate操作不同的数据类型
       /*
          opsForValue()       字符串
          opsForList()         列表
          opsForSet()         集合
          opsForHash()         哈希表
          opsForZSet()         Z集合
          opsForGeo()         地图坐标数据
          opsForHyperLogLog()
       */
//基本操作可以通过RedisTemplate完成,如事务和基本的crud
//还可以直接通过redisTemplate.getConnectionFactory().getConnection()获取连接操作redis
       ValueOperations value = redisTemplate.opsForValue();//操作String字符串
          value.set("hao","111");//这时存入redis是没有经过序列化的乱码
      RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();

  }
}

序列化(含模版)

所有的对象需要序列化,不然存入redis是默认的jdk序列化,而不是便于观察的json序列化

RedisTemplate的默认序列化是jdk序列化,会经过转译的,我们重写RedisTemplate来使用json序列化

Redis配置文件重写(固定模版,直接使用)

@Configuration
public class RedisConfig {
   //编写自定义redisTemplate,因为我们要使用json序列化1,所以用<String,Object>类型
   @Bean
   public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
      //定义Template格式,json是<String, Object>
       RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
       template.setConnectionFactory(factory);
       
   //序列化设置
       //转json序列化
       Jackson2JsonRedisSerializer<Object> 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);
       //转String序列化
       StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
       
   //配置需要序列化的位置    
       //key采用String的序列化
       template.setKeySerializer(stringRedisSerializer);
       //哈希的key采用String的序列化
       template.setHashKeySerializer(stringRedisSerializer);
       //value采用json的序列化
       template.setValueSerializer(jackson2JsonRedisSerializer);
       //哈希的value采用json的序列化
       template.setHashValueSerializer(jackson2JsonRedisSerializer);
       
       //提交properties的修改
       template.afterPropertiesSet();
       return template;
  }
}

重写redistemplate后往redis里存Java对象就不会出现乱码的序列化问题了!!

工具类(固定模板)

需要导入依赖
        <dependency>
           <groupId>com.google.guava</groupId>
           <artifactId>guava</artifactId>
           <version>30.0-jre</version>
       </dependency>
工具类的代码
/**
    * @author Administrator
    */
   @Component
   public class RedisUtils {

       @Autowired
       private StringRedisTemplate redisTemplate;

       public RedisUtils(StringRedisTemplate redisTemplate) {
           this.redisTemplate = redisTemplate;
      }

       /**
        * 写入缓存
        *
        * @param key   redis键
        * @param value redis值
        * @return 是否成功
        */
       public boolean set(final String key, String value) {
           boolean result = false;
           try {
               ValueOperations<String, String> operations = redisTemplate.opsForValue();
               operations.set(key, value);
               result = true;
          } catch (Exception e) {
               e.printStackTrace();
          }
           return result;
      }


       /**
        * 写入缓存设置时效时间
        *
        * @param key   redis键
        * @param value redis值
        * @return 是否成功
        */
       public boolean set(final String key, String value, Long expireTime) {
           boolean result = false;
           try {
               ValueOperations<String, String> operations = redisTemplate.opsForValue();
               operations.set(key, value);
               redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
               result = true;
          } catch (Exception e) {
               e.printStackTrace();
          }
           return result;
      }

       /**
        * 批量删除对应的键值对
        *
        * @param keys Redis键名数组
        */
       public void removeByKeys(final String... keys) {
           for (String key : keys) {
               remove(key);
          }
      }

       /**
        * 批量删除Redis key
        *
        * @param pattern 键名包含字符串(如:myKey*)
        */
       public void removePattern(final String pattern) {
           Set<String> keys = redisTemplate.keys(pattern);
           if (keys != null && keys.size() > 0) {
               redisTemplate.delete(keys);
          }
      }

       /**
        * 删除key,也删除对应的value
        *
        * @param key Redis键名
        */
       public void remove(final String key) {
           if (exists(key)) {
               redisTemplate.delete(key);
          }
      }

       /**
        * 判断缓存中是否有对应的value
        *
        * @param key Redis键名
        * @return 是否存在
        */
       public Boolean exists(final String key) {
           return redisTemplate.hasKey(key);
      }

       /**
        * 读取缓存
        *
        * @param key Redis键名
        * @return 是否存在
        */
       public String get(final String key) {
           String result = null;
           ValueOperations<String, String> operations = redisTemplate.opsForValue();
           result = operations.get(key);
           return result;
      }

       /**
        * 哈希 添加
        *
        * @param key     Redis键
        * @param hashKey 哈希键
        * @param value   哈希值
        */
       public void hmSet(String key, String hashKey, String value) {
           HashOperations<String, String, String> hash = redisTemplate.opsForHash();
           hash.put(key, hashKey, value);
      }

       /**
        * 哈希获取数据
        *
        * @param key     Redis键
        * @param hashKey 哈希键
        * @return 哈希值
        */
       public String hmGet(String key, String hashKey) {
           HashOperations<String, String, String> hash = redisTemplate.opsForHash();
           return hash.get(key, hashKey);
      }

       /**
        * 判断hash是否存在键
        *
        * @param key     Redis键
        * @param hashKey 哈希键
        * @return 是否存在
        */
       public boolean hmHasKey(String key, String hashKey) {
           HashOperations<String, String, String> hash = redisTemplate.opsForHash();
           return hash.hasKey(key, hashKey);
      }

       /**
        * 删除hash中一条或多条数据
        *
        * @param key     Redis键
        * @param hashKeys 哈希键名数组
        * @return 删除数量
        */
       public long hmRemove(String key, String... hashKeys) {
           HashOperations<String, String, String> hash = redisTemplate.opsForHash();
           return hash.delete(key, hashKeys);
      }

       /**
        * 获取所有哈希键值对
        *
        * @param key Redis键名
        * @return 哈希Map
        */
       public Map<String, String> hashMapGet(String key) {
           HashOperations<String, String, String> hash = redisTemplate.opsForHash();
           return hash.entries(key);
      }

       /**
        * 保存Map到哈希
        *
        * @param key Redis键名
        * @param map 哈希Map
        */
       public void hashMapSet(String key, Map<String, String> map) {
           HashOperations<String, String, String> hash = redisTemplate.opsForHash();
           hash.putAll(key, map);
      }

       /**
        * 列表-追加值
        *
        * @param key   Redis键名
        * @param value 列表值
        */
       public void lPush(String key, String value) {
           ListOperations<String, String> list = redisTemplate.opsForList();
           list.rightPush(key, value);
      }

       /**
        * 列表-获取指定范围数据
        *
        * @param key   Redis键名
        * @param start 开始行号
        * @param end   结束行号
        * @return 列表
        */
       public List<String> lRange(String key, long start, long end) {
           ListOperations<String, String> list = redisTemplate.opsForList();
           return list.range(key, start, end);
      }

       /**
        * 集合添加
        *
        * @param key   Redis键名
        * @param value 值
        */
       public void add(String key, String value) {
           SetOperations<String, String> set = redisTemplate.opsForSet();
           set.add(key, value);
      }

       /**
        * 集合获取
        *
        * @param key Redis键名
        * @return 集合
        */
       public Set<String> setMembers(String key) {
           SetOperations<String, String> set = redisTemplate.opsForSet();
           return set.members(key);
      }

       /**
        * 有序集合添加
        *
        * @param key   Redis键名
        * @param value 值
        * @param score 排序号
        */
       public void zAdd(String key, String value, double score) {
           ZSetOperations<String, String> zSet = redisTemplate.opsForZSet();
           zSet.add(key, value, score);
      }

       /**
        * 有序集合-获取指定范围
        *
        * @param key       Redis键
        * @param startScore 开始序号
        * @param endScore   结束序号
        * @return 集合
        */
       public Set<String> rangeByScore(String key, double startScore, double endScore) {
           ZSetOperations<String, String> zset = redisTemplate.opsForZSet();
           return zset.rangeByScore(key, startScore, endScore);
      }

       /**
        * 模糊查询Redis键名
        *
        * @param pattern 键名包含字符串(如:myKey*)
        * @return 集合
        */
       public Set<String> keys(String pattern) {
           return redisTemplate.keys(pattern);
      }

       /**
        * 获取多个hashMap
        *
        * @param keySet
        * @return List<Map < String, String>> hashMap列表
        */
       public List hashMapList(Collection<String> keySet) {
           return redisTemplate.executePipelined(new SessionCallback<String>() {
               @Override
               public <K, V> String execute(RedisOperations<K, V> operations) throws DataAccessException {
                   HashOperations hashOperations = operations.opsForHash();
                   for (String key : keySet) {
                       hashOperations.entries(key);
                  }
                   return null;
              }
          });
      }

       /**
        * 保存多个哈希表(HashMap)(Redis键名可重复)
        *
        * @param batchMap Map<Redis键名,Map<键,值>>
        */
       public void batchHashMapSet(HashMultimap<String, Map<String, String>> batchMap) {
           // 设置5秒超时时间
           redisTemplate.expire("max", 25, TimeUnit.SECONDS);
           redisTemplate.executePipelined(new RedisCallback<List<Map<String, String>>>() {

               @Override
               public List<Map<String, String>> doInRedis(RedisConnection connection) throws DataAccessException {
                   Iterator<Map.Entry<String, Map<String, String>>> iterator = batchMap.entries().iterator();
                   while (iterator.hasNext()) {
                       Map.Entry<String, Map<String, String>> hash = iterator.next();
                       // 哈希名,即表名
                       byte[] hashName = redisTemplate.getStringSerializer().serialize(hash.getKey());
                       Map<String, String> hashValues = hash.getValue();
                       Iterator<Map.Entry<String, String>> it = hashValues.entrySet().iterator();
                       // 将元素序列化后缓存,即表的多条哈希记录
                       Map<byte[], byte[]> hashes = new HashMap<byte[], byte[]>();
                       while (it.hasNext()) {
                           // hash中一条key-value记录
                           Map.Entry<String, String> entry = it.next();
                           byte[] key = redisTemplate.getStringSerializer().serialize(entry.getKey());
                           byte[] value = redisTemplate.getStringSerializer().serialize(entry.getValue());
                           hashes.put(key, value);
                      }
                       // 批量保存
                       connection.hMSet(hashName, hashes);
                  }
                   return null;
              }
          });
      }

       /**
        * 保存多个哈希表(HashMap)(Redis键名不可以重复)
        *
        * @param dataMap Map<Redis键名,Map<哈希键,哈希值>>
        */
       public void batchHashMapSet(Map<String, Map<String, String>> dataMap) {
           // 设置5秒超时时间
           redisTemplate.expire("max", 25, TimeUnit.SECONDS);
           redisTemplate.executePipelined(new RedisCallback<List<Map<String, String>>>() {

               @Override
               public List<Map<String, String>> doInRedis(RedisConnection connection) throws DataAccessException {
                   Iterator<Map.Entry<String, Map<String, String>>> iterator = dataMap.entrySet().iterator();
                   while (iterator.hasNext()) {
                       Map.Entry<String, Map<String, String>> hash = iterator.next();
                       // 哈希名,即表名
                       byte[] hashName = redisTemplate.getStringSerializer().serialize(hash.getKey());
                       Map<String, String> hashValues = hash.getValue();
                       Iterator<Map.Entry<String, String>> it = hashValues.entrySet().iterator();
                       // 将元素序列化后缓存,即表的多条哈希记录
                       Map<byte[], byte[]> hashes = new HashMap<byte[], byte[]>();
                       while (it.hasNext()) {
                           // hash中一条key-value记录
                           Map.Entry<String, String> entry = it.next();
                           byte[] key = redisTemplate.getStringSerializer().serialize(entry.getKey());
                           byte[] value = redisTemplate.getStringSerializer().serialize(entry.getValue());
                           hashes.put(key, value);
                      }
                       // 批量保存
                       connection.hMSet(hashName, hashes);
                  }
                   return null;
              }
          });
      }

       /**
        * 保存多个哈希表(HashMap)列表(哈希map的Redis键名不能重复)
        *
        * @param list Map<Redis键名,Map<哈希键,哈希值>>
        * @see RedisUtils*.batchHashMapSet()*
        */
       public void batchHashMapListSet(List<Map<String, Map<String, String>>> list) {
           // 设置5秒超时时间
           redisTemplate.expire("max", 25, TimeUnit.SECONDS);
           redisTemplate.executePipelined(new RedisCallback<List<Map<String, String>>>() {

               @Override
               public List<Map<String, String>> doInRedis(RedisConnection connection) throws DataAccessException {
                   for (Map<String, Map<String, String>> dataMap : list) {
                       Iterator<Map.Entry<String, Map<String, String>>> iterator = dataMap.entrySet().iterator();
                       while (iterator.hasNext()) {
                           Map.Entry<String, Map<String, String>> hash = iterator.next();
                           // 哈希名,即表名
                           byte[] hashName = redisTemplate.getStringSerializer().serialize(hash.getKey());
                           Map<String, String> hashValues = hash.getValue();
                           Iterator<Map.Entry<String, String>> it = hashValues.entrySet().iterator();
                           // 将元素序列化后缓存,即表的多条哈希记录
                           Map<byte[], byte[]> hashes = new HashMap<byte[], byte[]>();
                           while (it.hasNext()) {
                               // hash中一条key-value记录
                               Map.Entry<String, String> entry = it.next();
                               byte[] key = redisTemplate.getStringSerializer().serialize(entry.getKey());
                               byte[] value = redisTemplate.getStringSerializer().serialize(entry.getValue());
                               hashes.put(key, value);
                          }
                           // 批量保存
                           connection.hMSet(hashName, hashes);
                      }
                  }
                   return null;
              }
          });
      }
  }
通过工具类测试
    @Test
   void test2(){
       redisUtils.set("name","hao");
       System.out.println(redisUtils.get("name"));
  }

 

 

redis.conf详解

1、点击Esc键,这一步的意思是vim准备接受命令了。
2、然后直接敲击键盘输入命令。这一步需要注意的是,不要试图用鼠标在屏幕上找要任何输入命令的地方。你只需要点了Esc键,直接敲击键盘,然后键入命令,回车就行。另外,注意输入法需要是英文状态。
下面是各种常用命令:
1   :q    --退出(如果没有做任何操作,可以直接退出,如果修改内容,还没有保存,这样就退不了)
2   :q!    --不保存退出(没有保存,就可直接退出了,也就是强退)
3   :wq    --写入文件并退出
4   :wq!    --强制写入,并退出(有些打开的文件是只读的,可以用这个命令)

redis启动的时候,建议就通过配置文件启动

  1. 配置文件unit单位对大小写不敏感

  2. 可以和其他配置文件包容,就好比spring的include,import。

网络配置

bind 127.0.0.1 -::1             ##绑定IP地址
protected-mode yes ##保护模式
port 6379 ##端口设置

通用配置GENERAL

daemonize yes                   ##以守护进程的方式运行,默认为no,建议开yes来后台运行
pidfile /var/run/redis_6379.pid ##如果以后台运行,就需要指定一个pid文件

# Specify the server verbosity level. ##日志
# This can be one of:
# debug (a lot of information, useful for development/testing)
# verbose (many rarely useful info, but not a mess like the debug level)
# notice (moderately verbose, what you want in production probably)
# warning (only very important / critical messages are logged)
# nothing (nothing is logged)
loglevel notice ##日志级别
logfile "" ##日志文件位置名,为空就直接输出日志
databases 16 ##默认数据库数量
always-show-logo no ##是否总是显示redis的logo

快照SNAPSHOTTING

持久化 ,规定多久时间多少次操作就会持久化到文件.rdb和.aof中

save 3600 1     ##3600秒内修改过1次key就rdb持久化一次
save 300 100 ##300秒内进行过100次操作就持久化一次
save 60 10000   ##60秒内进行过10000次操作就持久化一次(高并发情况)高并发情况
等价于
save 3600 1 300 100 60 10000

stop-writes-on-bgsave-error yes ##持久化出现错误后是否停止写入
rdbcompression yes ##是否压缩rdb文件
rdbchecksum yes ##是否校验rdb文件
dir ./ ##rdb保存的目录

 

主从复制REPLICATION

 

 

 

 

安全SECURITY

正式环境一定要设置密码

config  get requirepass     ##获取密码

config set requirepass ##设置密码

auto password ##输入密码

 

配置文件

requirepass password        ##可以设置redis密码,默认被注释,没有密码

 

限制CLIENTS

maxclients 10000        ##最大客户端数量设置,默认10000

maxmemory <bytes> ##默认最大内存设置,单位默认是字节

maxmemory-policy noeviction ##内存满了的处理策略
#maxmemory-policy 配置的策略
#noeviction: 不删除策略,达到最大内存限制时,如果需要更多内存,直接返回错误信息。(默认值)
#allkeys-lru: 所有key通用;优先删除最近最少使用(less recently used,LRU)的key。
#volatile-lru: 只限于设置了expire的部分;优先删除最近最少使用(less recently used ,LRU)的key。
#allkeys-random: 所有key通用;随机删除一部分key。
#volatile-random: 只限于设置了expire的部分;随机删除一部分key。
#volatile-ttl: 只限于设置了expire的部分;优先删除剩余时间(time to live,TTL)短的key。
#redis中并不会准确的删除所有键中最近最少使用的键,而是随机抽取maxmeory-samples个键,删除这三个键中最近最少使用的键

 

APPEND ONLY 模式-AOF

appendonly no   ##默认是不开启的,大部分情况下rdb就够用了
appendfilename "appendonly.aof"  ##持久化文件名

# appendfsync always ##每次修改执行一次 sync同步,消耗性能
appendfsync everysec ##每秒执行一次 sync,如果宕机了就可能丢失实时的数据
# appendfsync no ##不执行同步

具体配置在持久化中详细列出

 

 

 

redis持久化----重点!

内存数据库断电即失,需要持久化操作

RDB(redis database)

在主从复制中,rdb就是备用的,放在从机中。

rdb是在指定的时间间隔内将内存中的数据集快照写入磁盘,恢复时将快照直接读到内存即可

redis会单独创建(fork)一个子进程来进行持久化,持久化过程中会创建一个临时文件,新写入的数据会存入临时文件中,持久化结束后再将临时文件的内容替换持久化好的文件,这个过程redis主进程不进行任何IO操作,确保了极高的性能,如果要进行大规模恢复且对于数据恢复的完整性不是非常敏感,rdb就比aof方式更加高效,rdb缺点就是最后一次持久化后的数据可能丢失。

rdb保存的文件就是dump.rdb,配置文件中可以修改名字,生产环境会将这个文件进行备份

[root@localhost bin]# redis-server myconfig/redis.conf
[root@localhost bin]# redis-cli -p 6379
127.0.0.1:6379> keys * ##说明redis内有值
1) "k1"
127.0.0.1:6379> SHUTDOWN
not connected> exit
[root@localhost bin]# ls
docker-compose dump.rdb myconfig redis-benchmark redis-check-aof redis-check-rdb redis-cli redis-sentinel redis-server
[root@localhost bin]# rm -rf dump.rdb ##清除rdb文件
[root@localhost bin]# redis-server myconfig/redis.conf
[root@localhost bin]# redis-cli -p 6379
127.0.0.1:6379> keys * ##说明redis内不存在值了
(empty array)

执行rdb备份条件

1.满足配置文件save的条件

2.执行flushall

3.退出redis也会产生.rdb文件

4.直接save操作

恢复rdb文件

直接将rdb文件放在redis根目录即可,redis启动会自动检查dump.rdb恢复其中的数据!

rdb操作

127.0.0.1:6379> CONFIG get dir              ##如果这个文件存在dump.rdb文件,启动就自动恢复
1) "dir"
2) "/usr/local/bin"

优缺点

优点:

  1. 适合大规模数据恢复

  2. 对数据完整性要求不高(没备份时宕机会丢失部分数据)

缺点:

  1. 需要一定时间间隔进行操作,宕机丢失数据

  2. fork进程会占用一定的内容空间

 

AOF(Append only file)

将所有命令记录下来,相当于history文件。恢复的时候直接执行一遍这些命令即可

以日志的形式记录每一个修改的操作,只许追加文件不能改写这个文件,redis启动时会读取该文件,重新构建数据,大数据情况下效率非常慢

aof模式保存的是appenldonly.aof文件

开启

配置文件中appendonly no的no改为yes即可

修复

如果appendonly.aof被打开修改发生了错位,redis启动不了

redis-check-aof --fix   appendonly.aof      ##可以进行修复,会存在数据丢失问题,比如出错位置会丢失,按秒存储也会丢失最后一秒内的数据

重写规则

aof默认是文件无限追加,文件会越来越大

如果aof大于64mb,会fork一个新的进程对文件进行重写。

 

优缺点

优点:

  1. 每一次修改都同步,文件存储完整性更加好

  2. 默认开启每秒同步一次,可能丢失一秒数据

缺点:

  1. aof文件远远大于rdb

  2. 修复速度也比rdb慢

  3. redis开启时运行效率也比rdb慢,所以默认使用rdb

 

 

 

 

redis发布订阅

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

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

原理图

订阅端:

127.0.0.1:6379> SUBSCRIBE haohao        ##订阅这个频道,没有则创建
1) "subscribe"
2) "haohao"
3) (integer) 1
1) "message"
2) "haohao"
3) "hello"
1) "message"
2) "haohao"
3) "hi"
127.0.0.1:6379> UNSUBSCRIBE haohao ##退订
1) "unsubscribe"
2) "haohao"
3) (integer) 0

发布端:

127.0.0.1:6379> PUBLISH haohao "hello"      ##给haohao频道推送"haohao"这个消息
(integer) 1
127.0.0.1:6379> publish haohao "hi"
(integer) 1

使用场景:

简单的实时消息系统、实时聊天室

复杂一点的会使用消息中间件MQ

 

redis主从复制

概念

主从复制,读写分离,redis80%情况下都是读操作,为了缓解服务器压力,使用了主从复制模式,一般情况下最少是一主二从来满足哨兵模式。

多读少写的常用方案:

主从复制的主要作用:

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

  2. 故障恢复:主节点出现问题将由从节点提供服务

  3. 负载均衡

  4. 高可用的基石:实现哨兵模式和集群的基础

一般来说,redis的项目中一台redis服务器是万万不能的,原因如下:

  1. 单个服务器会发生单点故障,而且处理请求压力巨大

  2. redis服务器内存不应该超过20g,所以需要多台服务器来满足内存要求

环境配置

只配置从库,不配置主库,默认情况下每一台redis服务器都是主节点

127.0.0.1:6379> info replication        ##查看主从复制的信息
# Replication
role:master ##角色
connected_slaves:0 ##从机数量
master_failover_state:no-failover
master_replid:8a7fbfbc6449223845f357d907ba6ef417b2c6bf
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. pid名字

  3. log文件名字

  4. dump.rdb名字

 

配置一主二从

命令设置

重启需要重新设置

slaveof host port 设置为host port的从机

配置设置

永久生效

replicaof masterhost masterport

 

以下是从机的信息

127.0.0.1:6379> SLAVEOF 192.168.2.107 6379
OK
127.0.0.1:6379> info replication
# Replication
role:slave
master_host:192.168.2.107
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:8a7fbfbc6449223845f357d907ba6ef417b2c6bf
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

master and slave

主机可以写,主机的所有信息都会被从机自动保存

从机不能写,只能读

默认情况下:主机宕机了,从机不会代替主机来进行写操作,但可以读操作,保证了高可用性

 

复制原理

  1. 从机启动连接到主机会发送sync的同步命令

  2. 主机接收到命令,启动后台的存盘进程,同时收集所有用于修改的命令,在后台进程执行完毕后,主机将整个文件传送到从机,并完成一次同步

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

    • 增量复制:master继续将所有的新命令继续依次传递给slave,完成同步

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

 

主机迁移

原始模式

slaveof on one   ##如果该从机没有主机了,那么它将会变成主机,其他节点的手动连接到这个新的主机,原主机恢复了也不影响该从机变成主机的事实,原主机变成“光杆司令”。

哨兵模式

主从切换:原始模式需要手动配置,费时费力,redis2.8提供哨兵架构来解决这个问题,可以后台监控主机是否故障,如果故障会根据投票数自动将从库转换成主库

单哨兵

多哨兵

 

配置哨兵

在配置文件文件夹里新建一个配置文件,这里可以就叫sentinel.conf

#sentinel monitor 被监控服务器的名称 host port 投票数,主机挂了从机中票数最多者成为主机
sentinel monitor myredis 127.0.0.1 6379 1
启动哨兵
redis-sentinel myconfig/sentinel.conf		##输入就启动哨兵了

启动哨兵后会自动获取其info replication信息,得到从机信息。哨兵会定时发送心跳消息,如果主机宕机了哨兵会在从机中重新确定新的主机,如果原主机恢复了,那也会自动成为新主机的从机。

 

优缺点

优点:

  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主节点失联 那么这时客观上认为主节点失联了
# 注意:如果 quorum 给的值过大, 超过主机数量, 可能会导致 master 主机挂掉之后, 没有新的 slave来替代 master
# sentinel monitor <master-name> <ip> <redis-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-0123passw0rd

# 指定多少毫秒之后 主节点没有应答哨兵sentinel 此时 哨兵主观上认为主节点下线 默认30秒
# sentinel down-after-milliseconds <master-name> <milliseconds>
sentinel 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. 同一个sentinel对同一个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>是“leader”或者“observer”中的一个。
# 参数 from-ip, from-port, to-ip, to-port是用来和旧的master和新的master(即旧的slave)通信的
# 这个脚本应该是通用的,能被多次调用,不是针对性的。
# sentinel client-reconfig-script <master-name> <script-path>
sentinel client-reconfig-script mymaster /var/redis/reconfig.sh

 

缓存击穿和雪崩

 

缓存穿透

什么是缓存穿透

缓存穿透是指在使用缓存系统时,特定的查询在缓存和数据库中都找不到结果,导致每次查询都要访问数据库,从而增加数据库的压力,降低系统的性能。

当一个查询请求经过缓存系统时,缓存先检查是否有缓存的结果,如果有则直接返回给客户端,如果没有则查询数据库并将结果存入缓存后返回。但是,如果查询的数据在数据库中不存在,那么每次查询都会通过缓存系统直接访问数据库,导致数据库无效查询增加,浪费了系统资源。

常见情况

查询不存在的数据:当用户查询一个不存在的数据,例如某个不存在的用户ID,由于缓存中没有缓存该数据,每次查询都会失败并直接访问数据库。

恶意查询:如果攻击者故意发送大量不存在的请求,试图绕过缓存,并导致大量无效的数据库查询请求。

问题

增加数据库负载:由于缓存穿透导致大量无效的数据库查询操作,增加了数据库的负载,可能导致数据库性能下降。

击穿缓存:如果缓存中缓存了查询结果为空的键,恶意攻击者可以通过大量请求这些不存在的键,使缓存中的该键过期,从而导致后续请求都直接访问数据库,形成缓存击穿

解决方法

  1. 布隆过滤器(Bloom Filter):在查询前先通过布隆过滤器快速判断查询的数据是否存在,若不存在则不再访问缓存和数据库,减轻数据库压力。

  2. 空结果缓存:在缓存中存储空结果的键,可以避免缓存穿透,防止恶意攻击。

  3. 延迟双写:在查询到数据库不存在该数据时,在缓存中也写入一个空结果的占位符,设置较短的过期时间,以防止并发大量请求穿透缓存直接访问数据库。

  4. 异步更新缓存:当发现缓存和数据库中都不存在某个查询结果时,可以使用异步更新缓存的方式,先返回空结果给用户,然后通过后台任务去查询数据库并更新缓存,提高查询的响应速度和系统的并发能力。

总结

综上所述,缓存穿透是一种常见的缓存问题,采取适当的预防措施可以避免对数据库造成不必要的压力,并提高系统的性能和稳定性。

 

 

缓存击穿

什么是缓存击穿

缓存击穿是指在使用缓存系统时,一个热门的、经常被访问的数据缓存过期或失效时,大量并发请求同时涌入,直接访问数据库,导致数据库负载剧增,造成系统性能下降甚至崩溃的情况。

常见情况

高并发热点数据:某个数据非常热门,并且被大量并发请求频繁访问。如果该数据的缓存过期或者被意外清空,大量的请求会直接访问数据库,导致数据库压力激增。

突发请求:在某个时间点突然出现大量请求访问某个数据,而该数据的缓存恰好在此时失效,导致大量请求绕过缓存直接访问数据库。

问题

增加数据库负载:大量并发请求同时访问数据库,导致数据库性能下降,甚至崩溃。

响应时间延长:绕过缓存直接访问数据库,数据库响应时间增加,造成请求的响应时间延长。

解决方法

  1. 加锁或互斥机制:在缓存失效时,只允许一个请求访问数据库,并将结果缓存,其他请求等待并从缓存中获取数据。

  2. 热点数据永远不过期:对于热点数据,可以将其缓存时间设置为永不过期,或者设置一个合理的较长过期时间,确保不会频繁去访问数据库。

  3. 异步更新缓存:当某个热点数据的缓存过期时,可以使用异步任务来更新缓存,先返回旧的缓存结果给请求,然后在后台异步更新缓存。

  4. 限流和降级:对于突发的大量请求,可以采取限流策略,限制并发访问的请求数量,或者通过降级策略返回预设的默认值,避免数据库负载过大。

  5. 前置缓存:在缓存层之前添加一个前置缓存(如CDN等),将请求分摊到多个缓存节点,减轻热点数据的单一缓存节点压力。

总结

综上所述,缓存击穿是一种常见的缓存问题,通过合理的缓存策略、并发控制和异步更新缓存等手段可以有效避免和应对缓存击穿问题,提高系统的性能和稳定性。

 

 

缓存雪崩

什么是缓存雪崩

缓存雪崩是指在使用缓存系统时,大量缓存失效或过期,导致原本应该由缓存提供的数据,都需要从数据库中重新加载,从而引发数据库压力剧增、性能下降,甚至系统崩溃的现象。

常见情况

缓存批量失效:多个缓存键的过期时间或失效时间几乎同时到达,导致大量缓存同时失效。

重启或故障:缓存系统出现重启、宕机或故障,导致缓存中的所有数据一时无法访问,请求直接访问数据库。

数据库压力:当缓存失效后,大量请求同时涌入数据库,因为数据库无法承受如此大的压力而导致性能下降。

问题

数据库压力过大:大量请求直接访问数据库,导致数据库处理能力不足,出现性能问题,甚至引发数据库崩溃。

  • 响应时间延长:由于缓存失效,请求需要直接访问数据库,导致响应时间延长。

解决方法

  1. 设置随机过期时间:为了避免大量缓存同时失效,可以为不同的缓存设置稍有差异的过期时间,分散缓存过期的可能性。

  2. 二级缓存机制:使用多级缓存,将数据同时存储到多个缓存层,一级缓存失效时可以从二级缓存中获取数据,避免所有缓存同时失效。

  3. 并发重建缓存:在缓存失效的时候,通过加锁或者分布式锁的方式,只允许一个请求去加载数据并重新构建缓存,其他请求等待并从缓存中获取数据。

  4. 缓存预热:在系统低峰期,提前加载热门的缓存数据,避免在高峰期同时加载大量缓存数据。

  5. 容灾备份:设置多个缓存节点,保证缓存的高可用性,一旦某个缓存节点发生故障,可以快速切换到其他节点。

  6. 异步更新缓存:对于热点数据,可以使用异步任务来更新缓存,避免大量的请求同时涌入数据库。

总结

综上所述,缓存雪崩是一种常见的缓存问题,在系统设计和缓存策略上采取合理的措施可以有效预防和处理缓存雪崩,提高系统的可用性和稳定性。

 

 

 

 

 

 

 

 

 

 

 

 

posted @ 2023-10-09 16:04  haohao2036  阅读(30)  评论(0编辑  收藏  举报