Redis学习笔记

Redis

结合视频看更香哦: 尚硅谷Redis

一、NoSQL 数据库简介

1、技术发展

技术的分类

1、解决功能性的问题:Java、Jsp、RDBMS、Tomcat、HTML、Linux、JDBC、SVN

2、解决扩展性的问题:Struts、Spring、SpringMVC、Hibernate、Mybatis

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

Redis 是一种典型的 NoSQL 数据库

Web1.0的时代,数据访问量很有限,用一夫当关的高性能的单点服务器可以解决大部分问题。

1650362862247

随着Web2.0的时代的到来,用户访问量大幅度提升,同时产生了大量的用户数据。加上后来的智能移动设备的普及,所有的互联网平台都面临了巨大的性能挑战。

1650362900005

使用 Redis 解决内存压力:

1650362950998

使用 Redis 解决 IO 压力:

1650362993575

2、NoSQL 数据库

NoSQL(NoSQL = **\Not Only SQL\ ),意即“不仅仅是SQL”,泛指非关系型的数据库。

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

  • 不遵循SQL标准。
  • 不支持ACID。【事务的四个特性:原子性,一致性,隔离性,持久性】,但并不是不支持事务
  • 远超于SQL的性能。

NoSQL 支持的场景 :

  • 对数据高并发的读写
  • 海量数据的读写
  • 对数据高可扩展性的

NoSQL 不支持的场景 :

  • 需要事务支持
  • 基于sql的结构化查询存储,处理复杂的关系,需要即席查询。

NoSQL 数据库的类型:

(1)*Memcache*

  1. 很早出现的NoSql数据库

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

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

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

(2)*Redis*

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

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

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

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

(3)*MongoDB*

  1. 高性能、开源、模式自由(schema free)的****文档型数据库****

  2. 数据都在内存中, 如果内存不足,把不常用的数据保存到硬盘

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

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

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

二、Redis 概述安装

1、Redis 概述

  • Redis是一个开源的key-value存储系统。
  • 和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)和hash(哈希类型)。
  • 这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。
  • 在此基础上,Redis支持各种不同方式的排序。
  • 与memcached一样,为了保证效率,数据都是缓存在内存中。
  • 区别的是Redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件。
  • 并且在此基础上实现了master-slave(主从)同步。

应用场景:

  • 高频次,热门访问的数据,降低数据库IO
  • 分布式架构,做 sessio 共享
  • 多样的数据结构存储持久化数据

1650364168988

2、Redis 安装

redis 中文下载网站: CRUG网站 (redis.cn)

在 Linux 环境中安装 redis,使用 xftp 将 .tar.gz 压缩文件存放到 /opt 目录下。

(1)Redis 是基于 gcc 环境的,先安装 gcc

yum install gcc

(2)解压 redis 压缩包

tar -xzvf redis-6.2.1.tar.gz

(2)进入到 redis 中目录去,使用 make 进行编译。

如果安装不成功,出现以下错误。

1650368481029

运行:make distclean

(3)编译完,进行安装 make install

(4)默认安装在 /usr/local/bin 目录下

目录下的文件详情:

redis-benchmark:性能测试工具,可以在自己本子运行,看看自己本子性能如何

redis-check-aof:修复有问题的AOF文件,rdb和aof后面讲

redis-check-dump:修复有问题的dump.rdb文件

redis-sentinel:Redis集群使用

redis-server:Redis 服务器启动命令

redis-cli:客户端,操作入口

1650374479944

3、Redis 启动

前台启动方式:不推荐

在 /usr/local/bi 目录下,执行 redis-server

1650375218870

启动后不能输入任何命令,也不能动。

后台启动方式:

(1)将 /opt/redis-6.2.1/redis.conf 文件备份文件,以防修改出错

cp /opt/redis-6.2.1/redis.conf /etc/redis.conf

(2)修改 redis.conf 文件中的 daemonize 为 yes

1650375438252

(3)通过指定配置文件启动 redis

redis-server /etc/redis.conf

(4)查看是否启动成功,看到端口:6379

ps -ef | grep redis

1650375620327

(5)测试是否连接成功 redis-cli 进入命令行客户端

或者通过端口号进入命令端 : redis-cli -p 6379

1650380843738

(6)exit 退出到终端,使用以下命令关闭redis

redis-cli shutdown

或者使用 ps -ef 找到进程号,使用 kill 命令终止redis

4、Redis 相关知识

(1)Redis 中默认有 16 个数据库,从 0开始,默认就是使用的是 0 号数据库

使用 select index 切换数据库

1650426287885

(2)串行 vs 多线程+锁(memcached) vs 单线程+多路IO复用(Redis)

与Memcache三点不同: 支持多数据类型,支持持久化,单线程+多路IO复用

三、Redis 五大数据类型

Redis 是针对 key-value 进行存储的,了解一下Redis中对key的操作:

keys * 查询当前库的所有 key (指定查找某个key : keys *1

exists key 判断某个key是否存在

(Integer) 1 表示有一个 k1

1650427455258

type key 查看你的key是什么类型

del key 删除指定的key数据

unlink key 根据value选择非阻塞删除

仅将keys从keyspace元数据中删除,真正的删除会在后续异步操作, 意思就是 unlik key 并不是马上删除

expire key 10 10秒钟:为给定的key设置过期时间

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

1650427710215

select index 命令切换数据库

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

flushdb 清空当前库

flushall 通杀全部库

1、String 类型

(1)简介

String是Redis最基本的类型,你可以理解成与Memcached一模一样的类型,一个key对应一个value。

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

String类型是Redis最基本的数据类型,一个Redis中字符串value最多可以是512M

(2)常用命令

set <key> <value> 添加键值对, 如果重复增加相同 key 的值,后一个会把前一个 value 值覆盖掉

get <key> 查询对应键值

append <key> <value> 将给定的 追加到原值的末尾

strle <key>获得值的长度

setnx <key><value> 只有在 key 不存在时 设置 key 的值

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

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

incrby / decrby <key> <步长>将 key 中储存的数字值增减。自定义步长。

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

mget <key1><key2><key3> ..... 同时获取一个或多个 value

msetnx <key1><value1><key2><value2> ..... 同时设置一个或多个 key-value 对,当设置的所有的 key 都不存在时,该操作才会成功。如果有一个 key 存在,那么 都不会成功。

1650435622662

getrange <key> <起始位置> <结束位置>

截取value指定范围的内容,类似java中的substring

setrange <key> <起始位置> <value>

用 覆写所储存的字符串值,从<起始位置>开始(索引从0开始)。

setex <key> <过期时间> <value> 设置键值的同时,设置过期时间,单位秒。

getset <key> <value> 以新换旧,设置了新值同时获得旧值。

(3)String 类型底层的数据结构:

String 的数据结构为简单动态字符串(Simple Dynamic String,缩写SDS)。是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配.

1650436414608

如图中所示,内部为当前字符串实际分配的空间capacity一般要高于实际字符串长度len。当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次只会多扩1M的空间。需要注意的是字符串最大长度为512M。

2、List 列表

**Redis 列表是一个 key 对应多个 value 值;**并且里面的值是可以重复的。

(1)简介

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

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

1650436650728

(2)常用的命令

lpush/rpush <key><value1><value2><value3> ....

从左边/右边插入一个或多个值。

lrange <key> <start> <stop> 0 -1 是取出所有的值。

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

1650437138283

lpush :

存进去是: v1 v2 v3 的顺序

取出来: v3 v2 v1 的顺序。

lpush采用双向表的头插法、不断的将新值插到链表的头部,将旧值往尾部顶。

rpush :

1650437554382

存进去是 : c1 c2 c3

取出来是 : c1 c2 c3

rpush 采用的是双向链表的尾插法。不断的将新值插到链表的尾部,旧值往头部顶

lpop/rpop <key>

从左边/右边弹出一个值。值在键在,值光键亡。弹出来的值,列表中的 key-value 都不存在了。

rpoplpush <key1><key2>

从列表右边吐出一个值,插到列表左边。

lindex <key><index>按照索引下标获得元素(从左到右)

lle <key>获得列表长度

linsert <key> before/after <value><newvalue>

在的 前面/后面 插入插入值

lrem <key><n><value>

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

lset <key> <index> <value>

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

(3)底层数据结构

List的数据结构为快速链表quickList。

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

它将所有的元素紧挨着一起存储,分配的是一块连续的内存。当数据量比较多的时候才会改成quicklist。

因为普通的链表需要的附加指针空间太大,会比较浪费空间。比如这个列表里存的只是int类型的数据,结构上还需要两个额外的指针prev和next。

1650438440526

Redis 将链表ziplist 结合起来组成了quicklist。也就是将多个ziplist使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。

3、Set 集合

一个 key 对应多个 value ,但是每一个 value 都不能是重复的。并且无序

(1)简介

Redis set 对外提供的功能与 list 类似是一个列表的功能,特殊之处在于set是可以 自动排重 的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。

Redis的Set是string类型的无序集合。它底层其实是一个value为null的hash表,所以添加,删除,查找的复杂度都是O(1)

一个算法,随着数据的增加,执行时间的长短,如果是O(1),数据增加,查找数据的时间不变

(2)常用的命令

sadd <key> <value1> <value2> .....

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

smembers <key> 取出该集合的所有值。

1650438950288

sismember <key> <value>

判断集合是否为含有该值,有1,没有0

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

srem <key><value1><value2> .... 删除集合中的某个元素。

spop <key> 随机从该集合中吐出一个值。key-value都消失

srandmember <key><n> 随机从该集合中取出n个值。不会从集合中删除 。

smove <source> <destination> value

把集合中一个值从一个集合移动到另一个集合

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

sunio <key1><key2> 返回两个集合的并集元素。

sdiff <key1> <key2>

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

(3)set 集合数据结构

Set数据结构是dict字典,字典是用哈希表实现的。

Java中HashSet的内部实现使用的是HashMap,只不过所有的value都指向同一个对象。Redis的set结构也是一样,它的内部也使用hash结构,所有的value都指向同一个内部值。

4、hash 类型

(1)简介

Redis hash 是一个键值对集合。

Redis hash 是一个string类型的 field 和 value 的映射表,hash特别适合用于存储对象。类似Java里面的Map<String,Object>

用户ID为查找的key,存储的value用户对象包含姓名,年龄,生日等信息,如果用普通的key/value结构来存储

1650440362154

(2)常用命令

hset <key> <field> <value>

给集合中的 键赋值

hget <key1> <field>

从集合取出 value

hmset <key1><field1><value1><field2><value2>...

批量设置hash的值

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) 数据结构

