随笔 - 24  文章 - 0  评论 - 1  阅读 - 1179 

Redis

Redis基础

使用尚硅谷视频作为基础,其他经过资料收集

redis官网:https://redis.io/

redis中文网:http://www.redis.cn/

1、场景引入

1.1、技术发展

1.1.1、技术分类

解决功能性的问题:Java、JSP、Tomcat、HTML、Linux、JDBC

解决扩展性的问题:Spring、SpringMVC、MyBatis

解决性能的问题:NoSQL、Java线程、Hadoop、Nginx、MQ、ElasticSearch

1.1.2、时代演变

Web1.0时代:客户端 --》 Web服务器 --》数据库服务

Web2.0时代:用户访问量大幅度提升,产生了大量用户数据,智能移动设备(客户端)的普及,带来了服务器CPU及内存的压力,带来了数据库的IO压力。

1.1.3、解决CPU及内存压力

多个服务器:负载均衡

产生session问题,用户登录后session如何存储?

方案一:存在cookie中:不安全,网络负担效率低

方案二:存在文件服务器或者数据库里:存在大量IO的效率低问题

方案三:session复制:session数据冗余,节点越多浪费越大

方案四:缓存数据库(NoSQL):完全在内存中,速度快,数据结构简单

1.1.4、解决IO压力

缓存数据库:减少io的读操作

水平切分、垂直切分、读写分离,通过破坏一定的业务逻辑换取性能

打破了传统关系型数据库以业务逻辑为依据的存储模式,而针对不同数据结构类型改为以性能为最优先的存储方式。

1.2、NoSQL数据库

Not Only SQL,泛指非关系型数据库。

不依赖业务逻辑方式存储,以简单的key-value模式存储,大大增加了数据库的扩展能力。

不遵循SQL标注,不支持ACID,远超于SQL的性能。

1.2.1、适用场景

对数据高并发的读写

海量数据的读写

数据高扩展性

用不着sql的和用了 sql也不行的情况,可以考虑使用NoSQL

1.2.2、不适用场景

需要事务支持

基于SQL的结构化查询存储,处理复杂的关系,需要即席查询

1.2.3、常见NoSQL数据库

Memcache

  • 早期数据库

  • 数据都在内存中,一般不持久化

  • 支持简单的key-value模式,支持类型单一

  • 一般是作为缓存数据库辅助持久化的数据库

Redis

  • 几乎覆盖了Memcache的绝大部分功能

  • 数据都在内存中,支持持久化,主要用作备份恢复

  • 除了支持简单的key-value模式,还支持多种数据结构的存储,比如list、set、hash、zset等

  • 一般是作为缓存数据库辅助持久化的数据库

MongoDB

  • 高性能、开源、模式自由的文档型数据库

  • 数据都在内存中,如果内存不足,把不常用的数据库存到硬盘

  • 虽然是key-value模式,但是对value(尤其是json)提供了丰富的查询功能

  • 支持二进制数据及大型对象

  • 可以根据数据的特点替代RDBMS,成为独立的数据库,或者配合RDBMS,存储特定的数据

1.3、行式、列式数据库

行式数据库,每一行作为一条记录,每一列代表一种属性

进行id查询时效率快(OLTP事务性处理),进行特征查询时效率慢(OLAP分析型处理)

列式数据库,每一列(属性)作为一个数据块存储,不在意这条记录(行)的位置或顺序

进行属性查询时效率高

常见列式数据库:

HBase:Hadoop项目中的数据库,主要用于需要对大量的数据进行随机、实时的读写操作的场景中

Cassandra:免费开源的NoSQL数据库,用于管理由大量商用服务器构建起来的庞大集群的海量数据集(数据量通常达到PB级别)。在众多显著特性当中,最为卓越的长处是对写入及读取操作进行规模调整,而且其不强调主集群的设计思路能够以相对直观的方式简化各集群的创建与扩展流程。

1.4、图关系型数据库

Neo4j

主要应用:社会关系、公共交通网络、地图及网络拓扑

2、Redis概述和安装

Redis是一个开源的key-value存储系统

和Memcache类似,支持的value类型更多,包括string字符串、list链表、set集合、zset有序集合、hash哈希类型等,这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,且均是原子性的,且支持不同方式的排序,数据都是缓存在内存中

区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件

实现了master-slave(主从)同步

2.1、应用场景

配合关系型数据库做高速缓存:高频次、热门访问的数据,降低数据库IO;分布式架构,做session共享

存储持久化数据:

  • 最新N个数据:List实现按自然时间排序

  • 排行榜:zset有序集合

  • 时效性的数据,如手机验证码:expires过期

  • 计数器,如秒杀:保障原子性的自增自减方法INCR、DECR

  • 去除大量数据的重复数据:set集合

  • 构建队列:list集合

  • 发布订阅信息系统:pub/sub模式

2.2、Redis安装

默认在Linux下处理,不用考虑Windows的支持

官网下直接下载,按最新版本即可,压缩包放入linux的/opt目录下

准备工作:安装c语言的编译环境,安装最新的gcc编译器yum install gcc

 yum install centos-release
 yum install -y devtoolset-8-toolstain
 scl enable devtoolset-8 bash

测试gcc版本

 gcc --version

解压命令,解压对应版本压缩包,最新6.2.6

 tar -zxvf redis-6.2.6.tar.gz
 cd redis-6.2.6
 make # 编译
 make install

安装目录:默认为/usr/local/bin

推荐后台启动

拷贝redis.conf文件到/etc目录下

 cp redis.conf /etc/redis.conf

后台启动设置daemonize no改为yes

进入/usr/local/bin目录下,输入redis-server /etc/redis.conf加载配置(带修改后配置文件)

启动redis,看到下一行出现表示启动成功

 redis-cli
 127.0.01:6379>

ps -ef | grep redis 查看进程是否启动

关闭时可以使用kill杀掉进程,或者进入redis-cli后使用shutdown

以后的redis命令都需要进入redis-cli后进行

2.3、Redis介绍

默认16个数据库,类似数组下标从0开始,初始默认使用0号库

统一密码管理,所有库使用同样密码

Redis是单线程+多路IO复用技术

多路复用是指使用一个线程来检查多个文件描述符(Socket)的就绪状态,比如调用select和poll函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞直到超时。得到就绪状态后进行真正的操作可以在同一个线程里执行,也可以启动线程执行(比如使用线程池)

3、常用五大数据类型

3.1、Redis键(key)

key - value 键key值value对

创建一个key-value:set key value,选择相同的key会将之前的value覆盖

获得key对应的value:get key

keys * 查看当前库所有key

exists key 判断某个key是否存在

type key 查看key类型

del key 删除指定的key数据

unlink key 根据value选择非阻塞删除,仅将keys从keyspace元数据中删除,真正删除需要后续异步操作

expire key 10 为给定的key设置过期时间10s

ttl key 查看还有多少秒过期,-1表示永不过期,-2表示已过期

dbsize 查看当前数据库的key的数量

flushdb 清空当前库

flushall 通杀全部库

使用命令select <dbid>切换数据库,如select 8

3.2、字符串String

最基本类型,一个key对应一个value

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

一个redis字符串value最多可以是512M

3.2.1、常用命令

set key value 参数(大小写不敏感)

部分参数也可以写成set参数 key value,如setnx、setex

  • NX(not exists):当数据库不存在时,可以将key-value添加数据库

  • XX:当数据库中key存在时,可以将key-value添加数据库,与NX互斥

  • EX:key的超时秒数

  • PX:key的超时毫秒数,与EX互斥

  • setex key 过期时间 value 设置键值的同时,设置过期时间,单位为秒

append key value 将给定的value追加到原值的末尾,如果原值不存在,则新建一个key-value

strlen key 获得值的长度

setnx key value 只有在key不存在时,才设置key的值value

incr key 将key中存储的数字值增1,只能对数字值操作,如果为空,新增值为1

decr key 将key中存储的数字值减1,只能对数字值操作,如果为空,新增值为-1

incrby/decrby key 定义步长

上述自增自减是原子性的操作。所谓原子操作是指不会被线程调度机制打断的操作,这种该操作一旦开始,就一直运行到结束,中间不会有任何context switch(切换到另一个线程)

在单线程中,能够在单条指令中完成的操作都可以认为是“原子操作”,因为中断只能发生于指令之间

在多线程中,不能被其他进程(线程)打断的操作就叫原子操作

Redis单命令的原子性主要得益于Redis的单线程

注:Java中的i++不是原子操作

如:i=0,两个线程分别对i进行++100次,值的取值范围为2~200

极端2的出现情况:i++表示先取i的值然后+1,第一个最先执行的线程完成99次i++后,做第100次+1操作时取完值后被第二个线程打断,此时第二个线程正好完成一次+1操作(此时i的值为1),然后该线程取i的值执行+1操作,得到结果为2。完成循环后结束。

mset key1 value1 key2 value2 …… 同时设置一个或多个key-value对

mget key1 value1 key2 value2 …… 同时获取一个或多个value

msetnx key1 value1 key2 value2 …… 同时设置一个或多个key-value对时,当且仅当所有给定key不存在

批量操作保证原子性,有一个失效则全部都失败

getrange key 起始位置 结束位置 获得值的范围,类似java的substring,有前包和后包,对值的内容进行截取

setrange key 起始位置 value 用value覆写key存储的字符串值,位置是从起始位置开始(索引从0开始)

getset key vale 以新换旧,设置了新值同时获得旧值

3.2.2、数据结构:简单动态字符串

string的数据结构为简单动态字符串,是可以修改的字符串,内部结构类似Java的ArrayList,采用预分配冗余空间的方式减少内存的频繁分配。当前字符串实际分配的空间capacity一般要高于实际字符串长度length。当字符串长度小于1M时,扩容都是加倍现有的空间;如果超过1M,扩容时只会多扩1M的空间。字符串最大长度为512M。

3.3、列表List

单键多值

简单的字符串列表,按照插入顺序排序,可以添加一个元素到列表的头部(左边)或者尾部(右边)

底层是双向链表,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差

3.3.1、常用命令

lpush/rpush key value1 value2 value3 …… 从左边/右边插入一个或多个值

lpush插入顺序与读取顺序相反(头插法)

lpop/rpop key 从左边/右边弹出一个值,值在键在,值光键亡

rpoplpush key1 key2 从key1列表右边弹出一个值,插到key2列表的左边

lrange key startIndex stopIndex 按照索引下标获得元素(从左到右)

stopIndex取-1表示全选

lindex key index 按照索引下标获取指定元素(从左到右)

llen key 获得列表长度

linsert key before value newValue 在value的后面插入newValue新值

lrem key n value 从左边删除n个value(从左到右)

lset key index value 将列表key下表为index的值替换成value

3.3.2、数据结构:快速列表

list的数据结构为快速列表quickList

首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是ziplist,也即是压缩列表

它将所有的元素紧挨着一起存储,分配的是一块连续的内存

当数据量比较多的时候才会改成quickList

因为普通的链表需要的附加指针空间太大(需要增加前指针、后指针),会比较浪费时间。

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

3.4、集合Set

与list相似,是一个列表的功能。特殊在于set可以自动排重,用于存储一个元素不重复的列表数据。

此外set提供了判断某个成员是否在一个set集合内的重要接口,是list不能提供的

set是string类型的无序集合,底层是一个value为null的hash表,添加、删除、查找的时间复杂度都是o(1),即使数据增加,查找数据的时间不会变。

3.4.1、常用命令

sadd key value1 value2 …… 将一个或多个member元素加入到集合key中,已经存在的member元素将被忽略

smembers key 取出该集合的所有值

sismember key value 判断集合key是否含有该value值,有返回1,无返回0

scard key 返回该集合的元素个数

srem key value1 value2 …… 删除集合中的某个元素

spop key 随机从该集合中弹出一个值(并从集合中删除)

srandmember key n 随机从该集合中取出n个值,但不会从集合中删除

smove source destination value 把集合中一个值从一个集合移动到另一个集合

sinter key1 key2 返回两个集合的交集元素

sunion key1 key2 返回两个集合的并集元素

sdiff key1 key2 返回两个集合的差集元素(key1中不包含key2的元素)

3.4.2、数据结构:字典

set数据结构是字典dict,使用哈希表实现

Java中的HashSet的内部实现使用的是HashMap,只不过所有的value都指向同一个对象。

Redis的set结构类似,内部使用hash结构,所有的value都指向同一个内部值

3.5、哈希(Hash)

Hash是一个键值对集合

是一个string类型的field和value的映射表,特别适合用于存储对象

keyvalue
  field value
  birthday 2000-1-1
userId name 张三
  age 20

类似Java中的Map<String,Object>

用户id为查找的key,存储的value对象包含姓名、年龄、生日等信息。

存储方式推出Hash的演变

第一种:

user1:{birthday=2000-1-1,name=zhangsan,age=20}

查找单条记录速度快,但进行属性查找时较慢

每次修改用户的某个属性需要先反序列化修改后再重新序列化,开销较大

第二种:

user1:birthday 2000-1-1

user1:name zhangsan

user1:age 20

按照属性查找速度快,可以得到一类记录,但查找单一条件精准记录较慢

用户id数据冗余

第三种:Hash

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

3.5.1、常用命令

hset key field value 给key集合中的field键赋值

hget key1 field 从key1集合field中取出value

hmset key1 field1 value1 field2 value2 …… 批量设置hash的值(新版本hset已经能够实现批量操作)

hexists key1 field 查看哈希表key中,给定域field是否存在

hkeys key 列出该hash集合的所有field

hvals key 列出该hash集合的所有value

hincrby key field increment 为哈希表key中的域field的值加上增量1或-1

hsetnx key field value 将哈希表key中的域field的值设置为value,当且仅当域field不存在

3.5.2、数据结构

对应两种:ziplist(压缩列表)、hashtable(哈希表),当field-value长度较短且个数较少时使用前者,否则是后者

3.6、有序集合Zset(Sorted Set)

