redis学习记录

redis学习记录

前言

很早之前的学习笔记,重新做整理并以视频的形式对redis重新进行学习

一、笔记整理

1.什么是redis?

Redis是用C语言开发的一个开源的高性能基于内存运行的键值对NoSQL数据库
在这里插入图片描述
在这里插入图片描述

2.redis和比较其他Nosql的特点

1.redis支持数据的持久化,可以将内存中的数据保存到磁盘中,重启时会再次加载使用。

2.redis不仅仅支持k-v类型数据,还提供list、set、hash、string、zset等结构数据储存

3.性能高,读写速度快,数据存在内存中类似于hashmap(链表+红黑树)。

4.支持事务,操作都是原子性。同时成功或失败。

5.特性丰富,可用于缓存消息,按key设置时间过期自动删除。

3.redis持久化机制

redis提供了两种方法进行持久化,一种是RDB持久化,是默认开启的;另一种是AOF持久化。

总结:AOF性能消耗高,丢失数据少;
RDB性能消耗低,丢失数据多,速度快
一般同时开启,优先AOF

1.RDB

RDB保存二进制数据
原理:redis会根据进程创建一个一模一样的子进程进行持久化,将数据写入到一个临时文件中,持久化结束,用这个临时文件替换上次持久化好的文件。在整个过程中主进程不进行任何io操作,确保了极高性能。
注意:RDB持久化数据可以指定时长,假设当设定为60s时,当服务器在59s宕机,则该时间会丢失数据,而AOF丢失数据不会超过2s。

2.AOF

AOF保存命令字浮串(保存协议)
默认配置1s执行一次
原理:将redis的操作日志以追加的方式写入文件。
默认配置

// 超过原大小的100%
auto-aof-reewrite-percentage 100
// AOF文件达到64mb时会重写
auto-aof-rewrite-min-size 64mb

4.redis集群

哨兵:建立在主从集群之上(是升级),主机挂掉之后,哨兵节点会立刻在从主机中选出一个主节点,换主机。
cluster:由多个哨兵模式组成(但是没有哨兵节点),主机挂掉直接选出一个作为主节点。主机挂掉可用率高。

5.redis缓存的四大问题

redis缓存:数据查询先查询缓存,如果缓存中没有,就从数据库中查询,然后再保存到缓存中,然后在返回数据。下次查询时,就会在缓存中直接查到数据,不会再查询数据库。

缓存穿透:

问题描述:缓存中没有,数据库中也没有,但是每次都需要查询数据库。
解决
1.缓存空对象

当缓存中没有数据时,数据库也没数据时,就在缓存中设置一个v=null的对象放进缓存。

2.redis来实现布隆过滤器

链接: 布隆过滤器究竟是什么.
在这里插入图片描述
布隆过滤器执行步骤:第一步是将数据库所有的数据加载到布隆过滤器。第二步当有请求来的时候先去布隆过滤器查询,如果bf说没有,第三步直接返回。如果bf说有,在往下走之前的流程。
布隆过滤器执行原理:当k进入布隆过滤器后就会执行3个(或多个)hash函数,将执行的结果到bit[]进行比对,如果函数指向都是1时,表示可能存在(hash碰撞)。

从容器的角度来说:
如果布隆过滤器判断元素在集合中存在,不一定存在
如果布隆过滤器判断不存在,一定不存在

从元素的角度来说:
如果元素实际存在,布隆过滤器一定判断存在
如果元素实际不存在,布隆过滤器可能判断存在

布隆过滤器容错率低,则数组大或者哈希函数多
容错率越低,开销越大。

缓存击穿:

问题描述:缓存中没有,数据库中有(是并发问题)

在缓存中没有的情况下,多个线程同时调用,则会多次向数据库做查询操作,并没有起到缓存的效果,数据库也容易宕机。

解决

数据查询先查询缓存,如果缓存中没有,则加锁,再查询一遍缓存,再查询数据库,把数据加入缓存,返回数据,解锁。(只查询一次数据库)。

缓存雪崩:

问题描述:机器宕机或大量数据同时失效
解决

1.使用高可用集群,cluster集群。
2.缓存过期时间错隔开。

缓存与数据库数据不一致:

问题描述:1.更改数据时,若先更改数据库,再更改缓存时,机器挂掉,导致数据库中为新数据,缓存中为旧数据,先缓存同理,数据不一致。2.两个线程同时更改一个数据,若A先B后,由于网络问题,B更新了缓存,但是数据库被A覆盖,也导致数据库与缓存数据不一致。
解决

在修改数据库时候先删除缓存再做数据库修改,则数据一致。

引发问题:当删除缓存后,线程A去库中更新数据,在这个过程中,B去读取数据,发现缓存中没有,就从把数据库中数据(未更新)引入缓存。然后A更新完毕后,数据不一致。
解决

更新数据时,先根据查询的标识(id)进行hash取值,并放进库存服务的队列中(一个队列表示一个工作线程),然后再进行数据更新,更新时先删除缓存,此时,B线程查询数据(根据查询标识也放进队列中),队列中线程是单线程去执行,则会保证A先执行完,B再去查询,则会数据一致。

二、新学习记录

1.nosql数据库简介

1.1 技术发展

技术分类:
1、解决功能性的问题:Java、Jsp、RDBMS、Tomcat、HTML、Linux、JDBC、SVN
2、解决扩展性的问题:Struts、Spring、SpringMVC、Hibernate、Mybatis
3、解决性能的问题:NoSQL、Java 线程、Hadoop、Nginx、MQ、ElasticSearch

1.2 nosql数据库

适用场景:
⚫ 对数据高并发的读写
⚫ 海量数据的读写
⚫ 对数据高可扩展性的

不适用场景:
⚫ 需要事务支持
⚫ 基于 sql 的结构化查询存储,处理复杂的关系,需要即席查询。
⚫ (用不着 sql 的和用了 sql 也不行的情况,请考虑用 NoSql)

应用场景:
在这里插入图片描述

2.redis安装

安装过程见具体文档

3.redis常用五大数据类型(指令)

3.1 Redis的key

keys *查看当前库所有 key (匹配:keys *1)
exists key 判断某个 key 是否存在
type key 查看你的 key 是什么类型
del key 删除指定的 key 数据
unlink key 根据 value 选择非阻塞删除
仅将 keys 从 keyspace 元数据中删除,真正的删除会在后续异步操作。
expire key 10 10 秒钟:为给定的 key 设置过期时间
ttl key 查看还有多少秒过期,-1 表示永不过期,-2 表示已过期
select 命令切换数据库
dbsize 查看当前数据库的 key 的数量
flushdb 清空当前库
flushall 通杀全部库

3.2 Redis字符串(String)

string类型是redis中最基本的数据类型,一个redis中字符串的value最多可以是512M。string是== 二进制安全的==。意味着redis的string可以包含所有数据,比如jpg图片或者序列化对象。

3.2.1 常用命令
set <key><value>添加键值对

在这里插入图片描述

*NX:当数据库中 key 不存在时,可以将 key-value 添加数据库
*XX:当数据库中 key 存在时,可以将 key-value 添加数据库,与 NX 参数互斥
*EX:key 的超时秒数
*PX:key 的超时毫秒数,与 EX 互斥
get <key>查询对应键值
append <key><value>将给定的<value> 追加到原值的末尾
strlen <key>获得值的长度
setnx <key><value>只有在 key 不存在时 设置 key 的值
incr <key>
将 key 中储存的数字值增 1
只能对数字值操作,如果为空,新增值为 1
decr <key>
将 key 中储存的数字值减 1
只能对数字值操作,如果为空,新增值为-1
incrby / decrby <key><步长>将 key 中储存的数字值增减。自定义步长。