Hash类型对应的数据结构是两种:ziplist(压缩列表),hashtable(哈希表)。当field-value长度较短且个数较少时,使用ziplist,否则使用hashtable。

5、 ZSet (sorted set)有序集合

value不可重复,但是 评分可以重复,而且有序

(1)简介

Redis 有序集合和集合一样也是 string 类型元素的集合,且不允许重复的成员。

不同的是每个元素都会关联一个 double 类型的分数。Redis 正是通过分数来为集合中的成员进行从小到大的排序。

有序集合的成员是唯一的,但分数 ( score ) 却可以重复。

集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。

集合中最大的成员数为 2^32 – 1 ( 4294967295 ) , 每个集合可存储 40 多亿个成员。

(2)常用命令

zadd <key> <score1> <value1> <score2> <value2>…

将一个或多个 member 元素及其 score 值加入到有序集 key 当中。

zrange <key> <start> <stop> [WITHSCORES]

返回有序集 key 中,下标在 之间的元素,0 -1 是返回所有的元素

带WITHSCORES,可以让分数一起和值返回到结果集。

1650441774719

zrangebyscore key minmax [withscores] [limit offset count]

返回有序集 key 中,所有 score 值介于 mi 和 max 之间(包括等于 mi 或 max )的成员。有序集成员按 score 值递增(从小到大)次序排列。

zrevrangebyscore key maxmi [withscores] [limit offset count]

同上,改为从大到小排列。

1650442030112

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

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

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

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

(3)数据结构

SortedSet(zset)是Redis提供的一个非常特别的数据结构,

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

zset底层使用了两个数据结构

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

(2)跳跃表,跳跃表的目的在于给元素value排序,根据score的范围获取元素列表。

6、跳跃表介绍

下面是普通的链表,在对数据进行 读/写 时,还需要一个个遍历,读取。效率太慢了。

1650443499789

而跳跃表则将链表进行了优化,跳跃表在已排序链表的基础上,将元素进行分层:

1650443632748

当查找某个元素时,会从最上层开始找。

假设找 31 这个节点:

  • 从第二层开始, 1 < 31 ,因此向右走,21 < 31 ,因此还需向右走,但是 右边已经没有节点了。
  • 然后 从 21 节点向下走,走到第一层的 21节点,向右走是 41,明显比查找的 31 节点大。
  • 从第一层继续向下走到第0 层的21节点,向右走找到了31节点

四、Redis 配置文件 redis.conf

(1)第一部分

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

大小写不敏感

1650445128052

(2)第二部分:INCLUDES

类似jsp中的include,多实例的情况可以把公用的配置文件提取出来

1650616833263

(3)第三部分:NETWORK

bind 127.0.0.1 -::1 只支持本机访问,注释掉,可以通过远程访问。

protected-mode yes : 保护模式。改成 no 支持远程访问

port 6379 : 端口号

timeout 0 : 超时操作,当启动 redis 时,超过多长时间 断开连接。 0 表示永不超时, 默认以 s 为单位。

tcp-keepalive 300 : 监测周期,redis 每隔 300s 检查是否有人操作 redis,如果没有操作,就断开连接

(4)第四部分:GENERAL

daemonize yes : 是否开启后台启动

pidfile /var/run/redis_6379.pid : 将redis 启动时的 pid 进程号,保存到这个 /var/run/redis_6379.pid 文件中去。