与set非常相似,是一个没有重复元素的字符串集合

不同之处是有序集合的每个成员都关联了一个评分(score),这个评分被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是位移的,但是评分可以是重复的。

因为元素是有序的,所以可以很快的根据评分或者次序(position)获取一个范围内的元素。

访问有序集合中间元素也非常快,因此可以使用Zset作为一个没有重复成员的智能列表。

3.6.1、常用命令

zadd key score1 value1 score2 value2 …… 将一个或多个member元素及其score值加入到有序集合key中

zrange key start stop [withscores] 返回有序集合key中下标在start到stop之间的元素(带withscores可以将分数一同返回)

zrangebyscore key minmax [withscores] [limit offset count] 返回有序集合key中,所有score值介于min和max之间(闭区间)的成员

zrevrangebyscore key maxmin [withscores] [limit offset count] 同上,改为从大到小排列

zincrby key increment value 为value元素的score加上增量

zrem key value 删除该集合下,指定值的元素

zcount key min max 统计该集合,分数区间内的元素个数

zrank key value 返回该值在集合中的排名,从0开始

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

 [root@k8s-master01 ~]# docker exec -it redis redis-cli
 127.0.0.1:6379> zadd topn 1000 v1 2000 v2 3000 v3
 (integer) 3
 127.0.0.1:6379> zrevrange topn 0 9 withscores
 1) "v3"
 2) "3000"
 3) "v2"
 4) "2000"
 5) "v1"
 6) "1000"

3.6.2、数据结构

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

使用了两个数据结构:

  • hash:作用是关联元素value和权重score,保障元素value的唯一性,可以通过value找到相应的score值

  • 跳跃表:给元素value排序,根据score的范围获取元素列表

3.6.3、跳跃表(跳表)

参考文档:https://www.jianshu.com/p/9d78d8719afe

https://www.cnblogs.com/hunternet/p/11248192.html

跳跃表是在单链表的基础上在选取部分结点添加索引,这些索引在逻辑关系上构成了一个新的线性表,并且索引的层数可以叠加,生成二级索引、三级索引、多级索引,以实现对结点的跳跃查找的功能。

与二分查找类似,跳跃表能够在 O(㏒n)的时间复杂度之下完成查找,与红黑树等数据结构查找的时间复杂度相同,但是相比之下,跳跃表能够更好的支持并发操作,而且实现这样的结构比红黑树等数据结构要简单、直观许多。

对于一个单链表来讲,即便链表中存储的数据是有序的,如果我们要想在其中查找某个数据,也只能从头到尾遍历链表。这样查找效率就会很低,时间复杂度会很高,是 O(n)。

如果我们想要提高其查找效率,可以考虑在链表上建索引的方式。每两个结点提取一个结点到上一级,我们把抽出来的那一级叫作索引。

 

这个时候,我们假设要查找节点8,我们可以先在索引层遍历,当遍历到索引层中值为 7 的结点时,发现下一个节点是9,那么要查找的节点8肯定就在这两个节点之间。我们下降到链表层继续遍历就找到了8这个节点。原先我们在单链表中找到8这个节点要遍历8个节点,而现在有了一级索引后只需要遍历五个节点。

从这个例子里,我们看出,加来一层索引之后,查找一个结点需要遍的结点个数减少了,也就是说查找效率提高了,同理再加一级索引。

从图中我们可以看出,查找效率又有提升。在例子中我们的数据很少,当有大量的数据时,我们可以增加多级索引,其查找效率可以得到明显提升。

像这种链表加多级索引的结构,就是跳跃表!

跳跃表在链表的基础上增加了多级索引以提升查找的效率,但其是一个空间换时间的方案,必然会带来一个问题——索引是占内存的。原始链表中存储的有可能是很大的对象,而索引结点只需要存储关键值值和几个指针,并不需要存储对象,因此当节点本身比较大或者元素数量比较多的时候,其优势必然会被放大,而缺点则可以忽略。

4、Redis配置文件

一般目录在/etc/redis.conf,使用vim可查看编辑

4.1、Unit单位

 ## redis中的度量单位只支持bytes,不支持bit,大小写不敏感,且 k/kb、m/mb、g/gb 代表的单位大小有所不同。
 # 1k => 1000 bytes
 # 1kb => 1024 bytes
 # 1m => 1000000 bytes
 # 1mb => 1024*1024 bytes
 # 1g => 1000000000 bytes
 # 1gb => 1024*1024*1024 bytes

配置大小单位,开头定义了一些基本的度量单位,只支持bytes,不支持bit

大小写不敏感

4.2、Includes、Modules导包

 ################################## INCLUDES ###################################
 
 # 用于引入其他配置文件,和本配置文件中配置共同配置redis服务
 # include /path/to/local.conf
 # include /path/to/other.conf
 
 ################################## MODULES #####################################
 
 # Load modules at startup. If the server is not able to load modules
 # it will abort. It is possible to use multiple loadmodule directives.
 #
 # loadmodule /path/to/my_module.so
 # loadmodule /path/to/other_module.so

类似于jsp中的include,多实例时可以把公共的配置文件提取出来

可以加载模块module

4.3、Network网络

参考文档:https://www.cnblogs.com/DeepInThought/p/10704764.html

################################## NETWORK #####################################
#
# Examples:
#
# bind 192.168.1.100 10.0.0.1
# bind 127.0.0.1 ::1
bind 127.0.0.1
protected-mode yes
# 服务启动端口,默认为 6379 ,如果设置端口为 0 ,redis 服务将不会监听任何TCP连接
port 6379
# 设置TCP的连接队列 backlog ,默认为 511 ,backlog 队列总和 = 未完成三次握手队列 + 已完成三次握手队列
# 作用:在高并发环境中,设置高 backlog 值,来避免慢客户端的连接
# 设置方式:(1)设置 /proc/sys/net/core/somaxconn (2)设置 tcp_max_syn_backlog
# Linux内核会将 tcp_max_syn_backlog 的值减小到 somaxconn ,故需要同时设置
# tcp_max_syn_backlog 1024
tcp-backlog 511
# 当客户端超过N秒空闲后,服务器主动断开连接,设置为 0 表示不主动断开连接
timeout 0
# 检测 TCP 连接alive状态的频率,设置为0表示不检测,建议设置为60,单位:秒
tcp-keepalive 300

4.3.1、bind

默认情况下bind=127.0.0.1只能接受本机的访问请求

不写的情况下,无限制接受任何ip地址的访问

生产环境写应用服务器的地址,需要远程访问的就注释掉

如果开启了protected-mode,那么在没有设定bind ip且没有设置密码的情况下,Redis只允许接受本机的响应。

4.3.2、tcp-backlog

设置tcp的backlog:连接队列。backlog 队列总和 = 未完成三次握手队列 + 已完成三次握手队列

在高并发环境中,设置高 backlog 值,来避免慢客户端的连接

Linux内核会将 tcp_max_syn_backlog 的值减小到 somaxconn ,故需要同时设置增大/proc/sys/net/core/somaxconn(128)和/proc/sys/net/ipv4/tcp_max_syn_backlog(128)达到想要的效果。

4.4、General通用

################################# GENERAL #####################################

# 设置redis服务以后台守护进程启动,默认为 no 非daemon
# 后台启动后服务的 pid 位于文件 /var/run/redis 中
daemonize no

#如果您从upstart或systemd运行Redis,Redis可以与监控树交互。选项:
#supervised no-无监督交互
#supervised upstart-通过将Redis置于SIGSTOP模式发出启动信号
#supervised auto-套接字环境变量的监督自动检测upstart或systemd方法
#注:这些监督方法仅表示“过程已准备就绪”
#它们不能将连续的活动ping返回给主管。
supervised no

#如果指定了pid文件,Redis会在启动时将其写入指定的位置,并在退出时将其删除。
#当服务器运行非守护进程时,如果配置中未指定任何pid文件,则不会创建任何pid文件。服务器被守护时,即使未指定pid文件,也会使用该文件,默认为“/var/run/redis.pid”。
#创建一个pid文件是最好的努力:如果Redis无法创建它,那么不会发生任何错误,服务器将正常启动并运行。
pidfile /var/run/redis_6379.pid

# 设置输出日志级别,包括 debug详细信息、verbos有用信息、notice重要信息、warning警告信息
loglevel notice

# 设置日志输出文件,若为空字符串"" 或者 stdout ,则将日志从定位到 /dev/null
logfile ""

# 若要将redis日志记录到系统日志,将此参数设置为 yes
# syslog-enabled no

# 用于指定日志标识:若开启系统日志,指定日志以 redis 开头
# syslog-ident redis

# 设置系统日志输出设备,值可为 USER 或者 LOCAL0-LOCAL7,默认为local0
# syslog-facility local0

# 设置 redis 服务启动数据库的个数,默认 16 个,默认数据库为 DB 0 ,可使用 select <dbid> 进行切库,dbid为 0 至 databases-1
databases 16

# 是否总是显示redis那个"蛋糕"logo
always-show-logo yes

################################ SNAPSHOTTING ################################
# 指定在规定时间内,有多少此更新操作,就将数据同步至数据文件,可多个条件配合使用
# save <seconds> <changes>
#  save ""
# 默认配置如下:15分钟内有一个更改,5分钟内有10个更改,1分钟内有10000个更改

save 900 1
save 300 10
save 60 10000

# 当RDB在后台持久化出错后,是否依然进行数据库写操作,yes:停止写操作,no:继续写操作
stop-writes-on-bgsave-error yes

# 指定存储至本地数据库时是否压缩数据,默认为yes:使用压缩,redis采用LZF压缩算法,使用会消耗CPU,不使用占内存
rdbcompression yes

# 是否校验压缩后的rdb文件,默认为yes:进行校验,开启大概有10%性能损耗
rdbchecksum yes

# 指定本地数据库文件名,默认为dump.rdb
dbfilename dump.rdb

# 指定本地数据文件存放目录
dir ./

4.5、Security安全

################################## SECURITY ###################################

# 登录 redis 数据库密码认证问题
# 执行命令 config get requirepass ,获取配置文件中默认认证密码,默认密码为 foobared

# requirepass foobared

# 执行命令 config set requirepass "redis" ,设置认证密码为 redis ,设置为空 "" 表示不认证
# 在执行命令前使用 auth <password> 命令进行认证

# 禁止远程修改 DB 文件地址,就是对命令进行权限控制
# 将命令重命名为空 "",表示禁用该命令
# rename-command FLASHALL ""
# 也可将命令重命名为 qazwsx741852edc ,然后将此名授权给特定用户使用即可
# rename-command CONFIG "qazwsx741852edc"

示例:

[root@k8s-master01 ~]# docker exec -it redis redis-cli
127.0.0.1:6379> config get requirepass
1) "requirepass"
2) ""
127.0.0.1:6379> config set requirepass 123456
OK
127.0.0.1:6379> config get requirepass
1) "requirepass"
2) "123456"
127.0.0.1:6379> exit
[root@k8s-master01 ~]# docker exec -it redis redis-cli
127.0.0.1:6379> auth 123456
OK

4.6、客户端Client

limits限制部分放在client中

################################### CLIENTS ####################################

# 设置客户端默认最大连接数,设置为0表示不限制,默认为10000
# maxclients 10000

############################## MEMORY MANAGEMENT ################################

# 设置 redis 最大内存容量
# maxmemory <bytes>

# 内存达到上限的处理策略(lru means Least Recently Used<最近最少>,lfu means Least Frequently Used<最不常>)
# volatile-lru -> 利用LRU算法移除设置过过期时间的key(最近最少使用的)
# allkeys-lru -> 在所有集合key中,利用LRU算法移除任何key
# volatile-lfu -> 在过期集合中移除随机的key,利用LFU算法移除设置过过期时间的key
# allkeys-lfu -> 在所有集合key找那个,利用LFU算法移除任何key
# volatile-random -> 随机移除设置过过期时间的key
# allkeys-random -> 随机移除所有key
# volatile-ttl -> 移除即将过期的key,ttl最小的
# noeviction -> 不移除key,返回报错就行
# 默认为不移除key策略
# maxmemory-policy noeviction

# 设置每次移除时的样本大小,默认5个:每次移除时选取5个样本,移除其中符合策略的key
# maxmemory-samples 5

# 从节点是否忽略maxmemory设置的值
# replica-ignore-maxmemory yes

4.6.1、maxmemory

最大内存容量,建议必须设置,否则,将内存占满,将会造成服务器宕机

设置redis可以使用的内存量,一旦到达内存使用上限,redis将会试图移除内部数据,移出规则通过maxmemory-policy指定

4.6.2、maxmemory-samples

设置样本数量,LRU和最小TTL算法都不是精确的算法,而是估算值,所以需要设置样本的大小,Redis会默认检查这么多个key并选择其中LRU的那个key

一般设置范围为3~7,数值越小样本越不准确,但性能消耗越小

5、Redis的发布和订阅

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

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

给频道发布信息后,消息就会发送给订阅的客户端

命令行实现

1、打开一个客户端订阅channel1

127.0.0.1:6379> SUBSCRIBE channel1
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "channel1"

正在监听,没有返回

2、打开另一个客户端(同一台主机,另外一个连接),给channel1发布消息hello,返回的1是订阅者数量

可以重复发送信息

127.0.0.1:6379> publish channel1 hello
(integer) 1

3、打开第一个客户端可以看到发送的消息

127.0.0.1:6379> SUBSCRIBE channel1
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "channel1"
3) (integer) 1
1) "message"
2) "channel1"
3) "hello"

继续监听下一个结果

发布的消息没有持久化,未订阅的客户端收不到hello,只能收到订阅后发布的消息

6、Redis新数据类型

6.1、Bitmaps

现代计算机用二进制(位)作为信息的基础单位,1个字节等于8位,例如”abc“字符串就是由3个字节组成,但实际在计算机存储时将其用二进制表示。”abc“对应的ASCII码为97、98、99,对应二进制是01100001,01100010,01100011.合理利用操作位能够有效提高内存使用率和开发效率。Redis就提供了Bitmaps实现对位的操作。