在这里插入图片描述
这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另
一个线程)。
(1)在单线程中, 能够在单条指令中完成的操作都可以认为是"原子操作",因为中
断只能发生于指令之间。
(2)在多线程中,不能被其它进程(线程)打断的操作就叫原子操作。
Redis 单命令的原子性主要得益于 Redis 的单线程。

mset <key1><value1><key2><value2> ..... 
同时设置一个或多个 key-value 对 
mget <key1><key2><key3> .....
同时获取一个或多个 value 
msetnx <key1><value1><key2><value2> ..... 
同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在。

原子性,有一个失败则都失败

getrange <key><起始位置><结束位置>
获得值的范围,类似 java 中的 substring,前包,后包
setrange <key><起始位置><value><value> 覆写<key>所储存的字符串值,从<起始位置>开始(索引从 0 开始)。
setex <key><过期时间><value>
设置键值的同时,设置过期时间,单位秒。
getset <key><value>
以新换旧,设置了新值同时获得旧值。
3.2.2 数据结构

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

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

3.3 Redis列表(list)

list是一键多值

Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头
部(左边)或者尾部(右边)。
它的底层实际是个双向链表,对两端的操作性能很高,通过索引下标的操作中间的节
点性能会较差。
在这里插入图片描述

3.3.1 常用命令
lpush/rpush <key><value1><value2><value3> .... 从左边/右边插入一个或多个值。
lpop/rpop <key>从左边/右边吐出一个值。值在键在,值光键亡。
rpoplpush <key1><key2><key1>列表右边吐出一个值,插到<key2>列表左边。
lrange <key><start><stop>
按照索引下标获得元素(从左到右)
lrange mylist 0 -1 0 左边第一个,-1 右边第一个,(0-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。

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

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

3.4 Redis集合(set)

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

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

一个算法,随着数据的增加,执行时间的长短,如果是 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.5Redis哈希(Hash)

Redis hash 是一个键值对集合。
Redis hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象。
类似 Java 里面的 Map<String,Object>(redis的hash的key对应类似map,map有包含多个键值对)
用户 ID 为查找的 key,存储的 value 用户对象包含姓名,年龄,生日等信息,如果用
普通的 key/value 结构来存储
在这里插入图片描述
通过== key(用户 ID) + field(属性标签) ==就可以操作对应属性数据了,既不需要重复存储数
据,也不会带来序列化和并发修改控制的问题

3.5.1 常用命令
hset <key><field><value><key>集合中的 <field>键赋值<value>
hget <key1><field><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.5.2 数据结构

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

3.6 Redis有序集合Zset(sorted set)

Redis 有序集合 zset 与普通集合 set 非常相似,是一个没有重复元素的字符串集合。

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

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

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

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 之间(包括等于 min 或 max )的成员。
有序集成员按 score 值递增(从小到大)次序排列。
zrevrangebyscore key maxmin [withscores] [limit offset count] 
同上,改为从大到小排列。
zincrby <key><increment><value> 为元素的 score 加上增量
zrem <key><value>删除该集合下,指定值的元素
zcount <key><min><max>统计该集合,分数区间内的元素个数
zrank <key><value>返回该值在集合中的排名,从 0 开始。

案例:如何利用 zset 实现一个文章访问量的排行榜?
在这里插入图片描述

3.6.2 数据结构

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

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

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

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

3.6.3 跳跃表

1、简介
有序集合在生活中比较常见,例如根据成绩对学生排名,根据得分对玩家排名
等。对于有序集合的底层实现,可以用数组、平衡树、链表等。数组不便元素的插入、
删除;平衡树或红黑树虽然效率高但结构复杂;链表查询需要遍历所有效率低。Redis
采用的是跳跃表。跳跃表效率堪比红黑树,实现远比红黑树简单。
2、实例
对比有序链表和跳跃表,从链表中查询出 51
(1) 有序链表
在这里插入图片描述
要查找值为 51 的元素,需要从第一个元素开始依次查找、比较才能找到。共需要 6 次比较。
(2) 跳跃表
在这里插入图片描述

从第 2 层开始,1 节点比 51 节点小,向后比较。
21 节点比 51 节点小,继续向后比较,后面就是 NULL 了,所以从 21 节点向下到第 1 层
在第 1 层,41 节点比 51 节点小,继续向后,61 节点比 51 节点大,所以从 41 向下
在第 0 层,51 节点为要查找的节点,节点被找到,共查找 4 次。
从此可以看出跳跃表比有序链表效率要高

4.Redis配置文件

自定义目录:/myredis/redis.conf

4.1 ###Units 单位###

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

4.2 ###INCLUDES 包含###

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

4.3 ###网络相关配置

4.3.1 bind

默认情况 bind=127.0.0.1 只能接受本机的访问请求
不写的情况下,无限制接受任何 ip 地址的访问
生产环境肯定要写你应用服务器的地址;服务器是需要远程访问的,所以需要将其注
释掉
如果开启了 protected-mode,那么在没有设定 bind ip 且没有设密码的情况下,Redis
只允许接受本机的响应

修改后保存配置,停止服务,重启启动查看进程,不再是本机访问了。

4.3.2 protected-mode

将本机访问保护模式设置 no

4.3.3 port

端口号,默认6379

4.3.4 tcp-backlog

设置 tcp 的 backlog,backlog 其实是一个连接队列,backlog 队列总和=未完成三次握手
队列 + 已经完成三次握手队列。
在高并发环境下你需要一个高 backlog 值来避免慢客户端连接问题。
注意 Linux 内核会将这个值减小到/proc/sys/net/core/somaxconn 的值(128),所以需要
确认增大/proc/sys/net/core/somaxconn 和/proc/sys/net/ipv4/tcp_max_syn_backlog(128)
两个值来达到想要的效果

4.3.5 timeout

一个空闲的客户端维持多少秒会关闭,0 表示关闭该功能。即永不关闭

4.3.6 tcp-keepalive

对访问客户端的一种心跳检测,每个 n 秒检测一次。
单位为秒,如果设置为 0,则不会进行 Keepalive 检测,建议设置成 60

4.4 ###GENERAL 通用###

4.4.1 daemonize

是否为后台进程,设置为 yes
守护进程,后台启动

4.4.2 pidfile

存放 pid 文件的位置,每个实例会产生一个不同的 pid 文件

4.4.3 loglevel

指定日志记录级别,Redis 总共支持四个级别:debug、verbose、notice、warning,默
认为 notice
四个级别根据使用阶段来选择,生产环境选择 notice 或者 warning

4.4.4 logfile

日志文件名称

4.4.5 databases 16

设定库的数量 默认 16,默认数据库为 0,可以使用 SELECT 命令在连接上指定
数据库 id

4.5 ###SECURITY 安全###

4.5.1 设置密码

访问密码的查看、设置和取消
在命令中设置密码,只是临时的。重启 redis 服务器,密码就还原了。
永久设置,需要再配置文件中进行设置。

4.6 #### LIMITS 限制

4.6.1 maxclients

➢设置 redis 同时可以与多少个客户端进行连接。
➢ 默认情况下为 10000 个客户端。
➢ 如果达到了此限制,redis 则会拒绝新的连接请求,并且向这些连接请求方发出
“max number of clients reached”以作回应。

4.6.2 maxmemory

➢ 建议必须设置,否则,将内存占满,造成服务器宕机
➢ 设置 redis 可以使用的内存量。一旦到达内存使用上限,redis 将会试图移除内部数
据,移除规则可以通过 maxmemory-policy 来指定。
➢ 如果 redis 无法根据移除规则来移除内存中的数据,或者设置了“不允许移除”,
那么 redis 则会针对那些需要申请内存的指令返回错误信息,比如 SET、LPUSH 等。
➢ 但是对于无内存申请的指令,仍然会正常响应,比如 GET 等。如果你的 redis 是主
redis(说明你的 redis 有从 redis),那么在设置内存使用上限时,需要在系统中留
出一些内存空间给同步队列缓存,只有在你设置的是“不移除”的情况下,才不
用考虑这个因素。

4.6.3 maxmemory-policy

➢ volatile-lru:使用 LRU 算法移除 key,只对设置了过期时间的键;(最近最少使用)
➢ allkeys-lru:在所有集合 key 中,使用 LRU 算法移除 key
➢ volatile-random:在过期集合中移除随机的 key,只对设置了过期时间的键
➢ allkeys-random:在所有集合 key 中,移除随机的 key
➢ volatile-ttl:移除那些 TTL 值最小的 key,即那些最近要过期的 key
➢ noeviction:不进行移除。针对写操作,只是返回错误信息

4.6.4 maxmemory-samples

➢ 设置样本数量,LRU 算法和最小 TTL 算法都并非是精确的算法,而是估算值,所
以你可以设置样本的大小,redis 默认会检查这么多个 key 并选择其中 LRU 的那个。
➢ 一般设置 3 到 7 的数字,数值越小样本越不准确,但性能消耗越小。

5. Redis的发布和订阅

5.1 什么是发布和订阅

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

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

5.2 Redis的发布和订阅

1、客户端可以订阅频道如下图
在这里插入图片描述

2、当给这个频道发布消息后,消息就会发送给订阅的客户端
在这里插入图片描述

5.3 发布订阅命令行实现

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

SUBSCRIBE channel1

在这里插入图片描述

2、打开另一个客户端,给 channel1 发布消息 hello

publish channel1 hello

在这里插入图片描述
返回的 1 是订阅者数量

3、打开第一个客户端可以看到发送的消息
在这里插入图片描述

注:发布的消息没有持久化,如果在订阅的客户端收不到 hello,只能收到订阅后发布的消息

6.Redis新数据类型

6.1 Bitmaps

Bitmaps 这个“数据类型”可以实现对位的操作:

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

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

做大量活跃用户存储时可以节省内存。活跃用户较少时不合适使用(不如set);

6.2. HyperLogLog

当遇到与统计相关的功能需求,比如统计网站 PV(PageView 页面访问量),可以使用 Redis 的 incr、incrby 轻松实现。
但像 UV(UniqueVisitor,独立访客)、独立 IP 数、搜索记录数等需要去重和计数的问题如何解决?这种求集合中不重复元素个数的问题称为基数问题。

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

6.3. Geospatial

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

7.Redis_Jedis_测试

8.完成一个手机验证码功能

package com.atguigu.jedis;

import redis.clients.jedis.Jedis;

import java.util.Random;

public class PhoneCode {

    public static void main(String[] args) {
        //模拟验证码发送
        verifyCode("13678765435");

        //模拟验证码校验
        //getRedisCode("13678765435","4444");
    }

    //3 验证码校验
    public static void getRedisCode(String phone,String code) {
        //从redis获取验证码
        Jedis jedis = new Jedis("192.168.44.168",6379);
        //验证码key
        String codeKey = "VerifyCode"+phone+":code";
        String redisCode = jedis.get(codeKey);
        //判断
        if(redisCode.equals(code)) {
            System.out.println("成功");
        }else {
            System.out.println("失败");
        }
        jedis.close();
    }

    //2 每个手机每天只能发送三次,验证码放到redis中,设置过期时间120
    public static void verifyCode(String phone) {
        //连接redis
        Jedis jedis = new Jedis("192.168.44.168",6379);

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

        //每个手机每天只能发送三次
        String count = jedis.get(countKey);
        if(count == null) {
            //没有发送次数,第一次发送
            //设置发送次数是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();
        }

        //发送验证码放到redis里面
        String vcode = getCode();
        jedis.setex(codeKey,120,vcode);
        jedis.close();
    }

    //1 生成6位数字验证码
    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;
    }
}