loglevel notice : 日志级别,一共四个级别。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ij00nILf-1659344719090)(https://images-1313160403.cos.ap-beijing.myqcloud.com/MarkDown/202208011703736.png)]

logfile “” : 日志路径

(5)第五部分:SECURITY

requirepass foobared : 设置 redis 密码。默认是没有的,如果以后项目中使用,一定要设置密码。

还可以通过命令行设置密码 : config set requirepass xxxx

1650446171705

(6)第六部分:CLIENTS

maxclients 10000 : 设置可以连接的客户端数量。默认是 10000 个、超出数量则会拒绝访问。

(7)第七部分:MEMORY MANAGEMENT

maxmemory 设置redis的内存

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

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

MAXMEMORY POLICY : 设置移除规则。

1650446776049

volatile-lru:使用LRU算法移除key,只对设置了过期时间的键;(最近最少使用)

allkeys-lru:在所有集合key中,使用LRU算法移除key

volatile-random:在过期集合中移除随机的key,只对设置了过期时间的键

allkeys-random:在所有集合key中,移除随机的key

volatile-ttl:移除那些TTL值最小的key,即那些最近要过期的key

noeviction:不进行移除。针对写操作,只是返回错误信息

五、消息与订阅

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

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

订阅消息:

1650447172590

发布消息:

1650447193494

命令行模式模拟订阅与发布:

SUBSCRIBE channel1 订阅 channel1 频道

1650447348508

PUBLISH channel1 hello 向 channel1 发布消息 hello 1650447479169

客户端可以订阅多个频道。

六、三种新型数据类型

1、Bitmaps

(1)简介

现代计算机用二进制(位) 作为信息的基础单位, 1个字节等于8位, 例如“abc”字符串是由3个字节组成, 但实际在计算机存储时将其用二进制表示, “abc”分别对应的ASCII码分别是97、 98、 99, 对应的二进制分别是01100001、 01100010和01100011,如下图

1650448093433

合理地使用操作位能够有效地提高内存使用率和开发效率。

​ Redis提供了Bitmaps这个“数据类型”可以实现对位的操作:

(1) Bitmaps本身不是一种数据类型, 实际上它就是字符串(key-value) , 但是它可以对字符串的位进行操作。

(2) Bitmaps单独提供了一套命令, 所以在Redis中使用Bitmaps和使用字符串的方法不太相同。 可以把Bitmaps想象成一个以位为单位的数组, 数组的每个单元只能存储0和1, 数组的下标在Bitmaps中叫做偏移量。

1650448128492

(2)命令

setbit <key> <offset> <value>

设置Bitmaps中某个偏移量的值(0或1),偏移量 offset 从 0开始。 偏移量==数组下标

getbit <key> <offset>

获取Bitmaps中某个偏移量的值

bitcount <key> [start end]

统计字符串从start字节到end字节比特值为1的数量

bitop and(or/not/xor) <destkey> [key…]

bitop是一个复合操作, 它可以做多个Bitmaps的and(交集) 、 or(并集) 、 not(非) 、 xor(异或) 操作并将结果保存在destkey中。

Bitmaps 适用于大量数据的时候。

2、HyperLogLog

什么是基数?

集合中不重复的数。

(1)简介

在工作当中,我们经常会遇到与统计相关的功能需求,比如统计网站PV(PageView页面访问量),可以使用Redis的 incr、incrby轻松实现。

但像UV(UniqueVisitor,独立访客)、独立IP数、搜索记录数等需要去重和计数的问题如何解决?这种求集合中不重复元素个数的问题称为基数问题。

解决基数问题有很多种方案:

(1)数据存储在MySQL表中,使用distinct count计算不重复个数

(2)使用Redis提供的hash、set、bitmaps等数据结构来处理

以上的方案结果精确,但随着数据不断增加,导致占用空间越来越大,对于非常大的数据集是不切实际的。

能否能够降低一定的精度来平衡存储空间?Redis推出了HyperLogLog

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

在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。

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

总结:

简单来说如果需要对基数进行统计,就需要用到 HLL,set 也可以但是只是简单的求基数,使用 HLL会节省大量的空间。

(2)常用命令

pfadd <key>< element> [element ...]

添加指定元素到 HyperLogLog 中

pfcount<key> [key ...]

计算HLL的近似基数,可以计算多个HLL,比如用HLL存储每天的UV,计算一周的UV可以使用7天的UV合并计算即可

1650450186806

pfmerge <destkey> <sourcekey> [sourcekey ...]

将一个或多个HLL合并后的结果存储在另一个HLL中,比如每月活跃用户可以使用每天的活跃用户来合并计算可得

1650450427206

3、Geospatial

(1)简介

Redis 3.2 中增加了对GEO类型的支持。GEO,Geographic,地理信息的缩写。该类型,就是元素的2维坐标,在地图上就是经纬度。redis基于该类型,提供了经纬度设置,查询,范围查询,距离查询,经纬度Hash等常见操作。

(2)命令

geoadd <key> < longitude> <latitude> <member>

添加地理位置(经度,纬度,名称). 俩极无法增加

geopos <key> <member> 获得指定地区的坐标值

geodist <key> <member1> <member2> [m|km|ft|mi ]

获取两个位置之间的直线距离

georadius<key>< longitude><latitude>radius m|km|ft|mi

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

mi :英里 ft :英尺

七、Jedis 操作

增加依赖

        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.2.0</version>
        </dependency>
public class redisTest {
    public static void main(String[] args) {
        // 创建 jedis 对象
        // 传入虚拟机主机地址,端口号
        Jedis jedis = new Jedis("192.168.200.130",6379);
        // 测试是否连接成功
        System.out.println(jedis.ping());
    }
}

如果出现连接超时情况:查看虚拟机是否开启 6379 端口。

firewall-cmd --permanent --query-port=6379/tcp 查看 6379 是否打开

firewall-cmd --permanent --add-post=6370/tcp 打开 6379 端口

firewall-cmd --reload 重新载入启动的端口。一定要执行这一步!!!

表示连接成功:

1650529343696

1、Jedis 常用操作

  // 对 key 的操作
    @Test
   public  void test01(){
        Jedis jedis = new Jedis("192.168.200.130",6379);

        // String 类型--存放数据
        jedis.set("name","rose");
        // 判断 key 类型
        System.out.println(jedis.type("name"));
        // 判断当前数据库中的 key是否包含 key
        System.out.println(jedis.exists("name"));
        // 设置 key 的生存时间
        jedis.expire("name",20);
        // 判断 key 的生存时间
        System.out.println(jedis.ttl("name"));
        // 删除 key
        System.out.println(jedis.del("name"));
    }

    // 对 String 类型的操作
    @Test
    public void test02(){
        Jedis jedis = new Jedis("192.168.200.130",6379);

        // String 类型只能是 String类型的
        jedis.set("age","19");
        System.out.println(jedis.get("age"));
        // append 追加
        System.out.println(jedis.append("age", "newAge"));
        // 批量增加 mset,多个value 写在同一个 "" 内
        jedis.mset("name","张三,李四,王五","hobby","打篮球,踢足球");
        // 批量获取 mget
        System.out.println(jedis.mget("name","hobby"));
        // 获取 name 集合的长度
        System.out.println(jedis.strlen("name"));

        jedis.flushDB();
    }

    // 对 List 类型进行操作
    @Test
    public void test03(){
        Jedis jedis = new Jedis("192.168.200.130",6379);
        // 头插法。向 列表的头部插入数据.多个数据 , 分割
        jedis.lpush("游戏","王者","LOL","飞车","飞车");
        // 获取 游戏 列表的所有的数据
        List<String> games = jedis.lrange("游戏", 0, -1);
        games.forEach(System.out::println);
        // 根据下标获取数据。 从 0 开始
        System.out.println(jedis.lindex("游戏", 2));
        // 获取列表的长度
        System.out.println(jedis.llen("游戏"));
        // 列表左边弹出一个值
        System.out.println(jedis.lpop("游戏"));
        // 删除指定数量的value
        System.out.println(jedis.lrem("游戏",1, "飞车"));
        jedis.flushDB();

    }

    // 对 Set 集合 的操作
    @Test
    public void test04(){
        Jedis jedis = new Jedis("192.168.200.130",6379);
        // 增加数据
        jedis.sadd("books","钢铁侠1","钢铁侠2","钢铁侠1");
        // 获取
        Set<String> books = jedis.smembers("books");
        books.forEach(System.out::println);

        System.out.println(jedis.sismember("books", "钢铁侠3"));
        // 返回集合的元素数
        System.out.println(jedis.scard("books"));
        // 删除指定的元素
        System.out.println(jedis.srem("books", "钢铁侠1"));
        jedis.flushDB();
    }
    // 对 hash 类型操作
    @Test
    public void test05(){
        Jedis jedis = new Jedis("192.168.200.130",6379);
        Map<String,String> map = new HashMap<>();
        map.put("name","lisi");
        map.put("hobby","ball");

        jedis.hset("users",map);
        // 获取name属性
        System.out.println(jedis.hget("users", "name"));
        System.out.println(jedis.hlen("users"));

        System.out.println(jedis.keys("*"));
        System.out.println(jedis.hvals("users"));
    }
    // 对 zset 类型操作
    @Test
    public void test06(){
        Jedis jedis = new Jedis("192.168.200.130",6379);
        jedis.zadd("foods",300,"红烧肉");
        jedis.zadd("foods",200,"丸子");
        jedis.zadd("foods",400,"宫保鸡丁");

        System.out.println(jedis.zrangeByScore("foods", 200, 400));
                // 关闭 redis
        jedis.close();
        
    }

2、完成手机验证码功能

要求:

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

​ 使用 Random 类随机生成 6 位数字码。将验证码放到 redis 中 设置 ttl 为 120 s

2、输入验证码,点击验证,返回成功或者失败

将输入的验证码和 redis中的验证码进行比较,一致成功,不一致失败

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

使用 redis 中的 incr 命令,每次输入 将 incr +1 ,加到 >= 2 ,就停止输入。

// 模拟发送手机验证码的过程
public class PhoneCode {
    public static void main(String[] args) {
        String code = getCode();
        System.out.println(code);
        verifyCodeCount("123456",code);
    }

    //3、 判断验证码是否正确
    public static void verifyCode(String phone,String code){
        if (phone.equals(code)){
            System.out.println("验证码正确");
        }else{
            System.out.println("验证码失败");
        }
    }

    //2、将验证码放入到 redis 中,并设置生存周期.统计发送次数
    public static void verifyCodeCount(String phone,String code){
        Jedis jedis = new Jedis("192.168.200.130",6379);

        // 设置验证码的 key
        String codeKey = phone + "-code";
        // 设置发送次数的 key
        String sendCodeCountKey = phone + "-count";

        // 取出发送次数,进行判断
        String sendCodeCount = jedis.get(sendCodeCountKey);

        if (sendCodeCount == null){
            // 说明一次也没有发送
            // 设置 sendCodeCountKey 的发送次数为 1,并设置 一天时间内有效。
            jedis.setex(sendCodeCountKey,60 * 60 *24,"1");
        }else if (Integer.parseInt(sendCodeCount) <= 2){
            // 说明发送过了,但是没有达到三次,使用 incr 将发送次数 + 1
            jedis.incr(sendCodeCountKey);
        }else if (Integer.parseInt(sendCodeCount) > 2){
            // 超过三次
            System.out.println("每天接受的验证码只能发送三次");
            jedis.close();// 关闭 redis
            return;
        }
        // 将验证码 存入 redis ,并设置过期时间为 120s
        jedis.setex(codeKey,120,code);
    }

    //1、随机生成6位手机验证码
    public static String getCode(){
        Random random = new Random();
        String str = "";
        for (int i = 0; i < 6; i++) {
            // 将随机生成的int拼成 String类型
            str += random.nextInt(10);
        }
        retur  str ;
    }
}

八、SpringBoot 整合 Redis

1、引入redis场景

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

<!-- spring2.X集成redis所需common-pool2-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

自动配置类:RedisAutoConfiguration

(1)自SpringBoot 2.0 之后,SpringBoot 原来使用的 Jedis 替换成了 Lettuce

Jedis : 采用的直连,多个线程操作的话,是不安全的,如果想要避免不安全,使用 jedis pool 连接池

lettuce : 采用 netty。 实例可以再多个线程池中共享,不存在线程不安全的情况。

(2)SpringBoot 中提供了俩种操作 Redis 的模板:

RedisTemplate<Object, Object> : 允许 key-value 是 Object 类型的。

StringRedisTemplate : key-value 是String类型的

	@Bean
	@ConditionalOnMissingBean(name = "redisTemplate")
	@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
	public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        // RedisTemplate 模板,但是这个模板是空的。需要我们自己定义序列化规则。
		RedisTemplate<Object, Object> template = new RedisTemplate<>();
		template.setConnectionFactory(redisConnectionFactory);
		return template;
	}

	@Bean
	@ConditionalOnMissingBean
	@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
	public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
		return new StringRedisTemplate(redisConnectionFactory);
	}