Bitmaps本身不是数据类型,实际是字符串key-value,但是可以对字符串的位进行操作。可以把Bitmaps想象为一个以位为单位的数组,数组的每个单元只能存储0或1,数组的下标在Bitmaps中叫作偏移量。

6.1.1、命令

setbit key offset value 设置Bitmaps中某个偏移量的值(0或1)

offset偏移量从0开始,返回结果为0表示原来此位是0,1表、

getbit key offset 获取Bitmaps中某个偏移量的值

bitcount key [start] [end] 统计字符串被设置为1的bit数,不加start和end则全选,加了可以在特定的位进行。设置-1表示最后一个位,-2表示倒数第2个位。start、end是指bit组的字节的下标数。

setbit设置或清除的是bit位置,而bitcount计算的是byte位置(1byte有8bit)

bitop and(or/not/xor) destkey key…… 复合操作,做多个Bitmaps的and(交集)、or(并集)、not(非)、xor(异或)操作并将结果保存在destkey中

6.1.2、实例

实例1:每个独立用户是否访问过网站存放在Bitmaps中,将访问的用户记1,未访问过的记0,偏移量为用户的id

很多应用的用户id以一个指定数字开头(如10000),直接将用户id和Bitmaps的偏移量对应势必会造成一定的浪费,通常做法是每次做setbit操作时将用户id减去这个指定数字。

在第一次初始化Bitmaps时,加入偏移量非常大,那么整个初始化过程执行会比较慢,可能会造成Redis的阻塞。

不存在的用户查询为0

实例2:构建k1 [01000001 01000000 00000000 00100001],对应字节编号为0,1,2,3

bitcount k1 1 2 统计下标1、2字节组中的bit=1的个数,即01000000、00000000,有1个1,输出1

bitcount k1 1 3 统计下标1、2、3字节组中的bit=1的个数,即01000000、00000000、00100001,有3个1,输出3

bitcount k1 0 -2 统计下标从0开始到倒数第2个字节组中的bit=1的个数,即01000001、01000000、00000000,有3个1,输出3

[root@k8s-master01 ~]# docker exec -it redis redis-cli
127.0.0.1:6379> setbit k1 1 1
(integer) 0
127.0.0.1:6379> setbit k1 9 1
(integer) 0
127.0.0.1:6379> setbit k1 26 1
(integer) 0
127.0.0.1:6379> setbit k1 31 1
(integer) 0
127.0.0.1:6379> bitcount k1
(integer) 5
127.0.0.1:6379> bitcount k1 1 2
(integer) 1
127.0.0.1:6379> bitcount k1 1 3
(integer) 3
127.0.0.1:6379> bitcount k1 0 -2
(integer) 3

注意:Redis的setbit设置或清除得到是bit位置,而bitcount计算的是byte位置

实例3

计算两天都访问网站的用户数量,结果放在key:and中

bitop and key:and key1 key2

只返回数量,不返回用户id

6.1.3、Bitmaps和Set对比

假设网站有1亿用户,每天独立访问的用户有5千万,如果每天用集合类型和Bitmaps分别粗出活跃用户可以得到内存消耗

数据类型每个用户id占用空间需要存储的用户量全部内存量
集合类型 64位 50000000 64*50000000=400MB
Bitmaps 1位 100000000 1*100000000=12.5MB

使用Bitmaps能节省许多内存空间

但Bitmaps并不是万金油,假如该网站每天的独立访问用户很少,只有10万(存在大量的僵尸用户),使用Bitmaps不合适,因为大部分都是0

数据类型每个用户id占用空间需要存储的用户量全部内存量
集合类型 64位 100000 64*100000=800kB
Bitmaps 1位 100000000 1*100000000=12.5MB

Set存的人数可变,但Bitmaps是固定的,专门进行位操作

6.2、HyperLogLog

6.2.1、基数问题

统计独立访客UV(Unique Vistors)、独立IP数、搜索记录等需要去重和计数的问题,需要求集合中不重复元素个数,称为基数问题

解决方案有:

  • MySQL使用distinct count计算不重复个数

  • 使用Redis提供的hash、set、Bitmaps等数据结构处理

以上方案精确,但随着数据不断增加,导致占用空间越来越大,对于非常大的数据集不宜使用

Redis HyperLogLog是用来做基数统计的算法,优点是,在输入元素的数量或者体积非常大时,计算基数所需的空间总是固定并且保持在一个较小的值。

1个HyperLogLog键只需要花费12kb的内存,就可以计算接近264 个不同元素的基数。这和计算基数时,元素越多耗费内存越多的集合不同。

但是,因为HyperLogLog只会根据输入元素来计算基数,而不会存储输入元素本身,所以HyperLogLog不能像集合一样返回输入的各个元素。

基数:如数据集{1,3,5,7,5,7,8},对应的基数集为{1,3,5,7,8},基数(不重复元素个数)为5.基数估计就是在误差可接受范围内,快速计算基数。

6.2.2、命令

pfadd key element1 element2 …… 添加指定元素到HyperLogLog中

添加成功,使得HyperLogLog估计的近似基数发生变化(之前没有重复元素)则返回1,否则返回0

pfcount key1 key2 …… 计算HyperLogLog计算的近似基数,可以计算多个HyperLogLog。

比如存储每天的UV,计算一周的UV可以使用7天的UV合并计算。

pfmerge destkey sourcekey1 sourcekey2 …… 将一个或多个HyperLogLog合并后的结果存储在另一个HyperLogLog中,比如每月活跃用户可以使用每天的活跃用户合并计算

6.3、Geospatial

Redis 3.2 增加了对GEO(Geographic,地理信息) 类型的支持,对应元素的2维坐标,在地图上即对应经纬度。提供 了经纬度设置,查询,范围查询,距离查询,经纬度Hash等常见操作。

geoadd key longitude1 latitude1 member1 longitude2 latitude2 member2 …… 添加地理位置(经度、纬度、名称)

有效的经度为-180到180度,有效的纬度从-85.05112878度到85.05112878度之间。两极部分地区无法直接添加,一般会下载相应城市的数据,直接通过Java程序一次性写入。当坐标位置超出指定范围时,返回一个错误,否则返回正常输入的位置个数。已经添加的数据块无法重复添加。

geopos key member1 member2 …… 获得指定地区的坐标值

geodist key member1 member2 [m|km|ft|mi] 获取两个位置之间的直线举例,单位可设置

mi英里,ft英尺,不设置时默认为m

georadius key longitude latitude radius [m|km|ft|mi] 以给定的经纬度为中心,找出某一半径内的元素

7、Jedis操作Redis

创建一个空Maven项目

导入依赖

复制代码
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.2.0</version>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13</version>
    <scope>compile</scope>
</dependency>
复制代码

测试是否与虚拟机的客户端连接成功。

复制代码
package com.zhou.jedis;

import redis.clients.jedis.Jedis;

/**
 * @Name JedisDemo1
 * @Description
 * @Author 88534
 * @Date 2021/12/18 13:42
 */
public class JedisDemo1 {
    public static void main(String[] args) {
        // 创建Jedis对象
        Jedis jedis = new Jedis("192.168.56.100",6379);
        // 测试
        String value = jedis.ping();
        System.out.println(value);
    }
}
复制代码

返回PONG表示连接成功

连不上的可能原因有:

bind设置未注释,保护者模式为yes,防火墙打开(查看systemctl status firewalld),未开启6379端口等等

7.1、测试相关数据类型

连接redis,直接参照客户端的Redis命令,完成后关闭

Jedis jedis = new Jedis("192.168.56.100",6379);//虚拟机IP,端口号
// TODO redis command
jedis.close();

7.1.1、key

复制代码
/**
 * 操作key
 */
@Test
public void demo1(){
    Jedis jedis = new Jedis("192.168.56.100",6379);

    // 添加
    jedis.set("name","zhou");

    // 获取
    String name = jedis.get("name");
    System.out.println(name);
    
    // 设置多个key-value
    jedis.mset("k1","v1","k2","v2");
    List<String> mget = jedis.mget("k1", "k2");
    System.out.println(mget);

    // 获取所有key(keys *)
    Set<String> keys = jedis.keys("*");
    for (String key : keys) {
        System.out.println(key);
    }
    
    jedis.close();
}
复制代码

7.1.2、List

复制代码
/**
 * 操作List
 */
@Test
public void demo2(){
    Jedis jedis = new Jedis("192.168.56.100",6379);
    
    jedis.lpush("name:list","zhou","zhang","wang");
    List<String> values = jedis.lrange("name:list", 0, -1);
    for (String value : values) {
        System.out.println(value);
    }
    
    jedis.close();
}
复制代码

7.1.3、Set

复制代码
/**
 * 操作Set
 */
@Test
public void demo3(){
    Jedis jedis = new Jedis("192.168.56.100",6379);

    jedis.sadd("name:set","zhang","wang");
    Set<String> name = jedis.smembers("name:set");
    for (String s : name) {
        System.out.println(s);
    }
    
    jedis.close();
}
复制代码

7.1.4、Hash

复制代码
/**
 * 操作Hash
 */
@Test
public void demo4(){
    Jedis jedis = new Jedis("192.168.56.100",6379);

    jedis.hset("users","age","20");
    String hget = jedis.hget("users", "age");
    System.out.println(hget);
    
    jedis.close();
}
复制代码

7.1.5、Zset

复制代码
/**
 * 操作Zset
 */
@Test
public void demo5(){
    Jedis jedis = new Jedis("192.168.56.100",6379);

    jedis.zadd("China",100,"Shanghai");
    Set<String> china = jedis.zrange("China", 0, -1);
    for (String s : china) {
        System.out.println(s);
    }
    
    jedis.close();
}
复制代码

7.2、应用案例:手机验证码

要求:

  • 输入手机号,点击发送后随机生成6位数字码,2分钟有效

  • 输入验证码,点击验证,返回成功或失败

  • 每个手机号每天只能输入3次

思路:

  • Random生成随机码,把验证码放在redis里,设置过期时间120秒

  • 取出验证码进行判断是否一致

  • 每次发送后incr,判断大于2时(从0开始),提交不能发送

实现:

复制代码
package com.zhou.jedis;

import redis.clients.jedis.Jedis;

import java.util.Random;

/**
 * @Name PhoneCode
 * @Description
 * @Author 88534
 * @Date 2021/12/18 15:30
 */
public class PhoneCode {
    public static void main(String[] args) {
        sendCode("18888888888",getCode());
        verifyCode("18888888888","617492");

    }

    /**
     * 1、生成6位数字验证码
     * @return
     */
    public static String getCode(){
        Random random = new Random();
        String code = "";
        for (int i = 0; i < 6; i++) {
            int rand = random.nextInt(10);
            code += rand;
        }
        return code;
    }

    /**
     * 2、传入验证码
     * 每个手机每天只能发送三次,验证码放到redis中,设置过期时间
     * @param phone
     * @param code
     */
    public static void sendCode(String phone, String code){
        // 连接redis
        Jedis jedis = new Jedis("192.168.56.101",6379);

        // 拼接key
        // 手机发送次数key
        String countKey = "VerifyCode:" + phone + ":count";
        // 验证码key
        String codeKey = "VerifyCode:" + phone + ":code";

        // 每个手机每天只能发送三次
        String count = jedis.get(countKey);
        if (count == null) {
            // 没有发送次数,第一次发送,设置发送次数为1,过期时间为1天
            jedis.setex(countKey, 24*60*60,"1");
        } else if (Integer.parseInt(count)<=2) {
            // 发送次数+1
            jedis.incr(countKey);
        } else if (Integer.parseInt(count)>2){
            // 发送三次,不能再发送
            System.out.println("今天发送次数已经超过三次");
            jedis.close();
            return;
        }

        // 发送验证码到redis中
        jedis.setex(codeKey,120,code);

        jedis.close();
    }

    /**
     * 验证码校验
     * @param phone
     * @param code
     */
    public static void verifyCode(String phone,String code){
        // 从redis获取验证码
        Jedis jedis = new Jedis("192.168.56.101",6379);

        String codeKey = "VerifyCode:" + phone + ":code";
        String redisCode = jedis.get(codeKey);

        // 判断
        if (code.equals(redisCode)) {
            System.out.println("成功");
        } else {
            System.out.println("失败");
        }

        jedis.close();
    }
}
复制代码

7.3、整合SpringBoot

新建一个SpringBoot项目,引入redis依赖和web支持

<dependency>
复制代码
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--        spring2.X集成redis需要commons-pool2-->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
</dependency>
复制代码

application.properties配置

#Redis服务器地址
spring.redis.host=192.168.56.101
#Redis服务器连接端口
spring.redis.port=6379
#Redis数据库索引(默认为0)
spring.redis.database=0
#连接超时时间(单位:毫秒)
spring.redis.timeout=1800000
#连接池最大连接数(使用负值表示没有限制)
spring.redis.lettuce.pool.max-active=20
#最大阻塞等待时间(使用负值表示没有限制,单位:毫秒)
spring.redis.lettuce.pool.max-wait=-1
#连接池中的最大空闲连接
spring.redis.lettuce.pool.max-idle=5
#连接池中的最小空闲连接
spring.redis.lettuce.pool.min-idle=0

配置类RedisConfig

复制代码
package com.zhou.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

/**
 * @Name RedisConfig
 * @Description
 * @Author 88534
 * @Date 2021/12/18 21:39
 */
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {

    /**
     * Redis模板管理
     * @param factory
     * @return
     */
    @Bean
    public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String,Object> template = new RedisTemplate<>();
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance ,
                ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        template.setConnectionFactory(factory);
        // key序列化方式
        template.setKeySerializer(redisSerializer);
        // value序列化方式
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        return template;
    }

    /**
     * 缓存管理
     * @param factory
     * @return
     */
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory){
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        // 解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance ,
                ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // 配置序列化(解决乱码的问题),过期时间为600s
        RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(600))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                .disableCachingNullValues();
        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(configuration)
                .build();
        return cacheManager;
    }
}
复制代码