9. Redis 与 Spring Boot 整合

9.1. 整合步骤

1.在 pom.xml 文件中引入 redis 相关依赖

<!-- 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>
<version>2.6.0</version>
</dependency>

2.application.properties 配置 redis 配置

#Redis 服务器地址
spring.redis.host=192.168.140.136
#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
  1. 添加 redis 配置类
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
 @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.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
 jackson2JsonRedisSerializer.setObjectMapper(om);
 template.setConnectionFactory(factory);
//key 序列化方式
 template.setKeySerializer(redisSerializer);
//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 秒
 RedisCacheConfiguration config = 
RedisCacheConfiguration.defaultCacheConfig()
 .entryTtl(Duration.ofSeconds(600))
 .serializeKeysWith(RedisSerializationContext.SerializationPair.
fromSerializer(redisSerializer))
 .serializeValuesWith(RedisSerializationContext.SerializationPai
r.fromSerializer(jackson2JsonRedisSerializer))
 .disableCachingNullValues();
 RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
 .cacheDefaults(config)
 .build();
 return cacheManager;
 }
}

4.测试一下

@RestController
@RequestMapping("/redisTest")
public class RedisTestController {
 @Autowired
 private RedisTemplate redisTemplate;
 @GetMapping
 public String testRedis() {
 //设置值到 redis
 redisTemplate.opsForValue().set("name","lucy");
 //从 redis 获取值
 String name = (String)redisTemplate.opsForValue().get("name");
 return name;
 }
}

10. Redis_事务_锁机制_秒杀

10.1 Redis 的事务定义

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

redis事物的主要作用就是串联多个命令防止别的命令插队。

Redis 是利用这种 check-and-set 机制实现事务的

10.2 Multi、Exec、discard

Multi:表示队列开始,输入的命令会依次进入队列。
Exec:Exec执行后,命令队列中的命令才会依次执行。
discard:组队中的命令放弃组队

在这里插入图片描述

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

组队的过程中可以通过 discard 来放弃组队。

在这里插入图片描述

10.3 事务中报错处理

事务中出现错误主要分为两种情况:
组队中某个命令出现报错:组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消。
在这里插入图片描述

执行阶段某个命令报错:如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。
在这里插入图片描述

10.4 事务冲突问题

为什么要做成事务?
例如场景:有很多人有你的账户,同时去参加双十一抢购

10.4.1 例子

账户10000元
一个请求想给金额减 8000
一个请求想给金额减 5000
一个请求想给金额减 1000

如果同时执行可能会造成的问题
在这里插入图片描述
这时候就需要通过锁来实现步骤执行

10.4.2 悲观锁

在这里插入图片描述
悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会 block 直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁表锁等,读锁写锁等,都是在做操作之前先上锁。

10.4.3 乐观锁

在这里插入图片描述
乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制乐观锁适用于多读的应用类型,这样可以提高吞吐量。