2、redis 相关配置都在 RedisProperties 这个配置类中


#Redis服务器地址
spring.redis.host=192.168.200.130
#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

3、测试 默认使用Lettuce 客户端

@SpringBootTest
class SpringbootRedisApplicationTests {

    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    void contextLoads() {
        redisTemplate.opsForValue().set("k1","v1");
        System.out.println(redisTemplate.opsForValue().get("k1"));
    }

}

使用 RedisTemplate 可以操作不同的数据类型。

1650706153404

1、SpringBoot整合 Redis 之 序列化问题

(1)首先需要明白一点,通过SpringBoot保存数据到 redis 中是需要将数据进行序列化的。

(2)这就需要我们自定义 RedisTemplate 模板

4、自定义配置 RedisTemplate, 实现 key-value 的序列化。再客户端保存的任何数据都是需要经过序列化才能保存到 redis 服务器中的。

@Configuration
public class RedisConfig extends CachingConfigurerSupport {
    // 序列化方式
    // RedisConnectionFactory 连接工厂,自动从 IOC 容器中获取
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        // 配置连接工厂
        template.setConnectionFactory(factory);

        // 使用 jackso 序列化方式
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);

        //key序列化方式
        template.setKeySerializer(new StringRedisSerializer());
        //value序列化
        template.setValueSerializer(jackson2JsonRedisSerializer);
        //value hashmap序列化
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        return template;
    }

    @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.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // 配置序列化(解决乱码的问题),过期时间600秒
        RedisCacheConfiguratio config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(600))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                .disableCachingNullValues();
        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
        retur cacheManager;
    }
}

序列化之前:

1650554324598

序列化之后:

1650554286344

九、Redis 中的事务

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

什么是序列化?

序列化. 序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。. 在序列化期间,对象将其当前状态写入到临时或持久性存储区。.

Redis事务的主要作用就是串联多个命令防止别的命令插队。所有的命令都会按顺序执行。

1、Multi、Exec、Discard 命令

MULTI
标记着事务的开启,事务中的所有命令都会被序列化,按顺序的执行

EXEC
触发并执行事务中的所有命令

DISCARD
清空事务队列,放弃执行事务中的命令,并且客户端会从事务状态中退出

EXEC 命令的回复是一个数组, 数组中的每个元素都是执行事务中的命令所产生的回复。 其中, 回复元素的先后顺序和命令发送的先后顺序一致。

当客户端处于事务状态时, 所有传入的命令都会返回一个内容为 QUEUED 的状态回复(status reply), 这些被入队的命令将在 EXEC 命令被调用时执行

1650595513488

1650595846788

Multi 相当于开启组队,将 俩个 set 命令依次放入到 队列当中,并且按顺序执行。

执行完整个事务就已经完成了。

1650595999044

执行Discard 就会取消所有的命令 ,不会在执行。

2、事务中的错误处理

Multi 阶段出现错误:

1650596504817

所有的命令都不会执行,整个队列都会被取消。

Exec 阶段出现错误:

1650596579434

只有 number 类型才会自增,因此运行时会出错。但是没有错误的命令也会执行。

3、事务中的悲观锁、乐观锁

悲观锁:

顾名思义,就是很悲观,认为所有用户都会修改数据,所以会对数据上锁。只有一个用户能够拿到 锁,并对其数据进行修改,其他用户都会被阻塞,等待你 锁被释放。Java 中的 synchronized 就是 悲观锁机制。

使用场景: 适合多写少读的场景! 只写不读的场景! Redis 很少使用 悲观锁,因为 Redis 作为缓存服务器,以读操作为主,很少写操作

乐观锁:

顾名思义,就是很乐观,不会对数据进行上锁,但是一般会为数据增加一个版本号,在用户每次操作时,获取版本号。 判断版本号是否和数据库中的版本号一致,如果不一致,操作失败。

适用场景: 适合多读少写,或者只读不写的场景。

4、Redis 中的乐观锁

在执行 Multi 之前,先执行 watch key1 key2 , 对多个 key 进行监视,实现乐观锁机制。

如果有至少一个被监视的键在 EXEC 执行之前被修改了, 那么整个事务都会被取消, EXEC 返回nil-reply来表示事务已经失败。

unwatch key1 key2 取消对 key 的监视

用户A的操作:

1650599188463

用户 B 的操作:

1650599260650

5、Redis 事务的特性

单独的隔离操作

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

没有隔离级别的概念

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

不保证原子性

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

此处的原子性是指 Redis 中的事务是没有原子性这个特性的。没有 ACID 的特性。

但是 对于 Redis 而言,Redis的命令是具有原子性的,命令的 原子性 指的是: 一个 操作 的不可以再分, 操作 要么执行,要么不执行

Redis 的 操作 之所以是 原子性 的,是因为 Redis 是单线程的。 Redis 本身提供的所有API都是 原子操作 , Redis 中的事务其实是要保证批量 操作 的 原子性 。

对于 Redis 单线程的理解:

(1) Redis客户端对服务端的每次调用都经历了发送命令,执行命令,返回结果三个过程。其中执行命令阶段,由于Redis是单线程来处理命令的 。

(2)在执行命令阶段,执行的命令虽然不确定,但是确定的是 在 Redis 中不会有俩条命令同时执行。这也就是说 Redis 不会产生高并发的问题