简单测试Controller

复制代码
package com.zhou.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @Name RedisTestController
 * @Description
 * @Author 88534
 * @Date 2021/12/18 22:00
 */
@RestController
@RequestMapping("/redisTest")
public class RedisTestController {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @GetMapping
    public String testRedis(){
        // 设置值到redis
        redisTemplate.opsForValue().set("name","zhou");
        // 从redis获取值
        return redisTemplate.opsForValue().get("name");
    }
}
复制代码

Operations For 对各类对象执行各项命令对应的方法

运行工程

输入http://localhost:8080/redisTest

返回zhou


Redis进阶

8、Redis事务

8.1、Redis事务

Redis事务是一个单独的隔离操作,事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

Redis事务的主要作用是串联多个命令,防止别的命令插队。

多个命令当成一个整体,中途不允许CPU中断去执行其他命令,必须等待这些命令执行完了才可以去执行事务外的命令。

8.2、Multi、Exec、discard

从输入Multi命令开始,输入的命令都会一次进入命令队列中,但不会执行,直到输入Exec后,Redis才会将之前的命令队列中的命令一次执行。

组队过程中可通过discard放弃组队。

multi开始组队,此时客户端带TX,表示开始事务(transaction)

每插入一条命令,返回一个QUEUED表示进入任务队列

exec开始执行,每一条都返回OK表示每一条执行成功

discard放弃组队,这些命令均不执行

8.3、事务的错误处理

组队中某个命令出现了报告错误,执行时整个队列将会被取消

如果在执行阶段,则只有报错的命令不会被执行,其他的命令都会执行,不会互相影响,也不具备原子性

类似编译时异常和运行时异常

9、锁机制——事务冲突问题

多个请求同时发起增删改查操作,线程不安全,互相产生影响

9.1、悲观锁

悲观地认为所有线程都不安全,执行操作都需要加锁,上锁后才能访问线程共享变量

每次拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会被阻拦,直到别人拿到锁。

由于需要频繁的加锁开锁,影响并发性能

上厕所都要锁门,害怕别人来开门,不能给人进来

9.2、乐观锁

增加一个变量:版本号,执行一次操作后会更新一次版本号,别的线程会检查版本号是否一致,不一致则终止操作

每次拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。

乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。

9.3、watch key1 key2 ……

可以监视一个(或多个)key

开启多个服务端,在执行multi之前,先同时执行watch key1,同时对1个key进行watch。如果在事务执行之前这个(或这些)key被其他命令所改动,那么事务将被打断。输入exec不会执行。

9.4、Redis事务三特性

  • 单独的隔离操作

    • 事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

  • 没有隔离级别的概念

    • 队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行。

  • 不保证原子性

    • 事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚。

10、秒杀案例

10.1、搭建结构

商品库存: -个数

keystring
sk:productId:count 剩余个数

秒杀成功者清单: +人数

keyset
sk:productId:user 成功者user-id1
sk:productId:user 成功者user-id2
sk:productId:user 成功者user-id3

一个用户秒杀1次。只能不能重复秒杀,用户类型选用集合Set,使用随机码生成userId。假设产品id为100001

代码:

复制代码
 1 package com.zhou.controller;
 2 
 3 import com.zhou.config.JedisPoolUtil;
 4 import org.springframework.beans.factory.annotation.Autowired;
 5 import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
 6 import org.springframework.data.redis.core.StringRedisTemplate;
 7 import org.springframework.web.bind.annotation.GetMapping;
 8 import org.springframework.web.bind.annotation.RequestMapping;
 9 import org.springframework.web.bind.annotation.RestController;
10 import redis.clients.jedis.Jedis;
11 import redis.clients.jedis.JedisPool;
12 
13 import java.io.IOException;
14 import java.util.Random;
15 
16 /**
17  * @Name RedisTestController
18  * @Description
19  * @Author 88534
20  * @Date 2021/12/18 22:00
21  */
22 @RestController
23 public class RedisTestController {
24 
25     @Autowired
26     private StringRedisTemplate redisTemplate;
27 
28     @RequestMapping("/sk")
29     public String SecKill() {
30         doSecKill(getCode(),"100001");
31         return "OK";
32     }
33 
34     /**
35      * 随机生成userId
36      * @return
37      */
38     public static String getCode(){
39         Random random = new Random();
40         StringBuilder code = new StringBuilder();
41         for (int i = 0; i < 4; i++) {
42             int rand = random.nextInt(10);
43             code.append(rand);
44         }
45         return code.toString();
46     }
47 
48     public static boolean doSecKill(String userId, String productId) {
49         // 1.userId和prodId非空判断
50         if (userId == null || productId == null) {
51             return false;
52         }
53 
54         // 2.连接redis
55         Jedis jedis = new Jedis("192.168.56.101", 6379);
56         //JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
57         //Jedis jedis = jedisPoolInstance.getResource();
58 
59         // 3.拼接key
60         // 3.1 库存key
61         String stockKey = "sk:" + productId + ":count";
62         // 3.2 秒杀成功用户key
63         String userKey = "sk:" + productId + ":user";
64 
65         // 4.获取库存,如果库存为null,秒杀还没有开始
66         String stock = jedis.get(stockKey);
67         if (stock == null) {
68             System.out.println("秒杀还没有开始,请等待!");
69             jedis.close();
70             return false;
71         }
72 
73         // 5.判断用户是否重复秒杀操作
74         if (jedis.sismember(userKey, userId)) {
75             System.out.println("已经秒杀成功了,不能重复秒杀!");
76             jedis.close();
77             return false;
78         }
79 
80         // 6.判断如果商品数量,库存数量小于等于0,秒杀结束
81         if (Integer.parseInt(stock) <= 0) {
82             System.out.println("秒杀已经结束了!");
83             jedis.close();
84             return false;
85         }
86 
87         // 7.秒杀过程
88         // 7.1 库存-1
89         jedis.decr(stockKey);
90         // 7.2 把秒杀成功用户添加到成功用户清单
91         jedis.sadd(userKey,userId);
92         System.out.println("秒杀成功了!");
93         jedis.close();
94         return true;
95     }
96 }
复制代码

运行SpringBoot项目

在虚拟机打开一个远程连接,在root目录下创建postfile存储productId

[root@k8s-node01 ~]# vim postfile

模拟表单提交参数,以&符号结尾,文件内写入

productId=100001&

:wq保存返回

登录Redis,设置库存数为10:

set sk:100001:count 10

10.2、模拟高并发工具

使用工具ab模拟测试,CentOS7手动安装,重新开启一个远程连接

联网:yum install httpd-tools

安装完成后输入:

ab -n 1000 -c 100 -p ~/postfile -T application/x-www-form-urlencoded http://192.168.56.1:8080/sk

表示:向 http://192.168.56.1:8080/sk 发送一共1000个POST请求,携带内容为postfile,其中有100个并发请求。

  • -n表示请求总数

  • -c表示并发总数

  • -p表示post请求携带参数,需要用文件携带

  • -T表示请求类型,application/x-www-form-urlencoded表示POST表单的上传编码格式,模拟表单提交POST请求

  • 最后为需要发送的请求uri

得到如下结果:

[root@k8s-node01 ~]# ab -n 1000 -c 100 -p ~/postfile -T application/x-www-form-urlencoded http://192.168.56.1:8080/sk
This is ApacheBench, Version 2.3 <$Revision: 1430300 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 192.168.56.1 (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests


Server Software:
Server Hostname: 192.168.56.1
Server Port: 8080

Document Path: /sk
Document Length: 2 bytes

Concurrency Level: 100
Time taken for tests: 0.543 seconds
Complete requests: 1000
Failed requests: 0
Write errors: 0
Total transferred: 134000 bytes
Total body sent: 174000
HTML transferred: 2000 bytes
Requests per second: 1841.62 [#/sec] (mean)
Time per request: 54.300 [ms] (mean)
Time per request: 0.543 [ms] (mean, across all concurrent requests)
Transfer rate: 240.99 [Kbytes/sec] received
312.93 kb/s sent
553.92 kb/s total

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 8 9.3 5 39
Processing: 6 38 18.8 35 103
Waiting: 5 33 16.3 31 98
Total: 12 46 19.9 42 109

Percentage of the requests served within a certain time (ms)
50% 42
66% 51
75% 58
80% 62
90% 76
95% 82
98% 99
99% 106
100% 109 (longest request)

完成测试。

主机能ping通虚拟机,虚拟机ping不通主机问题:

确保:虚拟机服务已开启,网络适配器的连接方式已设为桥接模式,虚拟机ip和主机ip网段一致、“网关”“网卡”相同

解决办法:进入网络连接设置:Windows防火墙——>高级设置——>入站规则。启用“文件和打印机共享(回显请求 – ICMPv4-ln) 专用,公用”规则。

此时回到秒杀监视客户端,查看库存数量和用户信息。

127.0.0.1:6379> set sk:100001:count 10
OK
127.0.0.1:6379> get sk:100001:count
"-7"
127.0.0.1:6379> smembers sk:100001:user
1) "0325"
2) "6551"
3) "9645"
4) "3142"
5) "7949"
6) "9007"
7) "8117"
8) "0632"
9) "5538"
10) "7524"
11) "6217"
12) "1736"
13) "7003"
14) "3347"
15) "0735"
16) "3104"
17) "7974"

实际秒杀测试时会出现问题:连接redis超时(当设备性能较低时)、超卖现象(库存为负数)和库存遗留问题(库存为正数),并不是10个用户完成了秒杀

10.3、连接超时问题——连接池

连接池节省每次连接redis服务带来的消耗,把连接好的实例反复利用

通过参数管理连接的行为

连接池参数:

  • MaxTotal:控制一个pool可分配多少个jedis实例,通过pool.getResource()获取;如果赋值为-1,表示不限制;如果分配实例个数达到上限,pool状态为exhausted

  • maxIdle:控制一个pool最多有多少个状态为idle(空闲)的jedis实例

  • maxWaitMillis:当取用(borrow)一个jedis实例时,最大的等待毫秒数。如果超过等待时间,直接抛JedisConnectionException

  • testOnBorrow:获得一个jedis实例时是否检查连接可用性(ping());如果为true,则得到的jedis实例均是可用的。

示例:配置连接池

复制代码
 1 package com.zhou.jedis;
 2 
 3 import redis.clients.jedis.Jedis;
 4 import redis.clients.jedis.JedisPool;
 5 import redis.clients.jedis.JedisPoolConfig;
 6 
 7 /**
 8  * @Name JedisPoolUtil
 9  * @Description 配置连接池
10  * @Author 88534
11  * @Date 2021/12/20 0:03
12  */
13 public class JedisPoolUtil {
14     private static volatile JedisPool jedisPool = null;
15 
16     private JedisPoolUtil() {
17 
18     }
19 
20     public static JedisPool getJedisPoolInstance(){
21         if (null == jedisPool) {
22             synchronized (JedisPoolUtil.class) {
23                 JedisPoolConfig poolConfig = new JedisPoolConfig();
24                 poolConfig.setMaxTotal(200);
25                 poolConfig.setMaxIdle(32);
26                 poolConfig.setMaxWaitMillis(100*1000);
27                 poolConfig.setBlockWhenExhausted(true);
28                 // PING PONG
29                 poolConfig.setTestOnBorrow(true);
30 
31                 jedisPool = new JedisPool(poolConfig,"192.168.56.101",6379,60000);
32             }
33         }
34         return jedisPool;
35     }
36 
37     public static void release(JedisPool jedisPool, Jedis jedis){
38         if (null != jedis){
39             jedisPool.close();
40         }
41     }
42 }
复制代码

在原秒杀代码中修改连接redis项

// 2.连接redis
//Jedis jedis = new Jedis("192.168.56.101", 6379);
JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
Jedis jedis = jedisPoolInstance.getResource();

10.4、超卖问题——乐观锁

没有增加事务,每个操作之间互相影响。

同时发起:检查是否有库存,如果有则-1的操作,重复冲突的话就会造成-1的操作叠加

可以利用乐观锁淘汰用户

设置版本号,只有一个版本完成操作,其他不同版本的不能修改

乐观锁如果对比version不一致,就不会执行操作,并发情况下会出现失败的情况

原秒杀代码修改:

复制代码
public static boolean doSecKill(String userId, String productId) {
    // 1.userId和prodId非空判断
    if (userId == null || productId == null) {
        return false;
    }

    // 2.连接redis
    //Jedis jedis = new Jedis("192.168.56.101", 6379);
    JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
    Jedis jedis = jedisPoolInstance.getResource();

    // 3.拼接key
    // 3.1 库存key
    String stockKey = "sk:" + productId + ":count";
    // 3.2 秒杀成功用户key
    String userKey = "sk:" + productId + ":user";

    // 监视库存
    jedis.watch(stockKey);

    // 4.获取库存,如果库存为null,秒杀还没有开始
    String stock = jedis.get(stockKey);
    if (stock == null) {
        System.out.println("秒杀还没有开始,请等待!");
        jedis.close();
        return false;
    }

    // 5.判断用户是否重复秒杀操作
    if (jedis.sismember(userKey, userId)) {
        System.out.println("已经秒杀成功了,不能重复秒杀!");
        jedis.close();
        return false;
    }

    // 6.判断如果商品数量,库存数量小于等于0,秒杀结束
    if (Integer.parseInt(stock) <= 0) {
        System.out.println("秒杀已经结束了!");
        jedis.close();
        return false;
    }

    // 7.秒杀过程
    // 使用事务
    Transaction multi = jedis.multi();

    // 组队操作
    multi.decr(stockKey);
    multi.sadd(userKey,userId);

    // 执行
    List<Object> results = multi.exec();

    if (results == null || results.size() == 0){
        System.out.println("秒杀失败!");
        jedis.close();
        return false;
    }

    // 7.1 库存-1
    //jedis.decr(stockKey);
    // 7.2 把秒杀成功用户添加到成功用户清单
    //jedis.sadd(userKey,userId);

    System.out.println("秒杀成功了!");
    jedis.close();
    return true;
}
复制代码