Redis 就是利用这种 check-and-set 机制实现事务的

10.4.4 WATCH key [key …]

在执行 multi 之前,先执行 watch key1 [key2],可以监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
在这里插入图片描述

10.4.5 unwatch

取消 WATCH 命令对所有 key 的监视。

如果在执行 WATCH 命令之后,EXEC 命令或 DISCARD 命令先被执行了的话,那么就不需要再执行 UNWATCH 了。

10.5 Redis事务的三特性

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

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

不保证原子性
◼ 事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚

11. Redis_事务_秒杀案例

连接池

package com.atguigu;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

public class JedisPoolUtil {
	private static volatile JedisPool jedisPool = null;

	private JedisPoolUtil() {
	}

	public static JedisPool getJedisPoolInstance() {
		if (null == jedisPool) {
			synchronized (JedisPoolUtil.class) {
				if (null == jedisPool) {
					JedisPoolConfig poolConfig = new JedisPoolConfig();
					poolConfig.setMaxTotal(200);
					poolConfig.setMaxIdle(32);
					poolConfig.setMaxWaitMillis(100*1000);
					poolConfig.setBlockWhenExhausted(true);
					poolConfig.setTestOnBorrow(true);  // ping  PONG
				 
					jedisPool = new JedisPool(poolConfig, "192.168.44.168", 6379, 60000 );
				}
			}
		}
		return jedisPool;
	}

	public static void release(JedisPool jedisPool, Jedis jedis) {
		if (null != jedis) {
			jedisPool.returnResource(jedis);
		}
	}

}

秒杀过程

package com.atguigu;

import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.slf4j.LoggerFactory;

import ch.qos.logback.core.rolling.helper.IntegerTokenConverter;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.ShardedJedisPool;
import redis.clients.jedis.Transaction;

/**
 *
 */
public class SecKill_redis {

	public static void main(String[] args) {
		Jedis jedis =new Jedis("192.168.44.168",6379);
		System.out.println(jedis.ping());
		jedis.close();
	}

	//秒杀过程
	public static boolean doSecKill(String uid,String prodid) throws IOException {
		//1 uid和prodid非空判断
		if(uid == null || prodid == null) {
			return false;
		}

		//2 连接redis
		//Jedis jedis = new Jedis("192.168.44.168",6379);
		//通过连接池得到jedis对象
		JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
		Jedis jedis = jedisPoolInstance.getResource();

		//3 拼接key
		// 3.1 库存key
		String kcKey = "sk:"+prodid+":qt";
		// 3.2 秒杀成功用户key
		String userKey = "sk:"+prodid+":user";

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

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

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

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

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

		//组队操作
		multi.decr(kcKey);
		multi.sadd(userKey,uid);

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

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

		//7.1 库存-1
		//jedis.decr(kcKey);
		//7.2 把秒杀成功用户添加清单里面
		//jedis.sadd(userKey,uid);

		System.out.println("秒杀成功了..");
		jedis.close();
		return true;
	}
}


lua脚本

package com.atguigu;

import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.slf4j.LoggerFactory;

import ch.qos.logback.core.joran.conditional.ElseAction;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.ShardedJedisPool;
import redis.clients.jedis.Transaction;

public class SecKill_redisByScript {
	
	private static final  org.slf4j.Logger logger =LoggerFactory.getLogger(SecKill_redisByScript.class) ;

	public static void main(String[] args) {
		JedisPool jedispool =  JedisPoolUtil.getJedisPoolInstance();
 
		Jedis jedis=jedispool.getResource();
		System.out.println(jedis.ping());
		
		Set<HostAndPort> set=new HashSet<HostAndPort>();

	//	doSecKill("201","sk:0101");
	}
	
	static String secKillScript ="local userid=KEYS[1];\r\n" + 
			"local prodid=KEYS[2];\r\n" + 
			"local qtkey='sk:'..prodid..\":qt\";\r\n" + 
			"local usersKey='sk:'..prodid..\":usr\";\r\n" + 
			"local userExists=redis.call(\"sismember\",usersKey,userid);\r\n" + 
			"if tonumber(userExists)==1 then \r\n" + 
			"   return 2;\r\n" + 
			"end\r\n" + 
			"local num= redis.call(\"get\" ,qtkey);\r\n" + 
			"if tonumber(num)<=0 then \r\n" + 
			"   return 0;\r\n" + 
			"else \r\n" + 
			"   redis.call(\"decr\",qtkey);\r\n" + 
			"   redis.call(\"sadd\",usersKey,userid);\r\n" + 
			"end\r\n" + 
			"return 1" ;
			 
	static String secKillScript2 = 
			"local userExists=redis.call(\"sismember\",\"{sk}:0101:usr\",userid);\r\n" +
			" return 1";

	public static boolean doSecKill(String uid,String prodid) throws IOException {

		JedisPool jedispool =  JedisPoolUtil.getJedisPoolInstance();
		Jedis jedis=jedispool.getResource();

		 //String sha1=  .secKillScript;
		String sha1=  jedis.scriptLoad(secKillScript);
		Object result= jedis.evalsha(sha1, 2, uid,prodid);

		  String reString=String.valueOf(result);
		if ("0".equals( reString )  ) {
			System.err.println("已抢空!!");
		}else if("1".equals( reString )  )  {
			System.out.println("抢购成功!!!!");
		}else if("2".equals( reString )  )  {
			System.err.println("该用户已抢过!!");
		}else{
			System.err.println("抢购异常!!");
		}
		jedis.close();
		return true;
	}
}

秒杀案例

package com.atguigu;

import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Random;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.ws.soap.AddressingFeature.Responses;

/**
 * 秒杀案例
 */
public class SecKillServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;

    public SecKillServlet() {
        super();
    }

	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

		String userid = new Random().nextInt(50000) +"" ;
		String prodid =request.getParameter("prodid");
		
		//boolean isSuccess=SecKill_redis.doSecKill(userid,prodid);
		boolean isSuccess= SecKill_redisByScript.doSecKill(userid,prodid);
		response.getWriter().print(isSuccess);
	}

}

12.Redis 持久化之 RDB

12.1 Rdb是什么

rdb是redis持久化方式之一,另一个是aof
它是在指定时间间隔中将内存中数据集快照写入磁盘(行业术语Snapshot 快照),它恢复时是将快照文件直接读到内存里。

12.2 备份是如何执行的

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

12.3 Fork

  • Fork 的作用是== 复制一个与当前进程一样的进程==。新进程的所有数据(变量、环境变量、程序计数器等) 数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程
  • 在 Linux 程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会 exec 系统调用,出于效率考虑,Linux 中引入了“写时复制技术
  • 一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容发生变化时,才会将父进程的内容复制一份给子进程。

12.4 RDB持久化流程

在这里插入图片描述

12.5 dump.rdb 文件

在 redis.conf 中配置文件名称,默认为 dump.rdb

12.6 配置位置

rdb 文件的保存路径,也可以修改。默认为 Redis 启动时命令行所在的目录下
dir “/myredis/”
在这里插入图片描述

12.7如何触发 RDB 快照;保持策略

12.7.1 配置文件中默认的快照配置

在这里插入图片描述

12.7.2 命令 save VS bgsave

save :save 时只管保存,其它不管,全部阻塞。手动保存。不建议。
bgsaveRedis 会在后台异步进行快照操作, 快照同时还可以响应客户端请求。
可以通过 lastsave 命令获取最后一次成功执行快照的时间