(3)单线程指的是网络请求模块使用了一个线程(所以不需考虑并发安全性),即一个线程处理所有网络请求,其他模块仍用了多个线程。

十、持久化操作

什么是持久化?

持久化功能将内存中的数据同步到磁盘来避免Redis发生异常导致数据丢失的情况。. 当Redis实例重启时,即可利用之前持久化的文件实现数据恢复

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

  1. RDB (Redis DataBase)
  2. AOP(Append Of File)

1、RDB

指定的时间间隔内将内存中的数据集快照写入磁盘, 也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里

(1)RDB 的 工作方式:

  • Redis 调用forks. 同时拥有父进程和子进程。
  • 子进程将数据集写入到一个临时 RDB 文件中。
  • 当子进程完成对新 RDB 文件的写入时,Redis 用新 RDB 文件替换原来的 RDB 文件,并删除旧的 RDB 文件。

这种工作方式使得 Redis 可以从写时复制(copy-on-write)机制中获益。

整个过程中,主进程是不进行任何IO操作的而是利用Fork子进程进行操作,这就确保了极高的性能 如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。

(2)redis.conf 配置文件中 SNAPSHOTTING 部分的内容详解:

dbfilename dump.rdb 备份的文件名

dir ./ 备份文件的所在位置。 默认在 redis 启动目录中。/usr/local/bin/ 下`

stop-writes-on-bgsave-error yes 当硬盘内存满了之后,不会再去执行写操作

rdbcompressio yes 对于生成的备份文件是否进行压缩

rdbchecksum yes 在存储快照后,还可以让redis使用CRC64算法来进行数据校验,

但是这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能(推荐开启yes)

(3)俩种备份的方式:

(1)save 手动指定,持久化执行时间。

1650610904138

默认是禁止掉的。

当 3600s 内有一个 key 发生变化就会执行一次持久化

当 300s 内有 100 个 key 发生变化时就会执行一次持久化

当 60s 内有 10000 个key 发生变化时就会执行一次持久化

(2)后台 通过 执行bgsave进行备份

RDB 的优势 ?

  1. 适合大规模的数据恢复
  2. 对数据完整性和一致性要求不高更适合使用
  3. 节省磁盘空间
  4. 恢复速度快

🔴 模拟 dump.rdb 的备份?

在 redis 启动目录的 dump.rdb 文件中,保存了 redis 的内容,如果该文件丢失或者损坏,会导致 redis 里面的内容丢失。因此需要备份

1、首先在 /etc/redis.conf 文件中配置save规则

​ 自定义 15 s 内至少有2个key发生变化就进行持久化

1650612457036

2、在 15s 内对 redis 中的 key 进行操作。

1650612637643

明显看到 备份文件的字节增多,说明我们增加的 key 已经备份到 dump.rdb中

1650612523397

1650612531320

3、拷贝 一份 dump.rdb 文件,文件名随意。将原来的 dump.rdb 文件删除

4、关闭 redis,重新启动 redis,发现在删除 dump.rdb 文件后,redis 内的 key -value 也都消失了。

1650612649878

5、将拷贝的 dump.rdb 文件改名改成: dump.rdb ,然后再重新启动 redis,他就会自动加载 dump.rdb 文件进行恢复

1650612717257

2、AOF

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

1、启动 AOF

Redis 中默认是不开启 AOF 功能的,需要手动开启:

在 /etc/redis.conf 配置文件中APPEND ONLY MODE 部分 :

appendonly no 改成 yes 开启 AOF 功能

appendfilename "appendonly.aof" 文件名为: appendonly.aof

AOF 的同频设置 :

appendfsync always

始终同步,每次Redis的写入都会立刻记入日志;性能较差但数据完整性比较好

appendfsync everysec

每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失。

appendfsync no redis不主动进行同步,把同步时机交给操作系统。

生成的路径和dump.rdb 文件一致!!

1650613547302

如果同时开启 RDB 和 AOF 。Redis 会默认读取 AOF 中的内容。

2、修复 aof 文件

如果 appendonly.aof 文件损坏如何修复? 当 appendonly.aof 文件损坏我们是启动不了 redis 的。需要使用 启动目录下的 redis-check-aof 进行修复。

执行: redis-check-aof --fix appendonly.aof 就可以修复成功了。

3、AOF 的流程

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

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

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

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

1650614366509

4、恢复备份流程和 rdb 一模一样,通过拷贝、改名、重启redis自动恢复

5、AOF 的优势 ?

  • 备份机制更稳健,丢失数据概率更低。
  • 可读的日志文本,通过操作AOF稳健,可以处理误操作。
    • 比如:不小心 执行 FLUSHALL 命令,可以通过删除 AOF 文件中的 FLUSHALL 命令,并重启即可恢复。

6、AOF 的劣势 ?

  • 比起RDB占用更多的磁盘空间。
  • 因为 AOF 不仅记录数据,还会记录一些写的指令
  • 恢复备份速度要慢。
  • 每次读写都同步的话,有一定的性能压力。
  • 存在个别Bug,造成恢复不能。

3、AOF 和 RDB 如何选择使用?

官方推荐两个都启用。

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

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

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

十一、主从复制

1、简介

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

1650616033530

优点:

  • 读写分离,减轻压力
  • 容灾快速恢复。当一个从机 宕掉,立马切换另一台从机,保证数据的读取。后面会学到 集群 ,会对应多个主机。

2、搭建一主多从的环境

搭建一主俩从步骤:

首先关闭 aof 功能,防止 redis 读取 aof 文件。

(1)在根目录下创建 /myredis 目录,所有的操作再次目录下。

​ 创建三个目录,7001【主节点】,7002【从节点】,7003【从节点】 ,对应 三个 redis 实例:

cd /myredis 
mkdir  7001 7002 7003

(2) 拷贝 /etc/redis.conf 配置文件到 7001,7002,7003目录中:

cp redis-6.2.4/redis.conf 7001
cp redis-6.2.4/redis.conf 7002
cp redis-6.2.4/redis.conf 7003

# 也可以通过一条命令直接拷贝
echo 7001 7002 7003 | xargs -t -n 1 cp redis-6.2.4/redis.conf

(3)修改三个目录中的 redis.conf 配置文件中的 port、dir 【端口号、工作目录】

# 也可以进入到 redis.conf 文件中挨个修改
sed -i -e 's/6379/7001/g' -e 's/dir .\//dir \/myredis\/7001\//g' 7001/redis.conf
sed -i -e 's/6379/7002/g' -e 's/dir .\//dir \/myredis\/7002\//g' 7002/redis.conf
sed -i -e 's/6379/7003/g' -e 's/dir .\//dir \/myredis\/7003\//g' 7003/redis.conf

(4)修改每个 redis 实例的声明 IP

虚拟机本身有多个IP,为了避免将来混乱,我们需要在redis.conf文件中指定每一个实例的绑定ip信息,格式如下:

# redis实例的声明 IP
replica-announce-ip 192.168.150.101

# 也可以执行命令修改
# 逐一执行
sed -i '1a replica-announce-ip 192.168.200.132' 7001/redis.conf
sed -i '1a replica-announce-ip 192.168.200.132' 7002/redis.conf
sed -i '1a replica-announce-ip 192.168.200.132' 7003/redis.conf

(5)启动

# 第1个
redis-server 7001/redis.conf
# 第2个
redis-server 7002/redis.conf
# 第3个
redis-server 7003/redis.conf

(6)连接客户端:

[root@yangzhaoguang myredis]# redis-cli -p 7001
[root@yangzhaoguang bin]# redis-cli -p 7002
[root@yangzhaoguang bin]# redis-cli -p 7003

(7)开启主从关系

​ 现在三个实例还没有任何关系,要配置主从可以使用 replicaof 或者slaveof(5.0以前)命令。俩个是一样的。

俩种配置方式:

  • 在redis.conf中添加一行配置:slaveof <masterip> <masterport>
  • 使用redis-cli客户端连接到redis服务,执行 slaveof/replicaof 命令(重启后失效)
# 在7002,7003 客户端中执行以下命令
SLAVEOF 192.168.200.132 7001

(8)搭建完成,可通过 info replication 再客户端查看主从关系

只能在主节点中写入操作,不能在从节点中写操作

注意:

(1)当 从机关闭服务再重启之后,不再是从服务器而是主服务器,但是数据是可以同步的。

(2)当主服务器关闭服务在重启之后,还是主服务器

3、数据同步

全量同步 :

(1)主从关系搭建好后,从服务器 会向 主服务器 发送同步数据的请求。第一次发送请求。

1650957218139

那么思考一下,master 如何直到 slave 是不是第一次请求呢 ? 需要了解俩个重要的概念

Replication Id:简称 replid,是数据集的标记,id一致则说明是同一数据集。每一个master都有唯一的replid,slave则会继承master节点的replid

offset:偏移量,随着记录在repl_baklog中的数据增多而逐渐增大。slave完成同步时也会记录当前同步的offset。如果slave的offset小于master的offset,说明slave数据落后于master,需要更新。

1650957499901

可以通过 info replication 看到,从服务器的 id 确实和 主服务器的 id 一致、

全量同步的流程:

  • slave节点请求增量同步
  • master节点判断replid,发现不一致,拒绝增量同步,第一次请求 slave 是没有 replid 的。
  • master将完整内存数据生成RDB,发送RDB到slave
  • slave清空本地数据,加载master的RDB
  • master将RDB期间的命令记录在repl_baklog,并持续将log中的命令发送给slave
  • slave执行接收到的命令,保持与master之间的同步

增量同步:

主从第一次同步是全量同步,但是如果 slave 重启之后,就是增量同步

1650958287414

repl_baklog大小有上限,写满后会覆盖最早的数据。如果slave断开时间过久,导致尚未备份的数据被覆盖,则无法基于log做增量同步,只能再次全量同步。

总结:

什么时候执行全量同步?

  • slave节点第一次连接master节点时
  • slave节点断开时间太久,repl_baklog中的offset已经被覆盖时

什么时候执行增量同步?
slave节点断开又恢复,并且在repl_baklog中能找到offset时

简述全量同步和增量同步区别?
全量同步:master将完整内存数据生成RDB,发送RDB到slave。后续命令则记录在repl_baklog,逐个发送给slave。
增量同步:slave提交自己的offset到master,master获取repl_baklog中从offset之后的命令给slave

4、薪火相传

上一个Slave可以是下一个slave的Master,Slave同样可以接收其他 slaves的连接和同步请求,那么该slave作为了链条中下一个的master, 可以有效减轻master的写压力,去中心化降低风险。

该如何理解呢? 举一个生活中的例子:

假如一个领导手底下管理 2 个人,那么很容易就能管理。

但是如果一个领导管理 200 个人,就有点力不从心了,因此可以向下分组,分配小组长,领导管理组长—组长管理组员,就是这么个道理。

拿前面的例子来说,6379 是主服务器,6380 和 6381 都是从服务器,可以将 6381 当做 6380 的从服务器。 这就是薪火相传!

slaveof 192.168.200.130 6380

5、反客为主

使用 slaveof no one 可以将一个 从服务器 变成 主服务器,但这是手动的,可不可以自动完成呢?是可以的,就是下面的 哨兵模式!!

6、哨兵模式

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

哨兵的三个功能:

  • 监控
    • sentinel 会不断的检查 主服务器和从服务器是否工作正常,通过俩种状态判断:
    • sentinel 每隔 1 s 向集群的每个实例发送 ping 命令:
      • 主观下线: 当某个 sentinel 节点发现某个 实例 未在规定时间内响应,则认为该实例是主观下线
      • 客观下线: 若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线。quorum值最好超过Sentinel实例数量的一半
  • 提醒
    • 当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。
  • 自动故障转移
    • 当一个主服务器不能正常工作时,sentinel 会在从服务器中选取一个主服务器,并且让失效的主服务器的其他从服务器改为复制新的主服务器。

哨兵选择主服务器的策略:

(1)根据配置文件中 replica-priority 100 的优先级,值越小优先级越高,其实优先级初始都一样

(2)如果 replica-priority 一样,判断 slave 节点的 offset 值,越大说明数据和master 相差的最小,优先级越高

(3)每个 redis 实例启动后都会随机分配 40 位 runid ,选择最小的。

配置哨兵步骤:

节点IPPORT
s1192.168.150.10127001
s2192.168.150.10127002
s3192.168.150.10127003

(1)在 /myredis 目录下创建三个 sentinel 实例

# 创建目录
mkdir s1 s2 s3

(2)在 s1,s2,s3 分别创建三个 sentinel.conf 配置文件

每个配置文件中的 port、dir 是不一样的

port 27001
sentinel announce-ip 192.168.200.130
sentinel monitor mymaster 192.168.200.130 7001 2
# 指定了 Sentinel 认为服务器已经断线所需的毫秒数
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
dir "/myredis/s1"
  • port 27001:是当前sentinel实例的端口
  • sentinel monitor mymaster 192.168.150.101 7001 2:指定主节点信息
    • mymaster:主节点名称,自定义,任意写
    • 192.168.150.101 7001:主节点的ip和端口
    • 2:选举master时的quorum值

(3)通过配置文件启动哨兵

# 第1个
redis-sentinel s1/sentinel.conf
# 第2个
redis-sentinel s2/sentinel.conf
# 第3个
redis-sentinel s3/sentinel.conf

(4)当主节点挂掉之后,观察每个哨兵的日志

以 7002 为例:

1650962943754

7、使用 RedisTemplate 访问哨兵

(1)引入依赖

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

(2)配置文件中配置哨兵

nodes : sentinel 实例的 ip 地址、端口号

spring:
  redis:
    sentinel:
      master: mymaster
      nodes:
        - 192.168.200.132:27001
        - 192.168.200.132:27002
        - 192.168.200.132:27003

(3)配置主从读写分离

    /**
     * 配置主从读写分离
     * @return
     */
    @Bean
    public LettuceClientConfigurationBuilderCustomizer clientConfigurationBuilderCustomizer(){
        return  new LettuceClientConfigurationBuilderCustomizer() {
            @Override
            public void customize(LettuceClientConfiguration.LettuceClientConfigurationBuilder clientConfigurationBuilder) {
                clientConfigurationBuilder.readFrom(ReadFrom.REPLICA_PREFERRED);
            }
        };
    }

这里的ReadFrom是配置Redis的读取策略,是一个枚举,包括下面选择:
MASTER:从主节点读取
MASTER_PREFERRED:优先从master节点读取,master不可用才读取replica
REPLICA:从slave(replica)节点读取
**REPLICA _PREFERRED:**优先从slave(replica)节点读取,所有的slave都不可用才读取master

十二、集群

1、简介

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

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

2、搭建集群环境

创建 6 个 redis 实例:

IPPORT角色
192.168.150.1017001master
192.168.150.1017002master
192.168.150.1017003master
192.168.150.1018001slave
192.168.150.1018002slave
192.168.150.1018003slave

(1)删除之前操作的目录,避免产生干扰

rm -rf 7001 7002 7003 

(2) 重新创建新的目录

mkdir 7001 7002 7003 8001 8002 8003

(3)在 /myredis 目录下创建新的 redis.conf 配置文件

port 6379
# 开启集群功能
cluster-enabled yes
# 集群的配置文件名称,不需要我们创建,由redis自己维护
cluster-config-file /myredis/6379/nodes.conf
# 节点心跳失败的超时时间
cluster-node-timeout 5000
# 持久化文件存放目录
dir /myredis/6379
# 绑定地址
bind 0.0.0.0
# 让redis后台运行
daemonize yes
# 注册的实例ip
replica-announce-ip 192.168.200.132
# 保护模式
protected-mode no
# 数据库数量
databases 1
# 日志
logfile /myredis/6379/run.log

(4)将 redis.conf 配置文件拷贝到 6 个目录中

echo 7001 7002 7003 8001 8002 8003 | xargs -t -n 1 cp redis.conf

(5)修改redis.cong 中的 端口号、node 文件、dir、logfile

# 修改配置文件
printf '%s\n' 7001 7002 7003 8001 8002 8003 | xargs -I{} -t sed -i 's/6379/{}/g' {}/redis.conf

(6)启动所有的 redis 服务

printf '%s\n' 7001 7002 7003 8001 8002 8003 | xargs -I{} -t redis-server {}/redis.conf

关闭 所有的 redis 服务:

printf '%s\n' 7001 7002 7003 8001 8002 8003 | xargs -I{} -t redis-cli -p {} shutdown

(7)创建集群

redis5.0 之前是需要 安装 ruby 工具,但是5.0之后,已经集成好了,所以无需我们自己安装了。

注意:IP地址不能使用 127.0.0.1 本机地址,使用真实的IP地址

# 1 表示为每个主节点【主服务器】分配 1 个从节点【从服务器】
redis-cli --cluster create --cluster-replicas 1 192.168.200.132:7001 192.168.200.132:7002 192.168.200.132:7003 192.168.200.132:8001 192.168.200.132:8002 192.168.200.132:8003

M:master S:slave ,没问题就输入 yes

1650696204909

[OK] All 16384 slots covered.

这表示集群中的 16384 个槽都有至少一个主节点在处理, 集群运作正常。

(8)连接到命令端

# 增加一个 -c 选项表示使用集群方式连接
redis-cli -c -p 6379

进入到命令端 使用 cluster nodes 查看节点信息

插槽是什么?

Redis会把每一个master节点映射到0~16383共16384个插槽(hash slot)上,查看集群信息时就能看到:

1650697324096

在 6379 这服务器中增加数据,根据 key 自动算出了应该在 12706 这个插槽中,而12706 属于 6381 这台服务器,他会自动帮我们切换到 6381 这台服务器中。

1650697478418

  • 数据key不是与节点绑定,而是与插槽绑定。redis会根据key的有效部分计算插槽值,分两种情况:
    • key中包含"{}",且“{}”中至少包含1个字符,“{}”中的部分是有效部分
    • key中不包含“{}”,整个key都是有效部分

总结:

  • Redis如何判断某个key应该在哪个实例?
    • 将16384个插槽分配到不同的 master 主节点上
    • 根据key的有效部分计算哈希值,对16384取余
    • 余数作为插槽,寻找插槽所在节点即可

3、集群中的基本命令

向集群中插入多个值:

1650697760639

使用组的方式,进行插入 :

# {user}  表示 user组
mset name{user} rose age{user} 12

查询某个键值在哪个插槽上:

CLUSTER KEYSLOT key

统计某个插槽上的 key 的数量:

CLUSTER COUNTKEYSINSLOT slot

只能统计属于自己插槽范围内的插槽!!

取出某个插槽中的 key :

CLUSTER GETKEYSINSLOT <slot> <count>

1650698252214

4、节点的故障恢复

(1)如果一个主节点 挂掉之后,他的从节点会顶替他成为主节点,当主节点重新连接服务器时,会变成新的主节点的从节点

(2)如果集群中的主从都挂掉了,redis 会如何处理,主要是根据以下这个配置:

cluster-require-full-coverage

如果这个配置为yes ,那么当主从都挂掉后,集群就会停止工作。默认就是 yes

如果这个配置为 no ,那么该主服务器分配的插槽是不能用的、

5、使用 RedisTemplate 访问 集群

(1)增加 redis 依赖

(2)读写分离

(3)和访问哨兵唯一的区别就是,配置不同

      # 通过 RedisTemplate 访问集群
    cluster:
      nodes:
        - 192.168.200.132:7001
        - 192.168.200.132:7002
        - 192.168.200.132:7003
        - 192.168.200.132:8001
        - 192.168.200.132:8002
        - 192.168.200.132:8003

十三、缓存穿透

05-缓存穿透

解决方法:

(1) **对空值缓存:**如果一个查询返回的数据为空(不管是数据是否不存在),我们仍然把这个空结果(null)进行缓存,设置空结果的过期时间会很短,最长不超过五分钟

(2) 设置可访问的名单(白名单):

使用bitmaps类型定义一个可以访问的名单,名单id作为bitmaps的偏移量,每次访问和bitmap里面的id进行比较,如果访问id不在bitmaps里面,进行拦截,不允许访问。

(3) 采用布隆过滤器:(布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数)。

布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。)

将所有可能存在的数据哈希到一个足够大的bitmaps中,一个一定不存在的数据会被 这个bitmaps拦截掉,从而避免了对底层存储系统的查询压力。

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

十四、缓存击穿

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

06-缓存击穿

问题解决:

**(1)预先设置热门数据:**在redis高峰访问之前,把一些热门数据提前存入到redis里面,加大这些热门数据key的时长

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

(3)使用锁:

(1) 就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db。

(2) 先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX)去set一个mutex key

(3) 当操作返回成功时,再进行load db的操作,并回设缓存,最后删除mutex key;

(4) 当操作返回失败,证明有线程在load db,当前线程睡眠一段时间再重试整个get缓存的方法。

十五、缓存雪崩

和缓存击穿不同的是,缓**存击穿是指并发查同一条数据,缓存雪崩是不同数据都过期了,**很多数据都从数据库查。 -缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。

07-缓存雪崩

解决方法:

(1) **构建多级缓存架构:**nginx缓存 + redis缓存 +其他缓存(ehcache等)

(2) 使用锁或队列**:**

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

(3) 设置过期标志更新缓存:

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

(4) 将缓存失效时间分散开:

比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

十六、分布式锁

1、简介

(1)什么是分布式锁 ?

其实在JAVA API 中提供了很多锁的方式,但是对于分布式系统来说,这就没有用了,因此出现了分布式锁。为了控制 分布式系统之间同步访问共享资源的一种方式 。

(2)如何使用分布式锁 ?

目前主流的分布式锁实现方案有三种:1、基于数据库方式 2、基于缓存(Redis)3、基于 Zookeeper

接下来用的是 Redis 实现。

(3)分布式锁的特点 ?

  1. 互斥性
    1. 在任意时刻,只能有一个客户端 持有锁,如果多个客户端都有锁,那也就是失去了锁的意义。
  2. 不能发生死锁
    1. 即便持有锁的客户端宕机,也不能影响对其他客户端上锁。
  3. 容错
    1. 当某一部分节点出现异常,也能保证锁的正常运行
  4. 解铃还须系铃人
    1. 锁只能被该锁的客户端删除,不能由其他的客户删除。

2、设置分布式锁

1650713797525

分布式锁的整个实现流程:

(1)如果获取到锁,就执行响应的业务流程

(2)如果没有获取到,等待重试获取锁。

(3)如果获取到锁,在执行业务时意外宕机,还得需要自动释放锁。不能耽误后面的客户端获取锁。

在 Redis 中如何设置锁:

(1) 第一种方式

通过一个互斥的命令 设置锁: setnx

设置锁的过期时间: expire key

不建议这种方式,因为使用这种方式后序还需手动设置锁的过期时间,如果在这个期间发生异常就芭比Q了,直接导致锁无法释放

(2) 第二种方式【推荐】

自 2.6.1 版本之后在 进行 set 时就可以设置锁和 过期时间:

set key value nx ex 10

在 JAVA 中如何实现分布式锁:

    void test03() {
        // 为 lock 上锁 timeout: key 过期时间
        //  TimeUnit.SECONDS 时间单位为 s
        Boolean ifAbsent = redisTemplate.opsForValue()
                .setIfAbsent("lock", "value", 20, TimeUnit.SECONDS);

        // 返回值是一个 Boolean包装类,有可能为null
        if (Boolean.TRUE.equals(ifAbsent)) {
            // 如果获取到锁,执行响应的业务流程
            System.out.println("业务流程");

            // 最终执行完业务流程,需要释放锁
            System.out.println("业务流程执行完,释放锁.....");
            redisTemplate.delete("lock");
        }
    }

3、释放锁

(1)普通方式释放【不推荐】

调用 RedisTemplate 中的 delete API 进行删除

(2)使用 UUID 防止误删操作

普通方式的释放锁,还会有一些弊端.

1650720053173

因此我们需要在 线程 业务完成想要释放锁时,需要做一个判断,判断是否释放的是本线程的锁。

1、当每个线程上锁的时候,给 value 一个随机值

2、在删除本线程的锁的时候,判断 随机值 是否和当前线程一致

    // 分布式锁
    @Test
    void test03() {
        // 使用 uuid 作为锁的标识
        String uuid = UUID.randomUUID().toString();
        // 为 lock 上锁 timeout: key 过期时间
        //  TimeUnit.SECONDS 时间单位为 s
        Boolean ifAbsent = redisTemplate.opsForValue()
                .setIfAbsent("lock", uuid, 20, TimeUnit.SECONDS);

        // 获取 锁 中的标识
        String lockValue= (String) redisTemplate.opsForValue().get("lock");
        // 判断是否自己的锁
        if (uuid.equals(lockValue)) {
            // 如果获取到锁,执行响应的业务流程
            System.out.println("业务流程");

            try {
                //睡 10s
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // 最终执行完业务流程,需要释放锁
            System.out.println("业务流程执行完,释放锁.....");
            redisTemplate.delete("lock");
        }
    }

(3)使用LUA 脚本释放锁

使用 uuid 还不是最完美的释放锁,这是因为 上锁 和 释放锁是俩个动作,这俩个动作之间还会有一些问题。

所以我们需要使用 LUA 脚本来保证该流程的原子性操作。即共同成功,共同失败!

LUA 脚本:

一种轻量级的脚本语言,

通过 redis.call 执行脚本

不带参数类型的脚本:

1650940794384

带参数类型的脚本:

下标是从 1 开始的!!!

1650940955799

使用 LUA 脚本释放锁:

流程 :

1、获取锁的标识 ,也就是 value。 但是这个 value 是动态的,因此需要使用参数

2、判断取出来的标识是否与当前的标识相等,如果相等允许释放锁

3、否则不释放

-- redis.call("get",KEYS[1]) 取出标识
--  ARGV[1] 当前标识
-- redis.call("del",KEYS[1]) 释放锁

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

SpringBoot 使用 LUA 脚本释放锁:

RedisTemplate 中的 execute 方法 是 SpringBoot 中调用 LUA 脚本的 API

1650942676304

在 SpringBoot 中使用 LUA 脚本,建议写在 配置文件中,方便维护。

1650943123942

(1)execute 中的 第一个 参数是 RedisScript 类型的,可以使用它的默认实现类 DefaultRedisScript 来加载配置文件。

1650943303457

(2)加载配置文件这个动作只需要在类加载时执行一次,所以我们可以写到 static 代码块中,减少 IO 操作。

   private static final DefaultRedisScript<Long> REDIS_SCRIPT ;
    
    static {
        REDIS_SCRIPT = new DefaultRedisScript<>();
        // 加载本地 Resource 配置文件。默认从类路径下开始
        REDIS_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        // 设置返回值类型
        REDIS_SCRIPT.setResultType(Long.class);
    }

(3)提供释放锁的方法

    /**
     * 释放锁的方法【LUA方式】
     */
    public  void unlock(String keys, String arg){
        // LUA 脚本,key,value
        redisTemplate.execute(REDIS_SCRIPT, Collections.singletonList(keys),arg);
    }

第二种方式:

提供 lock.lua 配置文件

Redis 配置类中声明 lua 脚本:

    /**
     * LUA 脚本
     * @return
     */
    @Bean
    public DefaultRedisScript<Boolean> script(){
        DefaultRedisScript<Boolean> script = new DefaultRedisScript<>();
        // 设置 LUA 脚本的路径
        script.setLocation(new ClassPathResource("lock.LUA"));
        // 设置返回值类型
        script.setResultType(Boolean.class);
        return script ;
    }

注入 RedisScript

    @Autowired
    private RedisScript<Boolean> script;

使用 RedisTemplate 的execute 方法执行脚本

十七、redis 工具类

package com.example.springboot_redis.utils;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

//在真实开发中,经常使用
@Component
public final class RedisUtil {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    // =============================common============================

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

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

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

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

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

    /**
     * 普通缓存放入
     *
     * @param key   键
     * @param value 值
     * @return true成功 false失败
     */
    public boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 普通缓存放入并设置时间
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */
    public boolean set(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time,
                        TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    /**
     * 移除值为value的
     *
     * @param key    键
     * @param values 值 可以是多个
     * @return 移除的个数
     */
    public long setRemove(String key, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().remove(key, values);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
// ===============================list=================================

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

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

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

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

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

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

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

    /**
     * 根据索引修改list中的某条数据
     *
     * @param key   键
     * @param index 索引
     * @param value 值
     * @return
     */
    public boolean lUpdateIndex(String key, long index, Object value) {
        try {
            redisTemplate.opsForList().set(key, index, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 移除N个值为value
     *
     * @param key 键
     * @param count 移除多少个
     * @param value 值
     * @return 移除的个数
     */
    public long lRemove (String key,long count, Object value){
        try {
            Long remove = redisTemplate.opsForList().remove(key, count,value);
            return remove;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
}

    }
}

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

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

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

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

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

/**
 * 根据索引修改list中的某条数据
 *
 * @param key   键
 * @param index 索引
 * @param value 值
 * @return
 */
public boolean lUpdateIndex(String key, long index, Object value) {
    try {
        redisTemplate.opsForList().set(key, index, value);
        return true;
    } catch (Exception e) {
        e.printStackTrace();
        return false;
    }
}


/**
 * 移除N个值为value
 *
 * @param key 键
 * @param count 移除多少个
 * @param value 值
 * @return 移除的个数
 */
public long lRemove (String key,long count, Object value){
    try {
        Long remove = redisTemplate.opsForList().remove(key, count,value);
        return remove;
    } catch (Exception e) {
        e.printStackTrace();
        return 0;
    }
}

}


### Redis 学习笔记概述 Redis 是一种高性能的键值存储系统,支持多种数据结构并提供丰富的功能。为了全面掌握 Redis使用方法和技术细节,一份详尽的学习笔记应当覆盖以下几个方面: #### 一、基础概念介绍 - **定义与特性** - Redis 是一个开源的内存数据结构存储库,可以用作数据库、缓存和消息中间件[^1]。 - **应用场景** - 高效的数据读写操作使其适用于高速缓存场景;持久化的选项也允许作为可靠的主数据库。 #### 二、环境搭建指南 - **安装过程** - 安装完成后可以通过 `redis-server` 命令启动服务,默认情况下这会在前台运行并且占用当前终端会话[^2]。 - **后台模式配置** - 推荐通过编辑 `/usr/local/src/redis-6.2.6/redis.conf` 文件中的设置项使 Redis 在后台稳定工作。 #### 三、核心功能解析 - **基本命令集** - 包括字符串(Strings)、哈希(Hashes)、列表(Lists)等常见数据类型的增删改查指令。 - **高级特性应用** - 发布订阅(Pub/Sub),事务处理(Transaction),Lua脚本执行等功能的应用实例。 #### 四、集成开发实践 - **Spring Boot 整合案例** - 使用 Spring Data Redis 提供的 `RedisTemplate` 对象简化 Java 应用程序同 Redis 数据源之间的交互逻辑[^3]。 #### 五、优化策略探讨 - **性能调优技巧** - 考虑到 CPU 并非主要瓶颈因素而是受制于物理 RAM 和网卡吞吐能力的影响,合理规划硬件资源分配对于提升整体效率至关重要。 - **预加载机制说明** - 当应用程序首次上线前预先填充部分热点数据至 Redis 中可以有效缓解高峰期的压力,提高响应速度[^4]。 #### 六、可靠性保障措施 - **持久化方案对比** - RDB 快照方式能够在指定时间间隔内保存数据副本,确保意外断电等情况下的恢复可能性。 ```bash # 启动Redis服务器(建议采用守护进程方式) $ redis-server /path/to/redis.conf --daemonize yes ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

鲨瓜2号

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值