再次执行相同测试:

[root@k8s-node01 ~]# ab -n 1000 -c 100 -p ~/postfile -T application/x-www-form-urlencoded http://192.168.56.1:8080/sk
This is ApacheBench, Version 2.3 <$Revision: 1430300 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 192.168.56.1 (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests


Server Software:
Server Hostname: 192.168.56.1
Server Port: 8080

Document Path: /sk
Document Length: 2 bytes

Concurrency Level: 100
Time taken for tests: 0.665 seconds
Complete requests: 1000
Failed requests: 0
Write errors: 0
Total transferred: 134000 bytes
Total body sent: 175000
HTML transferred: 2000 bytes
Requests per second: 1504.24 [#/sec] (mean)
Time per request: 66.479 [ms] (mean)
Time per request: 0.665 [ms] (mean, across all concurrent requests)
Transfer rate: 196.84 [Kbytes/sec] received
257.07 kb/s sent
453.92 kb/s total

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 5 7.7 1 34
Processing: 15 51 18.4 46 125
Waiting: 14 49 17.2 45 104
Total: 26 55 18.6 49 128

Percentage of the requests served within a certain time (ms)
50% 49
66% 54
75% 60
80% 67
90% 85
95% 96
98% 110
99% 115
100% 128 (longest request)

查看库存数和用户数

127.0.0.1:6379> get sk:100001:count
"0"
127.0.0.1:6379> smembers sk:100001:user
1) "1321"
2) "8944"
3) "0786"
4) "0315"
5) "2964"
6) "4037"
7) "1442"
8) "2862"
9) "2450"
10) "7749"

此时库存能够解决超卖问题。

10.5、库存遗留问题——Lua脚本

当库存数量较大、抢购人数较多时,如模拟500个库存,处理2000个请求,300次并发

查看库存结果和用户名单

127.0.0.1:6379> set sk:100001:count 500
OK
127.0.0.1:6379> get sk:100001:count
"461"
127.0.0.1:6379> smembers sk:100001:user
1) "6875"
2) "7386"
3) "0402"
4) "1792"
5) "6069"
6) "9460"
7) "8146"
8) "2459"
9) "1365"
10) "8561"
11) "0731"
12) "5127"
13) "3144"
14) "1136"
15) "3513"
16) "6795"
17) "0869"
18) "8773"
19) "8399"
20) "0754"
21) "7157"
22) "5257"
23) "0526"
24) "6040"
25) "5121"
26) "2328"
27) "1905"
28) "8998"
29) "3169"
30) "8293"
31) "2830"
32) "5346"
33) "8871"
34) "6416"
35) "9546"
36) "6918"
37) "2135"
38) "2841"
39) "2229"

此时仍有大量剩余。

问题:乐观锁造成库存遗留问题。

版本更新过快,导致后边的请求全部不能继续抢。反馈在后台输出,即为一个抢成功后,后边有大量请求失败,导致无法抢购。

Lua是一个小巧的脚本语言,可以很容易被C/C++代码调用,反过来也可以调用C/C++的函数,是一种嵌入式脚本语言,不适合开发独立应用程序的语言。使用Lua实现可配置性、可扩展性。

在Redis中的优势

将复杂或多步的Redis操作,写为1个脚本,一次提交给Redis执行,减少反复连接redis的次数,提升性能。

Lua脚本是类似Redis事务,有一定的原子性,不会被其他命令插队,可以完成一些事务性的操作,弥补Redis原子性不足的缺点。

Lua脚本适用于Redis 2.6 以上的版本,通过Lua脚本解决争抢问题,实际是Redis利用其单线程的特性,用任务队列的方式解决多任务并发问题。

使用Lua脚本解决库存依赖问题

复制代码
 1 local userId = KEYS[1];
 2 local productId = KEYS[2];
 3 local stockKey = "sk:"..productId..":count";
 4 local userKey = "sk:"..productId..":user";
 5 local userExists = redis.call("sismember",userKey,userId);
 6 if tonumber(userExists) == 1 then
 7     return 2;    --已经秒杀过了
 8 end
 9 local num = redis.call("get",stockKey);
10 if tonumber(num) <= 0 then
11     return 0;        --不能再秒杀了
12 else
13     redis.call("decr",stockKey);
14     redis.call("sadd",userKey,userId);
15 end
16 return 1;
复制代码

秒杀代码修改:

复制代码
 1 public static boolean doSecKill(String userId, String productId) {
 2     // 1.userId和prodId非空判断
 3     if (userId == null || productId == null) {
 4         return false;
 5     }
 6 
 7     // 2.连接redis
 8     //Jedis jedis = new Jedis("192.168.56.101", 6379);
 9     JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
10     Jedis jedis = jedisPoolInstance.getResource();
11 
12     // 3.Lua脚本
13     String s = jedis.scriptLoad(secKillScript);
14     Object result = jedis.evalsha(s, 2, userId, productId);
15 
16     String s1 = String.valueOf(result);
17     // 非空判断在下方
18     if ("0".equals(s1)){
19         System.out.println("已抢空!");
20     } else if ("1".equals(s1)){
21         System.out.println("抢购成功!");
22     } else if ("2".equals(s1)){
23         System.out.println("该用户已抢过!");
24     } else {
25         System.out.println("抢购异常!");
26     }
27 
28     jedis.close();
29     return true;
30 }
31 
32 static String secKillScript="local userId = KEYS[1];\n" +
33             "local productId = KEYS[2];\n" +
34             "local stockKey = \"sk:\"..productId..\":count\";\n" +
35             "local userKey = \"sk:\"..productId..\":user\";\n" +
36             "local userExists = redis.call(\"sismember\",userKey,userId);\n" +
37             "if tonumber(userExists) == 1 then\n" +
38             "    return 2;\t--已经秒杀过了\n" +
39             "end\n" +
40             "local num = redis.call(\"get\",stockKey);\n" +
41             "if tonumber(num) <= 0 then\n" +
42             "    return 0;\t\t--不能再秒杀了\n" +
43             "else\n" +
44             "    redis.call(\"decr\",stockKey);\n" +
45             "    redis.call(\"sadd\",userKey,userId);\n" +
46             "end\n" +
47             "return 1;";
复制代码

此时重复测试,库存数能降为0,且查看用户有相应抢购数的用户。

11、Redis持久化

Redis提供了2个不同形式的持久化方式

  • RDB(Redis Database)

  • AOF(Append Of File)

11.1、RDB

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

11.1.1、备份执行

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

11.1.2、Fork

Fork的作用是复制一个与当前进程一样的进程,新进程的所有数据(变量、环境变量、程序计数器等)数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程。

在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会被exec系统调用,处于效率考虑,引入了“写时复制”技术。

一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段内容要求发生变化时,才会将父进程的内容复制一份给子进程.

11.1.3、配置文件

在redis.conf中

save:save只管保存

redis.conf中相应配置内容

################################ SNAPSHOTTING  ################################

# Save the DB to disk.将数据库保存到磁盘。
#
# save <seconds> <changes>
#
# Redis will save the DB if both the given number of seconds and the given
# number of write operations against the DB occurred.
# 如果给定的秒数和对数据库执行的写入操作数同时发生,Redis将保存数据库。
#
# Snapshotting can be completely disabled with a single empty string argument
# as in following example:使用单个空字符串参数可以完全禁用快照
#
# Unless specified otherwise, by default Redis will save the DB:
# * After 3600 seconds (an hour) if at least 1 key changed
# * After 300 seconds (5 minutes) if at least 100 keys changed
# * After 60 seconds if at least 10000 keys changed
# 除非另有规定,默认情况下,Redis将保存数据库:
# 如果至少更换了一个key,3600秒(一小时)后保存
# 如果至少更换了100个key,300秒(5分钟)后保存
# 如果至少更改了10000个key,60秒后保存
#
# You can set these explicitly by uncommenting the three following lines.
# 可以通过取消注释以下三行来显式设置它们。
#
# save 3600 1
# save 300 100
# save 60 10000

# save 后如果给空值,表示禁用保存策略,停止RDB

# 当Redis无法写入磁盘时,直接关掉Redis的写操作,推荐yes
stop-writes-on-bgsave-error yes

# 对于存储到磁盘中的快照,是否需要进行压缩存储,如果yes,则采用LZF算法压缩。如果不希望消耗CPU进行压缩可以关闭。推荐yes
rdbcompression yes

# 存储快照后,使用CRC64算法进行数据校验检查数据完整性,但是会增加大约10%的性能消耗。希望获得最大的性能可以关闭。推荐yes
rdbchecksum yes

# 临时文件名
dbfilename dump.rdb


rdb-del-sync-files no

# 设置临时文件.rdb的存储路径,默认父目录为执行目录bin下
dir ./

11.1.4、优缺点

优势:

  • 适合大规模的数据恢复

  • 对数据完整性和一致性要求不高更适合使用

  • 节省磁盘空间

  • 恢复速度快

劣势:

  • Fork的时候,内存中的数据被克隆了一份,需要考虑2倍的空间

  • 虽然Redis在fork时使用了写时拷贝技术,但当数据庞大时消耗的性能较大

  • 在备份周期的一定间隔时间做一次备份,如果Redis意外down,就会丢失最后一次快照后的所有修改

11.1.5、rdb文件备份

通过配置文件redis.conf的dir查询rdb文件的目录

将*.rdb的文件拷贝到其他地方

rdb的恢复:

  • 关闭redis,杀掉进程

  • 把备份的文件拷贝到工作目录下(一般为bin)cp *.rdb dump.rdb

  • 启动Redis,备份数据会直接加载

11.2、AOF

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

11.2.1、持久化流程

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

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

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

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

11.2.2、开启

AOF默认不开启,可以在redis.conf配置文件中搜索appendonly,改为yes开启

重新开启Redis导入配置后,会在原目录bin下生成appendonly.aof

如果AOF和RDB同时开启,系统默认取AOF的数据(数据不会存在丢失)

redis.conf中相应配置内容

############################## APPEND ONLY MODE ###############################
# 是否开启
appendonly no

# AOF文件名 (默认: "appendonly.aof")
appendfilename "appendonly.aof"

# AOF同步策略设置
# appendfsync always 始终同步,每次Redis的写入就会立即记入日志,性能较差但数据完整性较好
appendfsync everysec # 每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失
# appendfsync no # 不主动进行同步,把同步时机交给操作系统,由操作系统自动调度刷磁盘,性能是最好的

# yes:不写入AOF文件只写入缓存,用户请求不会阻塞,但是在这段时间如果宕机就会丢失这段时间的缓存数据
# 降低数据安全性,提高性能
# no:把数据往磁盘里刷,但是遇到重写操作,可能会发生阻塞
# 数据安全,性能降低
no-appendfsync-on-rewrite no

# 触发机制,何时重写?
# Redis会记录上次重写时AOF大小,默认配置是当AOF文件大小比上次rewrite后大1倍且文件大于64M时触发,可以设置
auto-aof-rewrite-percentage 100 #文件达到100%开始重写,即文件是原来重写后文件的2倍时触发
auto-aof-rewrite-min-size 64mb

# 指redis在恢复时,会忽略最后一条可能存在问题的指令。默认值yes。即在aof写入时,可能存在指令写错的问题(突然断电,写了一半),这种情况下,yes会跳过并继续,而no会直接恢复失败.
aof-load-truncated yes

# 开启混合持久化,更快的AOF重写和启动时数据恢复。当开启该选项时,触发AOF重写将不再是根据当前内容生成写命令。而是先生成RDB文件写到开头,再将RDB生成期间的发生的增量写命令附加到文件末尾。
aof-use-rdb-preamble yes

11.2.3、恢复、修复

修改默认的appendonly no改为yes

正常恢复:

操作与RDB一样,都是拷贝备份文件,需要恢复时再拷贝到Redis工作目录下,启动系统即加载

异常恢复:

如遇到AOF文件损坏,通过/usr/local/bin/redis-check-aof --fix appendonly.aof进行恢复

备份被写坏的AOF文件

恢复,重启Redis加载

11.2.4、rewrite重写压缩

AOF采用文件追加方式,文件会越来越大,为避免出现溢出情况,新增了重写机制,当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集,可以使用命令bgwriteaof

重写原理:AOF文件持续增长而过大时,会fork出一条新进程来将文件重写(先写临时文件最后rename),在Redis4.0版本之后,加入了aof-use-rdb-preamble配置,重写就是把RDB的快照,以二进制的形式附在新的AOF头部,作为已有的历史数据,替换掉原来的流水账操作。重写完成后,继续像普通AOF一样追加内容。

文件格式: [RBD文件内容][追加的AOF日志]

重写虽然可以节约大量磁盘空间,减少恢复时间,但是每次重写仍然有一定负担,因此Redis需要满足一定条件才会重写

11.2.5、重写流程

  1. bgrewriteaof触发重写,判断是否当前有bgsave或bgrewriteaof在运行,如果有,则等待该命令结束后再继续执行。

  2. 主进程fork出子进程执行重写操作,保证主进程不会阻塞

  3. 子进程遍历Redis内存中数据到临时文件,客户端的写请求同时进入aof_buf缓冲区和aof_rewrite_buf重写缓冲区保证原AOF文件完整以及新AOF文件生成期间新的数据修改动作不会丢失

  4. 子进程写完新的AOF文件后,向主进程发信号,父进程更新统计信息

  5. 主进程把aof_rewrite_buf中的数据写入到新的AOF中

  6. 使用新的AOF文件覆盖旧的AOF文件,完成AOF重写。

重启加载AOF时

启动时加载AOF如果碰到RDB的开头前缀 REDIS,先按RDB恢复数据。再将附加在末尾的写命令重放,恢复完整数据

11.2.6、优缺点