12.7.3 flushall 命令

执行 flushall 命令,也会产生 dump.rdb 文件,但里面是空的,无意义

12.7.4 SNAPSHOTTING 快照
12.7.5 Save

格式:save 秒钟 写操作次数

RDB 是整个内存的压缩过的 Snapshot,RDB 的数据结构,可以配置复合的快照触发条件,

默认是 1 分钟内改了 1 万次,或 5 分钟内改了 10 次,或 15 分钟内改了 1 次。

禁用

不设置 save 指令,或者给 save 传入空字符串

12.7.6 stop-writes-on-bgsave-error

在这里插入图片描述
当 Redis 无法写入磁盘的话,直接关掉 Redis 的写操作。推荐 yes.

12.7.7 rdbcompression 压缩文件

对于存储到磁盘中的快照,可以设置是否进行压缩存储。如果是的话,redis 会采用LZF 算法进行压缩。
如果你不想消耗 CPU 来进行压缩的话,可以设置为关闭此功能。
推荐 yes.

12.7.8 rdbchecksum 检查完整性

在这里插入图片描述
在存储快照后,还可以让 redis 使用 CRC64 算法来进行数据校验,但是这样做会增加大约 10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能
推荐 yes.

12.7.9 rdb 的备份

先通过 config get dir 查询 rdb 文件的目录
将*.rdb 的文件拷贝到别的地方
rdb 的恢复
◆ 关闭 Redis
◆ 先把备份的文件拷贝到工作目录下 cp dump2.rdb dump.rdb
◆ 启动 Redis, 备份数据会直接加载

12.8优势

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

12.9 劣势

⚫ Fork 的时候,内存中的数据被克隆了一份,大致 2 倍的膨胀性需要考虑
⚫ 虽然 Redis 在 fork 时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能。
⚫ 在备份周期在一定间隔时间做一次备份,所以如果 Redis 意外 down 掉的话,就会丢失最后一次快照后的所有修改。

12.10 如何停止

动态停止 RDB:redis-cli config set save “”#save 后给空值,表示禁用保存策略

12.11 小总结

在这里插入图片描述

13. Redis 持久化之 AOF

13.1AOF

13.1.1是什么?

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

13.1.2 AOF 持久化流程

(1)客户端的请求写命令会被 append 追加到 AOF 缓冲区内;
(2)AOF 缓冲区根据 AOF 持久化策略[always,everysec,no]将操作 sync 同步到磁盘的AOF 文件中;
(3)AOF 文件大小超过重写策略或手动重写时,会对 AOF 文件 rewrite 重写,压缩AOF 文件容量;
(4)Redis 服务重启时,会重新 load 加载 AOF 文件中的写操作达到数据恢复的目的;
在这里插入图片描述

13.1.3 AOF 默认不开启

可以在 redis.conf 中配置文件名称,默认为 appendonly.aof
AOF 文件的保存路径,同 RDB 的路径一致。

13.1.4 AOF 和 RDB 同时开启,redis 听谁的?

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

13.1.5 AOF 启动/修复/恢复

⚫ AOF 的备份机制和性能虽然和 RDB 不同, 但是备份和恢复的操作同 RDB 一样,都是拷贝备份文件,需要恢复时再拷贝到 Redis 工作目录下,启动系统即加载。
⚫ 正常恢复
◼ 修改默认的 appendonly no,改为 yes
◼ 将有数据的 aof 文件复制一份保存到对应目录(查看目录:config get dir)
◼ 恢复:重启 redis 然后重新加载
⚫ 异常恢复
◼ 修改默认的 appendonly no,改为 yes
◼ 如遇到== AOF 文件损坏==,通过/usr/local/bin/redis-check-aof–fixappendonly.aof 进行恢复
◼ 备份被写坏的 AOF 文件
◼ 恢复:重启 redis,然后重新加载

13.1.6 AOF 同步频率设置

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

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

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

13.1.7 Rewrite 压缩

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

2、 重写原理,如何实现重写
AOF 文件持续增长而过大时,会 fork 出一条新进程来将文件重写(也是先写临时文件最后再 rename),redis4.0 版本后的重写,是指上就是把 rdb 的快照,以二级制的形式附在新的 aof 头部,作为已有的历史数据,替换掉原来的流水账操作。

no-appendfsync-on-rewrite:

no-appendfsync-on-rewrite=yes ,不写入 aof 文件只写入缓存,用户请求不会阻塞,但是在这段时间如果宕机会丢失这段时间的缓存数据。(降低数据安全性,提高性能)

no-appendfsync-on-rewrite=no, 还是会把数据往磁盘里刷,但是遇到重写操作,可能会发生阻塞。(数据安全,但是性能降低)

2.1、触发机制,何时重写

Redis 会记录上次重写时的 AOF 大小,默认配置是当 AOF 文件大小是上次 rewrite 后大小的一倍且文件大于 64M 时触发

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

auto-aof-rewrite-percentage:设置重写的基准值,文件达到 100%时开始重写(文件是原来重写后文件的 2 倍时触发)
auto-aof-rewrite-min-size:设置重写的基准值,最小文件 64MB。达到这个值开始重写。

例如:文件达到 70MB 开始重写,降到 50MB,下次什么时候开始重写?100MB系统载入时或者上次重写完毕时,Redis 会记录此时 AOF 大小,设为 base_size,如果 Redis 的 AOF 当前大小>= base_size +base_size*100% (默认)且当前大小>=64mb(默认)的情况下,Redis 会对 AOF 进行重写。

3、重写流程
(1)bgrewriteaof 触发重写,判断是否当前有 bgsave 或 bgrewriteaof 在运行,如果有,则等待该命令结束后再继续执行。
(2)主进程 fork 出子进程执行重写操作,保证主进程不会阻塞。
(3)子进程遍历 redis 内存中数据到临时文件,客户端的写请求同时写入 aof_buf 缓冲区和 aof_rewrite_buf 重写缓冲区保证原 AOF 文件完整以及新 AOF 文件生成期间的新的数据修改动作不会丢失。
(4)1).子进程写完新的 AOF 文件后,向主进程发信号,父进程更新统计信息。2).主进程把 aof_rewrite_buf 中的数据写入到新的 AOF 文件。
(5)使用新的 AOF 文件覆盖旧的 AOF 文件,完成 AOF 重写。
在这里插入图片描述

13.1.8 优势

◼ 备份机制更稳健,丢失数据概率更低。
◼ 可读的日志文本,通过操作 AOF 稳健,可以处理误操作。

13.1.9 劣势

◼ 比起 RDB 占用更多的磁盘空间。
◼ 恢复备份速度要慢。
◼ 每次读写都同步的话,有一定的性能压力。
◼ 存在个别 Bug,造成恢复不能。

13.1.10 小总结

在这里插入图片描述

13.2 . 总结(Which one)

13.2.1 用哪个好

官方推荐两个都启用。
如果对数据不敏感,可以选单独用 RDB。
不建议单独用 AOF,因为可能会出现 Bug。
如果只是做纯内存缓存,可以都不用。

13.2.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 这条规则。

  • 如果使用 AOF,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只 load 自己的 AOF 文件就可以了。

  • 代价,一是带来了持续的 IO,二是 AOF rewrite 的最后将 rewrite 过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。

  • 只要硬盘许可,应该尽量减少 AOF rewrite 的频率,AOF 重写的基础大小默认值 64M太小了,可以设到 5G 以上。

  • 默认超过原大小 100%大小时重写可以改到适当的数值。

14. Redis_主从复制

14.1 什么是主从复制

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

14.2作用

  • 读写分离,性能扩展。
  • 容灾快速恢复
    在这里插入图片描述

14.3 主从复制怎么用

拷贝多个 redis.conf 文件 include(写绝对路径)

开启 daemonize yes

Pid 文件名字 pidfile

指定端口 port

Log 文件名字

dump.rdb 名字 dbfilename

Appendonly 关掉或者换名字

14.3.1 新建 redis6379.conf,填写以下内容

include /myredis/redis.conf

pidfile /var/run/redis_6379.pid

port 6379

dbfilename dump6379.rdb
在这里插入图片描述

14.3.2 新建 redis6380.conf,填写以下内容

在这里插入图片描述

14.3.3 新建 redis6381.conf,填写以下内容1

在这里插入图片描述
slave-priority 10
设置从机的优先级,值越小,优先级越高,用于选举主机时使用。默认 100

14.3.4 启动三台 redis 服务器

在这里插入图片描述

14.3.5 查看系统进程,看看三台服务器是否启动

在这里插入图片描述

14.3.6 查看三台主机运行情况

info replication
打印主从复制的相关信息
在这里插入图片描述

14.3.7 配从(库)不配主(库)

slaveof
成为某个实例的从服务器
1、在 6380 和 6381 上执行: slaveof 127.0.0.1 6379
在这里插入图片描述
2、在主机上写,在从机上可以读取数据
在从机上写数据报错
在这里插入图片描述
3、主机挂掉,重启就行,一切如初

4、从机重启需重设:slaveof 127.0.0.1 6379
可以将配置增加到文件中。永久生效。

在这里插入图片描述

14.4 常用的三种形式

14.4.1 一主二仆

14.3就是一主二仆模式

切入点问题?slave1、slave2 是从头开始复制还是从切入点开始复制?比如从 k4 进来,那之前的 k1,k2,k3 是否也可以复制?

从机是否可以写?set 可否?主机 shutdown 后情况如何?从机是上位还是原地待命?

主机又回来了后,主机新增记录,从机还能否顺利复制?其中一台从机 down 后情况如何?依照原有它能跟上大部队吗?

在这里插入图片描述

14.4.2 薪火相传

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

用 slaveof
中途变更转向:会清除之前的数据,重新建立拷贝最新的

风险是一旦某个 slave 宕机,后面的 slave 都没法备份

主机挂了,从机还是从机,无法写数据了

在这里插入图片描述

14.4.3 反客为主

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

用 slaveof no one 将从机变为主机。

在这里插入图片描述

14.5 复制原理

⚫ Slave 启动成功连接到 master 后会发送一个 sync 命令
⚫ Master 接到命令启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令, 在后台进程执行完毕之后,master 将传送整个数据文件到 slave,以完成一次完全同步
⚫ 全量复制:而 slave 服务在接收到数据库文件数据后,将其存盘并加载到内存中。
⚫ 增量复制:Master 继续将新的所有收集到的修改命令依次传给 slave,完成同步
⚫ 但是只要是重新连接 master,一次完全同步(全量复制)将被自动执行

在这里插入图片描述

14.6 哨兵模式(sentinel)

14.6.1 什么是哨兵模式

反客为主的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库
转换为主库
在这里插入图片描述

14.6.2 使用步骤
14.6.2.1 调整为一主二仆模式,6379 带着 6380、6381

在这里插入图片描述

14.6.2.2 自定义的/myredis 目录下新建 sentinel.conf 文件,名字绝不能错
14.6.2.3 配置哨兵,填写内容

sentinel monitor mymaster 127.0.0.1 6379 1

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

14.6.2.4 启动哨兵

/usr/local/bin
redis 做压测可以用自带的 redis-benchmark 工具
执行 redis-sentinel /myredis/sentinel.conf
在这里插入图片描述

14.6.2.5 当主机挂掉,从机选举中产生新的主机

(大概 10 秒左右可以看到哨兵窗口日志,切换了新的主机)

哪个从机会被选举为主机呢?根据优先级别:slave-priority

原主机重启后会变为从机
在这里插入图片描述

14.6.2.6 复制延时

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

14.6.3 故障恢复

在这里插入图片描述
优先级在 redis.conf 中默认:slave-priority 100,值越小优先级越高

偏移量是指获得原主机数据最全的

每个 redis 实例启动后都会随机生成一个 40 位的 runid

14.6.4 主从复制
private static JedisSentinelPool jedisSentinelPool=null;
public static Jedis getJedisFromSentinel(){
if(jedisSentinelPool==null){
 Set<String> sentinelSet=new HashSet<>();
 sentinelSet.add("192.168.11.103:26379");
 JedisPoolConfig jedisPoolConfig =new JedisPoolConfig();
 jedisPoolConfig.setMaxTotal(10); //最大可用连接数
jedisPoolConfig.setMaxIdle(5); //最大闲置连接数
jedisPoolConfig.setMinIdle(5); //最小闲置连接数
jedisPoolConfig.setBlockWhenExhausted(true); //连接耗尽是否等待
jedisPoolConfig.setMaxWaitMillis(2000); //等待时间
jedisPoolConfig.setTestOnBorrow(true); //取连接的时候进行一下测试 ping 
pong
jedisSentinelPool=new 
JedisSentinelPool("mymaster",sentinelSet,jedisPoolConfig);
return jedisSentinelPool.getResource();
}else{
return jedisSentinelPool.getResource();
 }
}

15. Redis 集群

15.1 问题

容量不够,redis如何进行扩容?
并发操作,redis如何进行分摊?
另外,主从模式,薪火相传模式,主机宕机,导致ip地址发生变化,应用程序中配置需要修改对应的主机地址,端口号信息。
之前通过代理主机进行解决。但是redis3.0中提供了解决方案就是无中心化集群配置。

15.2 什么是集群

redis集群实现了对redis的水平扩容,即启动了N个redis节点,将整个数据库分布存储在这N个节点中,每个节点存储总数据的1/N。
redis集群通过分区来提供一定程度的可用性:即使集群中有一部分节点失效或者无法进行通讯,集群也可以继续处理命令请求。

15.3 删除持久化数据

删除reb,aof文件

15.4制作六个实例

6379,6380,6381,6389,6390,6391

15.4.1 配置基本信息

开启 daemonize yes

Pid 文件名字

指定端口

Log 文件名字

Dump.rdb 名字

Appendonly 关掉或者换名字

15.4.2 redis cluster 配置修改

cluster-enabled ==yes ==打开集群模式
cluster-config-file nodes-6379.conf 设定节点配置文件名
cluster-node-timeout 15000 设定节点失联时间,超过该时间(毫秒),集群自动进行主从切换。

nclude /home/bigdata/redis.conf
port 6379
pidfile "/var/run/redis_6379.pid"
dbfilename "dump6379.rdb"
dir "/home/bigdata/redis_cluster"
logfile "/home/bigdata/redis_cluster/redis_err_6379.log"
cluster-enabled yes
cluster-config-file nodes-6379.conf
cluster-node-timeout 15000
15.4.3修 改 好 redis6379.conf 文 件 , 拷 贝 多 个redis.conf 文件

在这里插入图片描述

15.4.4使用查找替换修改另外 5 个文件