客户端发送命令请求给服务器,服务器发送带网络协议格式的命令内容到AOF文件中

优点:

  • 备份机制更稳健,丢失数据概率更低

  • 可读的日志文本,通过操作AOF文件,可以处理误操作

缺点:

  • 比起RDB占用更多的磁盘空间

  • 恢复备份速度要慢

  • 每次读写都同步的话,有一定的性能压力

11.3、总结

11.3.1、如何选择

官方推荐两个方式都启用。

如果对数据不敏感,可单独用RDB

不建议单独用AOF,可能会出现Bug

如果只是做纯内存缓存,可以都不用

11.3.2、官方建议

  • RDB持久化方式能够在指定的时间间隔对数据进行快照存储

  • AOF持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以Redis协议追加保存每次写的操作到文件末尾

  • Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大

  • 只做缓存:如果只希望数据在服务器运行的时候存在,可以不使用任何持久化方式

  • 建议同时开启两种持久化方式

  • 在这种情况下,当Redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整

  • RDB的数据不实时,同时使用两者时服务器重启只会找AOF文件。

  • 建议不要只使用AOF,因为RDB更适合用于备份数据库(AOF在不断变化不好备份),快速重启,而且不会有AOF可能潜在的bug,留作以防万一的手段

  • 性能建议:因为RDB文件只作后备用途,建议只在从服务器(Slave)上持久化RDB文件,而且只需要15分钟备份一次就够了,只保留save 900 1这条规则

12、主从复制

主机数据更新后根据配置和策略,自动同步到备机的master/slaver机制,Master以写为主,Slave以读为主,对应一主多从

  • 读写分离,性能扩展,缓解服务器压力

  • 容灾快速恢复,从服务器宕机时,有别的从服务器承担读操作(读操作占绝大多数)

12.1、实际操作

1、在/~目录中创建/myredis文件夹

2、复制redis.conf配置文件到myredis文件夹中,修改appendonly为no

3、配置一主两从,vi创建3个配置文件

引入配置文件、进程、端口号、持久化

redis6379.conf

include /myredis/redis.conf
pidfile /var/run/redis_6379.pid
port 6379
dbfilename dump6379.rdb

redis6380.conf和redis6381.conf分别对应修改相应6379的部分到自己的端口号

4、开启3个服务器,分别绑定3个不同的配置

redis-server /myredis/redis6379.conf

redis-server /myredis/redis6380.conf

redis-server /myredis/redis6381.conf

开启3个远程连接,分别登录redis redis-cli -p 6379/6380/6381

查看进程:

[root@k8s-node01 myredis]# ps -aux | grep redis
root 339 0.0 0.0 112808 988 pts/0 R+ 22:46 0:00 grep --color=auto redis
root 10091 0.1 0.4 187088 9348 ? Ssl 14:31 0:31 redis-server *:6379
root 32301 0.0 0.5 162512 9944 ? Ssl 22:46 0:00 redis-server *:6380
root 32429 0.0 0.5 162512 9948 ? Ssl 22:46 0:00 redis-server *:6381

5、配从

slaveof ip port 成为某个实例的从服务器

设置有密码的需要加上 masterauth 密码

在6380和6381上执行:slaveof 127.0.0.1 6379

查看主从设置:

info replication

6379:

[root@k8s-node01 myredis]# redis-cli -p 6379
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=6380,state=online,offset=70,lag=0
slave1:ip=127.0.0.1,port=6381,state=online,offset=70,lag=0
master_failover_state:no-failover
master_replid:71bff6c4ae7ef839e52e2bafb86cfd160d5d1e8d
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:70
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:70

6380:

[root@k8s-node01 run]# redis-cli -p 6380
127.0.0.1:6380> slaveof 127.0.0.1 6379
OK
127.0.0.1:6380> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
master_link_status:up
master_last_io_seconds_ago:3
master_sync_in_progress:0
slave_read_repl_offset:56
slave_repl_offset:56
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:71bff6c4ae7ef839e52e2bafb86cfd160d5d1e8d
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:56
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:56

6、读写测试,主写从读

主能写,从能及时更新读

但从写将会报错:

127.0.0.1:6380> set a2 v2
(error) READONLY You can't write against a read only replica.

12.2、常用模式

12.2.1、一主二仆

大哥挂了小弟还认大哥,小弟挂了回头不认大哥自己当大哥学大哥

1、从服务器宕机时

主从配置不是在配置文件中写的,而是在客户端用命令写的,如果从服务器关闭(shutdown),主从关系将会失效,自认为是master。

但重建从服务器时,将会把原主机127.0.0.1上的所有数据无序复制过来,可查看之前主机添加的数据。

在配置文件中写主从关系将会永久有效。

  • 在slave的配置文件中增加类似下面这行的内容:

slaveof 127.0.0.1 6379

如果master需要通过密码登陆,就需要配置slave在进行所有同步操作也要使用到密码。 在一个运行的实例上尝试,使用 redis-cli :

config set masterauth <password>

也可以设置永久的。在配置文件中增加:

masterauth <password>

2、主服务器宕机时

从服务器仍然保持从属关系,只是主机状态变为down,主服务器重启后依然保持master的身份。

主从复制原理:

  1. 当从服务器连接上主服务器之后,从服务器向主服务器发送同步sync消息请求数据同步

  2. 主服务器街道从服务器发来的同步消息,把主服务器数据进行持久化,生成的rdb文件发送给从服务器,从服务器拿到rdb文件后进行读取。(全量复制,重新连接master后一次完全同步,自动执行)

  3. 每次主服务器进行写操作之后,和从服务器进行数据同步。(增量复制)

12.2.2、薪火相传

上一个slave可以是下一个slave的master,slave同样可以接收其他slaves的连接和同步请求,那么该slave作为链条的下一个master,可以有效减轻master的写压力(master不需要分发给大量的slave),去中心化降低风险。

缺点:一旦某个中间的slave宕机,后边的slave都无法备份;如果主机挂了,从机还是从机,无法写数据。

12.2.3、反客为主

当一个master宕机后,后边的slave可以立刻升为master,后边的slave不用做任何修改。

上位命令:

slaveof no one 将从机变为主机

缺点:需要手动修改,无法自动实现,需要通过哨兵模式(sentinel)自动跳转

12.3、哨兵模式(sentinel)

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

单独一个服务器作为哨兵监视主服务器,一旦主服务器宕机迅速找到下一个备选主服务器切换

12.3.1、使用

自定义的/myredis目录下新建sentinel.conf文件,确保文件名不出错

填写内容

sentinel monitor mymaster 127.0.0.1 6379 1

其中mymaster为监控对象起的服务器名称,1为至少有多少个哨兵同意迁移的数量

在/myredis中启动哨兵,端口号为26379

redis-sentinel sentinel.conf

主机挂了之后,哨兵会选举剩下的从机中的一台作为主机,sentinel向其他从机发送slaveof新主服务命令,其他从机作为新主的从机

选择条件依次为(从上到下):

  • 选择优先级靠前的:redis.conf中设置:slave-priority 100,值越小优先级越高

  • 选择偏移量最大的:指获得原主机数据最全的

  • 选择runid最小的从服务:每个redis实例启动后都会随机生成一个40位的runid

主机恢复之后,sentinel向其发送slaveof新主服务命令,复制新的master的信息,自动变为从机slave跟在选举后边(重生之小弟变为上司)

12.3.2、缺点:复制延时

由于所有的写操作都是先在Master上操作,然后同步更新到slave上,所以从Master同步到Slave机器有一定的延迟,当系统很繁忙的时候,延迟问题会更加严重,slave机器数量的增加也会使得这个问题更加严重。

12.3.3、Java代码实现

修改连接池属性JedisSentinelPool,添加一个哨兵26379,配置JedisPool与7.3整合SpringBoot配置文件类似

复制代码
 1 private static JedisSentinelPool jedisSentinelPool = null;
 2 
 3 public static Jedis getJedisFromSentinel(){
 4     if (jedisSentinelPool == null) {
 5         Set<String> sentinelSet = new HashSet<>();
 6         sentinelSet.add("192.168.56.101:26379");
 7         JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
 8         // 最大可连接数
 9         jedisPoolConfig.setMaxTotal(10);
10         // 最大闲置连接数
11         jedisPoolConfig.setMaxIdle(5);
12         // 最小闲置连接数
13         jedisPoolConfig.setMinIdle(5);      
14         // 连接耗尽是否等待
15         jedisPoolConfig.setBlockWhenExhausted(true);
16         // 等待时间
17         jedisPoolConfig.setMaxWaitMillis(2000);
18         // 取连接的时候进行一下测试ping PONG
19         jedisPoolConfig.setTestOnBorrow(true);
20         jedisSentinelPool = new JedisSentinelPool("mymaster",sentinelSet,jedisPoolConfig);
21     }
22     return jedisSentinelPool.getResource();
23 }
复制代码

13、集群

13.1、问题

  • 容量不够,Redis如何进行扩容?

  • 并发写操作,Redis如何分摊?

  • 主从模式、薪火相传模式、主机宕机,导致ip地址发生变化,应用程序中的配置需要修改对应的主机地址、端口等信息,如何同步?

之前是通过代理主机解决,但Redis3.0之后,提供了新的解决方案,就是无中心化集群配置

13.2、什么是集群

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

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

需要删除持久化数据,将rdb、aof文件统统删除掉。

13.3、制作实例

6个实例:6379、6380、6381、6389、6390、6391

13.3.1、Redis Cluster配置修改

cluster-enabled yes 打开集群模式

cluster-config-file nodes-6379.conf 设置节点配置文件名

cluster-node-timeout 15000 设置节点失联时间,超过该时间(单位:毫秒),集群自动进行主从切换。

添加放入redis6379.conf配置中

复制五份配置文件,端口对应实例

修改实例配置时技巧:vi进入后直接输入:%s/6379/{port}即可将6379换成对应的端口,批量替换操作

启动6个Redis服务,对应配置文件

[root@k8s-node01 ~]# cd /myredis/
[root@k8s-node01 myredis]# ls
dump6380.rdb dump6381.rdb redis6379.conf redis6380.conf redis6381.conf redis6389.conf redis6390.conf redis6391.conf redis.conf
[root@k8s-node01 myredis]# redis-server redis6379.conf
[root@k8s-node01 myredis]# redis-server redis6380.conf
[root@k8s-node01 myredis]# redis-server redis6381.conf
[root@k8s-node01 myredis]# redis-server redis6389.conf
[root@k8s-node01 myredis]# redis-server redis6390.conf
[root@k8s-node01 myredis]# redis-server redis6391.conf
[root@k8s-node01 myredis]# ps -aux | grep redis
root 2270 0.0 0.5 165072 10472 ? Ssl 22:54 0:00 redis-server *:6379 [cluster]
root 2380 0.0 0.5 165072 10428 ? Ssl 22:54 0:00 redis-server *:6380 [cluster]
root 2467 0.0 0.5 165072 10428 ? Ssl 22:54 0:00 redis-server *:6381 [cluster]
root 2608 0.0 0.5 165072 10492 ? Ssl 22:54 0:00 redis-server *:6389 [cluster]
root 2673 0.0 0.5 165072 10488 ? Ssl 22:54 0:00 redis-server *:6390 [cluster]
root 2745 0.0 0.5 165072 10492 ? Ssl 22:54 0:00 redis-server *:6391 [cluster]
root 2964 0.0 0.0 112808 988 pts/0 R+ 22:54 0:00 grep --color=auto redis
[root@k8s-node01 myredis]# ll
total 148
-rw-r--r--. 1 root root 2147 12月 21 01:22 dump6380.rdb
-rw-r--r--. 1 root root 2147 12月 21 01:22 dump6381.rdb
-rw-r--r--. 1 root root 114 12月 21 22:54 nodes-6379.conf
-rw-r--r--. 1 root root 141 12月 21 22:54 nodes-6380.conf
-rw-r--r--. 1 root root 141 12月 21 22:54 nodes-6381.conf
-rw-r--r--. 1 root root 114 12月 21 22:54 nodes-6389.conf
-rw-r--r--. 1 root root 114 12月 21 22:54 nodes-6390.conf
-rw-r--r--. 1 root root 114 12月 21 22:54 nodes-6391.conf
-rw-r--r--. 1 root root 177 12月 21 01:15 redis6379.conf
-rw-r--r--. 1 root root 177 12月 21 01:19 redis6380.conf
-rw-r--r--. 1 root root 177 12月 21 01:20 redis6381.conf
-rw-r--r--. 1 root root 177 12月 21 01:20 redis6389.conf
-rw-r--r--. 1 root root 177 12月 21 01:21 redis6390.conf
-rw-r--r--. 1 root root 177 12月 21 01:21 redis6391.conf
-rw-r--r--. 1 root root 93726 12月 21 01:12 redis.conf

生成6个节点配置文件

13.3.2、将6个节点合成一个集群

组合之前,确保所有Redis实例启动后,node-xxxx.conf生成正常

切换目录:cd /opt/redis-6.2.6/src/

redis-cli --cluster create --cluster-replicas 1 192.168.56.101:6379 192.168.56.101:6380 192.168.56.101:6381 192.168.56.101:6389 192.168.56.101:6390 192.168.56.101:6391

此处不要使用127.0.0.1,使用真实IP地址

如果设置有密码,需要在1后边加上 -a 和密码

一个集群至少要有三个主节点

--cluster-replicas 1:采用最简单的方式配置集群,一台主机,一台从机,正好三组分配方案,选择yes接受,自动进行分配;集群中的每个主节点创建一个从节点。分配原则尽量保证每个主数据库运行在不同的IP地址,每个从库和主库不在一个IP地址上。

一主一从作为主从备份手段,三个集群作为负载均衡,避免过多的键放在一个节点上