例如::%s/6379/6380

15.4.5 启动 6 个 redis 服务

在这里插入图片描述

15.5 将六个节点合成一个集群

组合之前,请确保所有 redis 实例启动后,nodes-xxxx.conf 文件都生成正常。
在这里插入图片描述

  • 合体:cd /opt/redis-6.2.1/src
redis-cli --cluster create --cluster-replicas 1 192.168.11.101:6379 
192.168.11.101:6380 192.168.11.101:6381 192.168.11.101:6389 
192.168.11.101:6390 192.168.11.101:6391

此处不要用 127.0.0.1, 请用真实 IP 地址
–replicas 1 采用最简单的方式配置集群,一台主机,一台从机,正好三组。

  • 普通方式登录
    可能直接进入读主机,存储数据时,会出现 MOVED 重定向操作。所以,应该以集群方式登录。
    在这里插入图片描述

15.6 -c 采用集群策略连接,设置数据会自动切换到相应的写主机

在这里插入图片描述

15.7 通过 cluster nodes 命令查看集群信息

在这里插入图片描述

15.8 redis cluster 如何分配这六个节点?

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

选项 --cluster-replicas 1 表示我们希望为集群中的每个主节点创建一个从节点。

分配原则尽量保证每个主数据库运行在不同的 IP 地址,每个从库和主库不在一个 IP 地址上。

15.9 什么是 slots

[OK] All nodes agree about slots configuration.

Check for open slots…
Check slots coverage…
[OK] All 16384 slots covered.

一个 Redis 集群包含 16384 个插槽(hash slot), 数据库中的每个键都属于这 16384 个插槽的其中一个,

集群使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽, 其中 CRC16(key) 语句用于计算键 key 的 CRC16 校验和 。

集群中的每个节点负责处理一部分插槽。 举个例子, 如果一个集群可以有主节点,
其中:
节点 A 负责处理 0 号至 5460 号插槽。
节点 B 负责处理 5461 号至 10922 号插槽。
节点 C 负责处理 10923 号至 16383 号插槽。

15.10 在集群中录入值

在 redis-cli 每次录入、查询键值,redis 都会计算出该 key 应该送往的插槽,如果不是该客户端对应服务器的插槽,redis 会报错,并告知应前往的 redis 实例地址和端口。

redis-cli 客户端提供了 –c 参数实现自动重定向。

如 redis-cli -c –p 6379 登入后,再录入、查询键值对可以自动重定向。

不在一个 slot 下的键值,是不能使用 mget,mset 等多键操作。
在这里插入图片描述
可以通过{}来定义组的概念,从而使 key 中{}内相同内容的键值对放到一个 slot 中去。
在这里插入图片描述

15.11 查询集群中的值

CLUSTER GETKEYSINSLOT 返回 count 个 slot 槽中的键。
在这里插入图片描述

15.12 故障恢复

如果主节点下线?从节点能否自动升为主节点?注意:15 秒超时
在这里插入图片描述

主节点恢复后,主从关系会如何?主节点回来变成从机。
在这里插入图片描述

如果所有某一段插槽的主从节点都宕掉,redis 服务是否还能继续?
如果某一段插槽的主从都挂掉,而 cluster-require-full-coverage 为 yes ,那么 ,整个集群都挂掉

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

15.13 集群的 Jedis 开发

即使连接的不是主机,集群会自动切换主机存储。主机写,从机读。

无中心化主从集群。无论从哪台主机写的数据,其他主机上都能读到数据。

public class JedisClusterTest {
 public static void main(String[] args) {
 Set<HostAndPort>set =new HashSet<HostAndPort>();
 set.add(new HostAndPort("192.168.31.211",6379));
 JedisCluster jedisCluster=new JedisCluster(set);
 jedisCluster.set("k1", "v1");
 System.out.println(jedisCluster.get("k1"));
 }
}

15.14 Redis 集群提供了以下好处

实现扩容

分摊压力

无中心配置相对简单

15.15 Redis 集群的不足

多键操作是不被支持的

多键的 Redis 事务是不被支持的。lua 脚本不被支持

由于集群方案出现较晚,很多公司已经采用了其他的集群方案,而代理或者客户端分片的方案想要迁移至 redis cluster,需要整体迁移而不是逐步过渡,复杂度较大。

16. Redis 应用问题解决

16.1 缓存穿透

16.1.1 问题描述

key 对应的数据在数据源并不存在,每次针对此 key 的请求从缓存获取不到,请求都会压到数据源,从而可能压垮数据源。比如用一个不存在的用户 id 获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。

在这里插入图片描述

16.1.2 解决方案

一个一定不存在缓存及查询不到的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。

解决方案:
(1) 对空值缓存:如果一个查询返回的数据为空(不管是数据是否不存在),我们仍然把这个空结果(null)进行缓存,设置空结果的过期时间会很短,最长不超过五分钟
(2) 设置可访问的名单(白名单):使用 bitmaps 类型定义一个可以访问的名单,名单 id 作为 bitmaps 的偏移量,每次访问和 bitmap 里面的 id 进行比较,如果访问 id 不在 bitmaps 里面,进行拦截,不允许访问。
(3) 采用布隆过滤器:(布隆过滤器(Bloom Filter)是 1970 年由布隆提出的。它实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数)。
布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。)
将所有可能存在的数据哈希到一个足够大的 bitmaps 中,一个一定不存在的数据会被 这个 bitmaps 拦截掉,从而避免了对底层存储系统的查询压力。
(4) 进行实时监控:当发现 Redis 的命中率开始急速降低,需要排查访问对象和访问的数据,和运维人员配合,可以设置黑名单限制服务

16.2 缓存击穿

16.2.1 问题描述

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

在这里插入图片描述

16.2.2 解决方案

key 可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题。