>>> Performing hash slots allocation on 6 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
Adding replica 192.168.56.101:6390 to 192.168.56.101:6379
Adding replica 192.168.56.101:6391 to 192.168.56.101:6380
Adding replica 192.168.56.101:6389 to 192.168.56.101:6381
>>> Trying to optimize slaves allocation for anti-affinity
[WARNING] Some slaves are in the same host as their master
M: 9d79a8f8230f4ab56c156aca855320c02b6aaa4d 192.168.56.101:6379
slots:[0-5460] (5461 slots) master
M: b7b19a258a06d36273a2dd37a33be5c6f114a301 192.168.56.101:6380
slots:[5461-10922] (5462 slots) master
M: d241d81e401b1d1e8f05b0ef037e949bcff79728 192.168.56.101:6381
slots:[10923-16383] (5461 slots) master
S: 6bc9fb2c1de375afe7e4aa996ca3964de52c4f36 192.168.56.101:6389
replicates 9d79a8f8230f4ab56c156aca855320c02b6aaa4d
S: 7efafaaa20bc9df37e60ade18dda462cf9d6571f 192.168.56.101:6390
replicates b7b19a258a06d36273a2dd37a33be5c6f114a301
S: e61dac14caef414265a0d4a07c68b728844fc267 192.168.56.101:6391
replicates d241d81e401b1d1e8f05b0ef037e949bcff79728
Can I set the above configuration? (type 'yes' to accept): yes
>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join
.
>>> Performing Cluster Check (using node 192.168.56.101:6379)
M: 9d79a8f8230f4ab56c156aca855320c02b6aaa4d 192.168.56.101:6379
slots:[0-5460] (5461 slots) master
1 additional replica(s)
M: d241d81e401b1d1e8f05b0ef037e949bcff79728 192.168.56.101:6381
slots:[10923-16383] (5461 slots) master
1 additional replica(s)
S: 6bc9fb2c1de375afe7e4aa996ca3964de52c4f36 192.168.56.101:6389
slots: (0 slots) slave
replicates 9d79a8f8230f4ab56c156aca855320c02b6aaa4d
S: e61dac14caef414265a0d4a07c68b728844fc267 192.168.56.101:6391
slots: (0 slots) slave
replicates d241d81e401b1d1e8f05b0ef037e949bcff79728
M: b7b19a258a06d36273a2dd37a33be5c6f114a301 192.168.56.101:6380
slots:[5461-10922] (5462 slots) master
1 additional replica(s)
S: 7efafaaa20bc9df37e60ade18dda462cf9d6571f 192.168.56.101:6390
slots: (0 slots) slave
replicates b7b19a258a06d36273a2dd37a33be5c6f114a301
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

13.3.3、采用集群策略连接 -c

设置数据会自动切换到相应写主机,可连接任意一个节点

redis-cli -c -p 6379

通过cluster nodes查看节点信息

[root@k8s-node01 src]# redis-cli -c -p 6379
127.0.0.1:6379> cluster nodes
d241d81e401b1d1e8f05b0ef037e949bcff79728 192.168.56.101:6381@16381 master - 0 1641273170584 3 connected 10923-16383
6bc9fb2c1de375afe7e4aa996ca3964de52c4f36 192.168.56.101:6389@16389 slave 9d79a8f8230f4ab56c156aca855320c02b6aaa4d 0 1641273170000 1 connected
9d79a8f8230f4ab56c156aca855320c02b6aaa4d 192.168.56.101:6379@16379 myself,master - 0 1641273169000 1 connected 0-5460
e61dac14caef414265a0d4a07c68b728844fc267 192.168.56.101:6391@16391 slave d241d81e401b1d1e8f05b0ef037e949bcff79728 0 1641273171000 3 connected
b7b19a258a06d36273a2dd37a33be5c6f114a301 192.168.56.101:6380@16380 master - 0 1641273171604 2 connected 5461-10922
7efafaaa20bc9df37e60ade18dda462cf9d6571f 192.168.56.101:6390@16390 slave b7b19a258a06d36273a2dd37a33be5c6f114a301 0 1641273170000 2 connected

13.3.4、slots

一个Redis集群中包含16384个插槽(hash slot),数据库中的每个键都属于这16384个插槽的其中一个,集群使用公式CRC16(key)%16384来计算键key属于哪个槽,其中CRC16(key)用于计算键key的CRC16校验和。

集群中的每个节点负责处理一部分插槽,实现了负载均衡。

节点A负责0~5460

节点B负责5461~10922

节点C负责10923~16383

13.4、集群操作

13.4.1、插入set

不同的key会分配到对应的插槽对应的节点内,自动实现客户端跳转。别的客户端无法查到该key。

不在一个slot下的键值,不能使用mget、mset等多键操作,可以通过{}定义组的概念,从而使多个key中{组}内相同内容的键值对放在一个slot中。

127.0.0.1:6379> set k1 v1
-> Redirected to slot [12706] located at 192.168.56.101:6381
OK
192.168.56.101:6381> keys *
1) "k1"
192.168.56.101:6381> set k2 v2
-> Redirected to slot [449] located at 192.168.56.101:6379
OK
192.168.56.101:6379> keys *
1) "k2"

192.168.56.101:6379> mset name zhou age 20 address China
(error) CROSSSLOT Keys in request don't hash to the same slot
192.168.56.101:6379> mset name {zhou} age {20} address {China}
(error) CROSSSLOT Keys in request don't hash to the same slot
192.168.56.101:6379> mset name{zhou} zhou age{zhou} 20 address{zhou} China
-> Redirected to slot [11570] located at 192.168.56.101:6381
OK
192.168.56.101:6381> keys *
1) "age{zhou}"
2) "address{zhou}"
3) "name{zhou}"
4) "k1"

13.4.2、查询集群中的值

cluster getkeysinslot <slot> <count>返回count个slot槽中的键

cluster countkeysinslot <slot> 查找slot中有几个key,不在该slot返回0,需要跳转到对应cli

cluster keyslot <key> 查找key所在slot

192.168.56.101:6381> cluster keyslot k1
(integer) 12706
192.168.56.101:6381> cluster countkeysinslot 12706
(integer) 1
192.168.56.101:6381> cluster keyslot k2
(integer) 449
192.168.56.101:6381> cluster countkeysinslot 449
(integer) 0
192.168.56.101:6381> cluster getkeysinslot 12706 10
1) "k1"

13.5、故障处理

查看集群状态,此时6379为主master,6389为对应从

[root@k8s-node01 myredis]# redis-cli -c -p 6379
127.0.0.1:6379> cluster nodes
9d79a8f8230f4ab56c156aca855320c02b6aaa4d 192.168.56.101:6379@16379 myself,master - 0 1641285715000 1 connected 0-5460
e61dac14caef414265a0d4a07c68b728844fc267 192.168.56.101:6391@16391 slave d241d81e401b1d1e8f05b0ef037e949bcff79728 0 1641285718440 3 connected
7efafaaa20bc9df37e60ade18dda462cf9d6571f 192.168.56.101:6390@16390 slave b7b19a258a06d36273a2dd37a33be5c6f114a301 0 1641285717430 2 connected
d241d81e401b1d1e8f05b0ef037e949bcff79728 192.168.56.101:6381@16381 master - 0 1641285717000 3 connected 10923-16383
6bc9fb2c1de375afe7e4aa996ca3964de52c4f36 192.168.56.101:6389@16389 slave 9d79a8f8230f4ab56c156aca855320c02b6aaa4d 0 1641285718000 1 connected
b7b19a258a06d36273a2dd37a33be5c6f114a301 192.168.56.101:6380@16380 master - 0 1641285717000 2 connected 5461-10922
127.0.0.1:6379> shutdown
not connected> exit

关闭6379,模拟宕机

打开6380查看集群状态,此时6379变为fail,6389自动变为master,反客为主

[root@k8s-node01 myredis]# redis-cli -c -p 6380
127.0.0.1:6380> cluster nodes
e61dac14caef414265a0d4a07c68b728844fc267 192.168.56.101:6391@16391 slave d241d81e401b1d1e8f05b0ef037e949bcff79728 0 1641285784219 3 connected
b7b19a258a06d36273a2dd37a33be5c6f114a301 192.168.56.101:6380@16380 myself,master - 0 1641285783000 2 connected 5461-10922
9d79a8f8230f4ab56c156aca855320c02b6aaa4d 192.168.56.101:6379@16379 master,fail - 1641285762646 1641285758000 1 disconnected
d241d81e401b1d1e8f05b0ef037e949bcff79728 192.168.56.101:6381@16381 master - 0 1641285785239 3 connected 10923-16383
7efafaaa20bc9df37e60ade18dda462cf9d6571f 192.168.56.101:6390@16390 slave b7b19a258a06d36273a2dd37a33be5c6f114a301 0 1641285785000 2 connected
6bc9fb2c1de375afe7e4aa996ca3964de52c4f36 192.168.56.101:6389@16389 master - 0 1641285786253 7 connected 0-5460

再打开一个远程连接,登录6379,再回到6380查看集群状态

127.0.0.1:6380> cluster nodes
e61dac14caef414265a0d4a07c68b728844fc267 192.168.56.101:6391@16391 slave d241d81e401b1d1e8f05b0ef037e949bcff79728 0 1641285848441 3 connected
b7b19a258a06d36273a2dd37a33be5c6f114a301 192.168.56.101:6380@16380 myself,master - 0 1641285845000 2 connected 5461-10922
9d79a8f8230f4ab56c156aca855320c02b6aaa4d 192.168.56.101:6379@16379 slave 6bc9fb2c1de375afe7e4aa996ca3964de52c4f36 0 1641285847381 7 connected
d241d81e401b1d1e8f05b0ef037e949bcff79728 192.168.56.101:6381@16381 master - 0 1641285846000 3 connected 10923-16383
7efafaaa20bc9df37e60ade18dda462cf9d6571f 192.168.56.101:6390@16390 slave b7b19a258a06d36273a2dd37a33be5c6f114a301 0 1641285847000 2 connected
6bc9fb2c1de375afe7e4aa996ca3964de52c4f36 192.168.56.101:6389@16389 master - 0 1641285846000 7 connected 0-5460

此时6389仍然为主,6379自动作为slave从机

在redis.conf中

如果某一段插槽的主从都挂掉,而cluster-require-full-coverage为yes,那么整个集群都挂掉。

如果某一段插槽的主从都挂掉,而cluster-require-full-coverage为no,那么该插槽数据全部不能使用,也无法存储。

13.6、集群的Jedis开发

复制代码
 1 package com.zhou.jedis;
 2 
 3 import redis.clients.jedis.HostAndPort;
 4 import redis.clients.jedis.JedisCluster;
 5 
 6 /**
 7  * @Name RedisClusterDemo
 8  * @Description 集群搭建
 9  * @Author 88534
10  * @Date 2022/1/4 16:55
11  */
12 public class RedisClusterDemo {
13     public static void main(String[] args) {
14         // 创建连接
15         HostAndPort hostAndPort = new HostAndPort("192.168.56.101", 6379);
16         JedisCluster jedisCluster = new JedisCluster(hostAndPort);
17 
18         // 操作
19         jedisCluster.set("b1","v1");
20         String value = jedisCluster.get("b1");
21         System.out.println("value:" + value);
22 
23         // 关闭
24         jedisCluster.close();
25     }
26 }
复制代码

输出:

value:v1

13.7、集群的优缺点

好处:

  • 实现扩容

  • 分摊压力

  • 无中心配置相对简单

不足:

  • 不支持多键操作

  • 不支持Redis事务和Lua脚本

  • 由于集群方案出现较晚,许多公司已经采用了其他集群方案,如果迁移到redis cluster需要整体迁移而不是逐步过渡,复杂度较大

14、Redis应用问题解决

面试常问问题

14.1、缓存穿透

key对应的数据在数据源并不存在,每次针对此key的请求从缓存中获取不到,请求都会压到数据源,从而可能压垮数据源。

比如用一个不存在的用户id(如-1)获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。

查缓存中没有的数据,从而导致大量请求直接访问数据库导致压力过大

内存有限,根据Redis淘汰数据规则,当请求数据多了,一些不常用的数据将会淘汰。重新再查这些数据需要重新访问数据库,增大数据库的负担,失去了缓存的意义。

解决方案

1、对空值缓存(简单应急方案):如果一个查询返回的数据为空(不管是数据是否不存在),把空结果null进行缓存,设置空结果的过期时间会很短,最长不超过5分钟。

2、设置可访问的名单(白名单):使用Bitmaps类型定义一个可以访问的名单,名单id作为Bitmaps的偏移量,每次访问和Bitmaps里边的id进行比较,如果访问id不在Bitmaps里则进行拦截,不允许访问。

3、采用布隆过滤器(Bloom Filter):由一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数)组成,检索一个元素是否在一个集合中。将所有可能存在的数据哈希到一个足够大的Bitmaps中,一个一定不存在的数据会被这个Bitmaps拦截掉,从而避免了对底层存储系统的查询压力。优点是空间效率和查询时间,缺点是有一定的误识别率和删除困难。

4、进行实时监控:当发现Redis的命中率开始急速降低,需要排查访问对象和访问的数据,和运维人员配合,可以设置黑名单限制服务。

14.2、缓存击穿

key对应的数据存在,但在Redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般会从后端数据库加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端数据库压垮。

解决方案

key可能会在某些时间点呗超高并发地访问,是一种非常“热点”的数据。

1、预先设置热门数据:在Redis访问高峰之前,把一些热门数据提前存入到Redis中,加大这些热门数据key的过期时长。

2、实时调整:现场监控哪些数据热门,实时调整key的过期时长

3、使用锁

  • 在缓存失效的时候(此时判断拿出来的值为空),不是马上访问数据库;

  • 使用缓存工具中某些带成功操作返回值的操作,如SETNX一个lock key);如set key_lock 1 EX 180 NX

  • 当操作返回成功时,再进行访问数据库操作,并回设缓存,最后删除这个lock key;

  • 当操作返回失败,证明有线程正在访问数据库,当前线程睡眠一段时间再重试get整个缓存

缺点:加入了排它锁机制,效率降低

14.3、缓存雪崩

缓存雪崩与缓存击穿的区别在于雪崩针对一段较短时间内很多key过期,击穿是某个需要高并发访问的key过期。

解决方案

1、构建多级缓存架构:Nginx缓存+Redis缓存+其他缓存(如ehcache等)

2、使用锁或队列:用加锁或者队列的方式来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。但不适用于高并发情况。

3、设置过期标志更新缓存:记录缓存数据是否过期(设置提前量),如果过期会出发通知另外的线程在后台更新实际key的缓存

4、将缓存失效时间分散开:如可以在原有的失效时间基础上增加一个随机值,如1-5分钟随机,降低每一个缓存的过期时间重复率,很难引发集体失效事件。但需要考虑实际内存大小,失效太长容易溢出。

14.4、分布式锁

14.4.1、问题描述

随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。

为了解决这个问题需要一种跨JVM的互斥机制来控制共享资源的访问。

分布式锁主流的实现方案:

  • 基于数据库实现分布式锁

  • 基于缓存(Redis等),性能最高

  • 基于ZooKeeper,可靠性最高

14.4.2、解决方案

Redis命令:setnx 设置分布式锁 expire 设置过期时间 del 删除锁

  • 使用setnx上锁,通过del释放锁

  • 锁一直没有释放,设置key过期时间,自动释放

  • 上锁之后突然出现异常,无法设置过期时间了,需要同时设置过期时间:如set <key> 10 nx ex 20

    • ex设置键的过期时间

    • nx表示只在键不存在时,才对键进行设置操作

14.4.3、简单实现

仍使用之前SpringBoot项目中的RedisTestController,加入一个Web请求

复制代码
 1 /**
 2  * 测试num的递增,快速访问testLock
 3  */
 4 @GetMapping("testLock")
 5 public void testLock(){
 6     // 1.获取锁,setnx,设置过期时间,任意设置值
 7     Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111",3, TimeUnit.SECONDS);
 8 
 9     // 2.获取锁成功,查询num的值
10     if (lock) {
11         Object value = redisTemplate.opsForValue().get("num");
12         // 2.1 判断num为空return
13         if (ObjectUtils.isEmpty(value)){
14             return;
15         }
16         // 2.2 把Redis的num加1
17         redisTemplate.opsForValue().increment("num");
18         // 2.3 释放锁,del
19         redisTemplate.delete("lock");
20     }else {
21         // 3.获取锁失败,每隔0.1秒再获取
22         try {
23             Thread.sleep(100);
24             testLock();
25         } catch (InterruptedException e) {
26             e.printStackTrace();
27         }
28     }
29 }
复制代码

启动SpringBoot项目,关闭之前的集群,重新使用一个配置文件开启Redis,设置测试num键为0

再开一个远程连接,进行并发测试,发起1000次请求,100次并发

ab -n 1000 -c 100 http://192.168.56.1:8080/testLock

得到测试结果:

[root@k8s-node01 ~]#  ab -n 1000 -c 100 http://192.168.56.1:8080/testLock
This is ApacheBench, Version 2.3 <$Revision: 1430300 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 192.168.56.1 (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests


Server Software:
Server Hostname: 192.168.56.1
Server Port: 8080

Document Path: /testLock
Document Length: 0 bytes

Concurrency Level: 100
Time taken for tests: 5.770 seconds
Complete requests: 1000
Failed requests: 0
Write errors: 0
Total transferred: 92000 bytes
HTML transferred: 0 bytes
Requests per second: 173.30 [#/sec] (mean)
Time per request: 577.037 [ms] (mean)
Time per request: 5.770 [ms] (mean, across all concurrent requests)
Transfer rate: 15.57 [Kbytes/sec] received

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 1 2.0 0 11
Processing: 1 292 843.9 2 5207
Waiting: 1 292 843.9 2 5207
Total: 1 293 845.5 2 5214

Percentage of the requests served within a certain time (ms)
50% 2
66% 2
75% 3
80% 8
90% 1155
95% 2477
98% 3384
99% 3990
100% 5214 (longest request)

返回Redis中查看,结果如下:

[root@k8s-node01 ~]# redis-server /etc/redis.conf 
[root@k8s-node01 ~]# redis-cli -p 6379
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> set num "0"
OK
127.0.0.1:6379> get num
"1000"

可以得到1000的结果,没有出现数据偏差,则分布锁使用成功。

14.4.4、UUID解决误删问题

有多个进程同时访问一个数据,设置一段过期时间

进程a:上锁-->具体操作,假设操作时服务器突然卡顿,过期时间内未完成操作,锁自动释放。

此时进程b抢到锁-->具体操作。正在b操作的时候,a服务器恢复正常继续完成操作,由于操作结束后带有释放锁的指令,因此将b的锁提前释放了。此时下一个进程在b没处理完的时候就进来抢占锁,造成了误删问题。

解决方案

UUID防止误删,生成唯一的锁uuid

set lock uuid nx ex 10

释放锁时,先判断当前UUID是否和要释放的锁一样

改造上述代码:注意比较时避免空指针异常

复制代码
 1 /**
 2  * 测试num的递增,快速访问testLock
 3  */
 4 @GetMapping("testLock")
 5 public void testLock(){
 6     String uuid = UUID.randomUUID().toString();
 7     // 1.获取锁,setnx,设置过期时间,任意设置值
 8     Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,3, TimeUnit.SECONDS);
 9 
10     // 2.获取锁成功,查询num的值
11     if (lock) {
12         Object value = redisTemplate.opsForValue().get("num");
13         // 2.1 判断num为空return
14         if (ObjectUtils.isEmpty(value)){
15             return;
16         }
17         // 2.2 把Redis的num加1
18         redisTemplate.opsForValue().increment("num");
19         // 2.3 释放锁,del
20         // 判断比较uuid值是否一样
21         String lockUuid = redisTemplate.opsForValue().get("lock");
22         if (uuid.equals(lockUuid)) {
23             redisTemplate.delete("lock");
24         }
25     }else {
26         // 3.获取锁失败,每隔0.1秒再获取
27         try {
28             Thread.sleep(100);
29             testLock();
30         } catch (InterruptedException e) {
31             e.printStackTrace();
32         }
33     }
34 }
复制代码

14.4.5、Lua脚本保证删除原子性

问题:删除操作缺乏原子性

a删除操作时正要删除,还没有删除的时候(如判断可以删了)锁到了过期时间,自动释放

b接过锁后执行操作,此时a要执行删除操作,a将b的锁释放,导致b错误

修改原代码如下:

复制代码
 1 @GetMapping("testLockLua")
 2 public void testLockLua(){
 3     // 1.声明一个uuid,将其作为一个value放入key中
 4     String uuid = UUID.randomUUID().toString();
 5     
 6     // 2.定义一个锁:Lua脚本可以使用同一把锁实现删除
 7     // 访问编号为25的商品
 8     String skuId = "25";
 9     // 锁住的是商品信息
10     String lockKey = "lock:" + skuId;
11     
12     // 3.获取锁
13     Boolean lock = redisTemplate.opsForValue().setIfAbsent(lockKey, uuid, 3, TimeUnit.SECONDS);
14     
15     // 第一种:lock与过期时间中间不写任何的代码
16     //redisTemplate.expire("lock",10,TimeUnit.SECONDS);
17 
18     if (lock) {
19         // 执行的业务逻辑开始
20         Object value = redisTemplate.opsForValue().get("num");
21         // 2.1 判断num为空return
22         if (ObjectUtils.isEmpty(value)){
23             return;
24         }
25         // 非空时如果在此处出现异常,则delete删除失败,锁永远存在
26         // 2.2 把Redis的num加1
27         redisTemplate.opsForValue().increment("num");
28         
29         // 使用Lua脚本锁
30         String script = "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
31         // 使用Redis执行Lua执行
32         DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
33         redisScript.setScriptText(script);
34         // 设置返回值类型为Long,返回一个单例key
35         // 因为删除判断的时候返回的0,所以需要给其封装为数据类型,如果不封装会默认返回String类型
36         // 那么返回字符串与0会发生错误
37         redisScript.setResultType(Long.class);
38         redisTemplate.execute(redisScript, Collections.singletonList(lockKey),uuid);
39 
40         // 2.3 释放锁,del
41         // 判断比较uuid值是否一样
42         //String lockUuid = redisTemplate.opsForValue().get("lock");
43         //if (uuid.equals(lockUuid)) {
44         //    redisTemplate.delete("lock");
45         //}
46     }else {
47         // 3.获取锁失败,每隔0.1秒再获取,其他线程等待
48         try {
49             // 睡眠
50             Thread.sleep(100);
51             testLockLua();
52         } catch (InterruptedException e) {
53             e.printStackTrace();
54         }
55     }
56 }
复制代码

测试不再描述,结果与上一节类似

14.4.6、总结

比较安全的做法,加入UUID

加锁:

Boolean lock = redisTemplate.opsForValue().setIfAbsent(lockKey, uuid, 3, TimeUnit.SECONDS);

使用Lua释放锁:

复制代码
// 使用Lua脚本锁
String script = "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
// 使用Redis执行Lua执行
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(script);
// 设置返回值类型为Long,返回一个单例key
// 因为删除判断的时候返回的0,所以需要给其封装为数据类型,如果不封装会默认返回String类型
// 那么返回字符串与0会发生错误
redisScript.setResultType(Long.class);
redisTemplate.execute(redisScript, Collections.singletonList(lockKey),uuid);
复制代码

使用递归进行重试:

try {
    // 睡眠
    Thread.sleep(100);
    testLockLua();
} catch (InterruptedException e) {
    e.printStackTrace();
}

为了确保分布式锁可用,需要同时满足:

  • 互斥性:在任意时刻,只有一个客户端能持有锁

  • 不会发生死锁:即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁

  • 加锁和解锁需要是同一个客户端,客户端自己不能把别人加的锁解了

  • 加锁和解锁必须具有原子性

15、Redis 6.0新功能

15.1、ACL

Access Control List 访问控制列表

允许根据可以执行的命令和可以访问的键限制某些连接

在Redis 5 版本之前,安全规则只有密码规则,还有通过rename命令调整高危命令如flushdb、keys * 、shutdown等。

ACL对用户进行更细颗粒度的权限控制

  • 接入权限:用户名和密码

  • 可以执行的命令

  • 可以操作的key

命令

1、使用acl list 命令展现用户权限列表

参 数 说明
user 用户
default 表示默认用户名,或则自己定义的用户名
on 表示是否启用该用户,默认为off(禁用)
#... 表示用户密码,nopass表示不需要密码
~* 表示可以访问的Key(正则匹配)
+@ 表示用户的权限,+/-表示授权还是销权; @为权限类。+@all 表示所有权限

2、使用acl cat查看数据类型能够执行的命令

3、使用acl whoami 查看当前用户

示例:

127.0.0.1:6379> acl list
1) "user default on nopass ~* &* +@all"
127.0.0.1:6379> acl cat string
1) "setnx"
2) "getrange"
3) "append"
4) "setrange"
5) "decr"
6) "incrby"
7) "stralgo"
8) "setex"
9) "substr"
10) "incrbyfloat"
11) "get"
12) "mget"
13) "getset"
14) "set"
15) "msetnx"
16) "incr"
17) "mset"
18) "getex"
19) "decrby"
20) "getdel"
21) "psetex"
22) "strlen"
127.0.0.1:6379> acl whoami
"default"

4、使用acl setuser <name> 创建和编辑用户ACL

ACL规则

1)启动和禁用用户

on:激活

off:禁用。但已验证的连接仍然可以工作。如果默认用户被标记为off,则新连接将在未进行身份验证的情况下启动,并要求用户使用AUTH选项发送AUTH或HELLO,以便以某种方式进行身份验证

2)权限的添加删除

+<command> :将指令添加到用户可以调用的指令列表中

-<command> :从用户可执行指令列表中移除指令

+@<category> :添加该类别中用户要调用的所有指令,有效类别为@admin、@set、@sortedset……等等,通过调用ACL CAT命令可以查看完整列表。@all表示所有命令,包括当前存在于服务器中的命令,以及将来将通过模块加载的命令

-@<category> :从用户可调用指令中移除类别

allcommands :+@all

nocommands :-@all

3)可操作性键的添加或删除

~<pattern> :添加可作为用户可操作的键的模式,可以加入正则表达式。例如~*允许操作所有的键。

举例:

# 将 xiaozhang 用户增加密码abc123、设置访问以 name 开头的 key 的权限和 set 权限
127.0.0.1:6379> ACL SETUSER xiaozhang on >abc123 ~name* +set
OK

# 我们可以看到 xiaozhang 目前只具有 set 权限
127.0.0.1:6379> acl list
user xiaozhang on #6ca13d52ca70c883e0...392593af6a84118090 ~name* -@all +set

# 切换用户
127.0.0.1:6379> AUTH xiaozhang abc123
OK

# 设置键值对
127.0.0.1:6379> set name xiaozhang
OK

# 没有获取权限
127.0.0.1:6379> get name
NOPERM this user has no permissions to run the 'get' command or its subcommand

15.2、IO多线程

客户端交互部分的网络IO交互处理模块多线程,而非执行命令多线程。执行命令仍然是单线程。

Redis的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程。

多线程IO默认不开启,需要在配置文件中配置

io-thread-do-reads yes

io-threads 4

15.3、工具支持Cluster集群

搭建集群不再需要ruby环境

redis-benchmark工具支持cluster模式,通过多线程的方式对多个分片进行压测

15.4、其他新特性

1、RESP3 新的Redis通信协议:优化服务端与客户端之间的通信

2、Client side caching 客户端缓存:基于RESP3协议实现的客户端缓存功能,将客户端经常访问的数据cache到客户端,减少TCP网络交互,进一步提升缓存性能

3、Proxy 集群代理模式:Cluster拥有像单实例一样的接入方式,代理不改变Cluster的功能限制,如不支持跨slot的多key操作等

4、Modules API

 

posted on   zrm0612  阅读(32)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示