解决问题:
(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 缓存的方法。

在这里插入图片描述

16.3 缓存雪崩

16.3.1 问题描述

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

缓存雪崩与缓存击穿的区别在于这里针对很多 key 缓存,前者则是某一个 key正常访问
在这里插入图片描述
缓存失效瞬间
在这里插入图片描述

16.3.2 解决方案

缓存失效时的雪崩效应对底层系统的冲击非常可怕!

解决方案:
(1) 构建多级缓存架构:nginx 缓存 + redis 缓存 +其他缓存(ehcache 等)
(2) 使用锁或队列:用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。不适用高并发情况
(3) 设置过期标志更新缓存:记录缓存数据是否过期(设置提前量),如果过期会触发通知另外的线程在后台去更新实际 key 的缓存。
(4) 将缓存失效时间分散开:比如我们可以在原有的失效时间基础上增加一个随机值,比如 1-5 分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

17.Redis 分布式锁

17.1 问题描述

随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的 Java API 并不能提供分布式锁的能力。为了解决这个问题就需要一种跨 JVM 的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!

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

  1. 基于数据库实现分布式锁
  2. 基于缓存(Redis 等)
  3. 基于 Zookeeper
    每一种分布式锁解决方案都有各自的优缺点:
  4. 性能:redis 最高
  5. 可靠性:zookeeper 最高
    这里,我们就基于 redis 实现分布式锁。

17.2 解决方案:使用 redis 实现分布式锁

redis:命令

  • set sku:1:info “OK” NX PX 10000
    EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于SETEX key second value 。
    PX millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。
    NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。
    XX :只在键已经存在时,才对键进行设置操作。

在这里插入图片描述

  1. 多个客户端同时获取锁(setnx)
  2. 获取成功,执行业务逻辑{从 db 获取数据,放入缓存},执行完成释放锁(del)
  3. 其他客户端等待重试

17.3 编写代码

Redis: set num 0

@GetMapping("testLock")
public void testLock(){
 //1 获取锁,setne
 Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111");
 //2 获取锁成功、查询 num 的值
 if(lock){
 Object value = redisTemplate.opsForValue().get("num");
 //2.1 判断 num 为空 return
 if(StringUtils.isEmpty(value)){
   return;
 }
 //2.2 有值就转成成 int
 int num = Integer.parseInt(value+"");
 //2.3 把 redis 的 num 加 1
 redisTemplate.opsForValue().set("num", ++num);
 //2.4 释放锁,del
 redisTemplate.delete("lock");
 }else{
 //3 获取锁失败、每隔 0.1 秒再获取
 try {
 Thread.sleep(100);
 testLock();
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 }
}

基本实现。

问题:setnx 刚好获取到锁,业务逻辑出现异常,导致锁无法释放
解决:设置过期时间,自动释放锁。

17.4 优化之设置锁的过期时间

设置过期时间有两种方式:

  1. 首先想到通过 expire 设置过期时间(缺乏原子性:如果在 setnx 和 expire 之间出现异常,锁也无法释放)
  2. 在 set 时指定过期时间(推荐)

在这里插入图片描述
设置过期时间:
在这里插入图片描述
问题:可能会释放其他服务器的锁。

场景:如果业务逻辑的执行时间是 7s。执行流程如下

  1. index1 业务逻辑没执行完,3 秒后锁被自动释放。
  2. index2 获取到锁,执行业务逻辑,3 秒后锁被自动释放。
  3. index3 获取到锁,执行业务逻辑
  4. index1 业务逻辑执行完成,开始调用 del 释放锁,这时释放的是 index3 的锁,导致 index3 的业务只执行 1s 就被别人释放。最终等于没锁的情况。
    解决:setnx 获取锁时,设置一个指定的唯一值(例如:uuid);释放前获取这个值,判断是否自己的锁

17.5 优化之 UUID 防误删

在这里插入图片描述
在这里插入图片描述

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

  1. index1 执行删除时,查询到的 lock 值确实和 uuid 相等

uuid=v1
set(lock,uuid);
在这里插入图片描述

  1. index1 执行删除前,lock 刚好过期时间已到,被 redis 自动释放

在 redis 中没有了 lock,没有了锁。

在这里插入图片描述

  1. index2 获取了 lock

index2 线程获取到了 cpu 的资源,开始执行方法
uuid=v2
set(lock,uuid);

  1. index1 执行删除,此时会把 index2 的 lock 删除

index1 因为已经在方法中了,所以不需要重新上锁。index1 有执行的权限。index1 已经比较完成了,这个时候,开始执行
在这里插入图片描述
删除的 index2 的锁!

17.6 优化之 LUA 脚本保证删除的原子性

@GetMapping("testLockLua")
public void testLockLua() {
 //1 声明一个 uuid ,将做为一个 value 放入我们的 key 所对应的值中
 String uuid = UUID.randomUUID().toString();
 //2 定义一个锁:lua 脚本可以使用同一把锁,来实现删除!
 String skuId = "25"; // 访问 skuId 为 25 号的商品 100008348542
 String locKey = "lock:" + skuId; // 锁住的是每个商品的数据
 // 3 获取锁
 Boolean lock = redisTemplate.opsForValue().setIfAbsent(locKey, uuid, 3, 
TimeUnit.SECONDS);
 // 第一种: lock 与过期时间中间不写任何的代码。
 // redisTemplate.expire("lock",10, TimeUnit.SECONDS);//设置过期时间
 // 如果 true
 if (lock) {
 // 执行的业务逻辑开始
 // 获取缓存中的 num 数据
 Object value = redisTemplate.opsForValue().get("num");
 // 如果是空直接返回
 if (StringUtils.isEmpty(value)) {
 return;
 }
 // 不是空 如果说在这出现了异常! 那么 delete 就删除失败! 也就是说锁永
远存在!
 int num = Integer.parseInt(value + "");
 // 使 num 每次+1 放入缓存
 redisTemplate.opsForValue().set("num", String.valueOf(++num));
 /*使用 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
 // 因为删除判断的时候,返回的 0,给其封装为数据类型。如果不封装那么默认
返回 String 类型,
 // 那么返回字符串与 0 会有发生错误。
 redisScript.setResultType(Long.class);
 // 第一个要是 script 脚本 ,第二个需要判断的 key,第三个就是 key 所对应的
值。
 redisTemplate.execute(redisScript, Arrays.asList(locKey), uuid);
 } else {
 // 其他线程等待
 try {
 // 睡眠
 Thread.sleep(1000);
 // 睡醒了之后,调用方法。
 testLockLua();
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 }
}

Lua 脚本详解:
在这里插入图片描述
项目中正确使用:
定义 key,key 应该是为每个 sku 定义的,也就是每个 sku 有一把锁。

String locKey ="lock:"+skuId; // 锁住的是每个商品的数据
Boolean lock = redisTemplate.opsForValue().setIfAbsent(locKey, 
uuid,3,TimeUnit.SECONDS);

在这里插入图片描述

17.7 总结

  1. 加锁
// 1. 从 redis 中获取锁,set k1 v1 px 20000 nx
String uuid = UUID.randomUUID().toString();
Boolean lock = this.redisTemplate.opsForValue()
 .setIfAbsent("lock", uuid, 2, TimeUnit.SECONDS);

  1. 使用lua释放锁
// 2. 释放锁 del
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return 
redis.call('del', KEYS[1]) else return 0 end";
// 设置 lua 脚本返回的数据类型
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
// 设置 lua 脚本返回类型为 Long
redisScript.setResultType(Long.class);
redisScript.setScriptText(script);
redisTemplate.execute(redisScript, Arrays.asList("lock"),uuid);
  1. 重试
Thread.sleep(500);
testLock();

为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:

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

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

  • 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

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

17.8 redis相关代码

链接: redis相关代码.

17.9 redis优秀面试题

链接: Redis夺命十二问,你能扛到第几问?.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
尚硅谷是一个教育机构,他们提供了一份关于Redis学习笔记。根据提供的引用内容,我们可以了解到他们提到了一些关于Redis配置和使用的内容。 首先,在引用中提到了通过执行命令"vi /redis-6.2.6/redis.conf"来编辑Redis配置文件。这个命令可以让你进入只读模式来查询"daemonize"配置项的位置。 在引用中提到了Redis会根据键值计算出应该送往的插槽,并且如果不是该客户端对应服务器的插槽,Redis会报错并告知应该前往的Redis实例的地址和端口。 在引用中提到了通过修改Redis的配置文件来指定Redis的日志文件位置。可以使用命令"sudo vim /etc/redis.conf"来编辑Redis的配置文件,并且在文件中指定日志文件的位置。 通过这些引用内容,我们可以得出结论,尚硅谷的Redis学习笔记涵盖了关于Redis的配置和使用的内容,并提供了一些相关的命令和操作示例。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [Redis学习笔记--尚硅谷](https://blog.csdn.net/HHCS231/article/details/123637379)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [Redis学习笔记——尚硅谷](https://blog.csdn.net/qq_48092631/article/details/129662119)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值