Redis 基础知识点总结

1. NoSQL 数据库简介

NoSQL 是为了解决性能问题产生的一种技术,Redis 就是一个典型的NoSQL 的数据库。

1.1 技术发展

技术分类:

  1. 解决功能特性的问题:Java、Jsp、Tomcat、Jdbc
  2. 解决扩展性的问题:Spring、SpringMVC、Mybatis
  3. 解决性能的问题:NoSql、Java多线程、Nginx、MQ、ElasticSearch
1.1.1 Web1.0时代

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

用户通过 url 访问 Web 服务器,然后 Web 服务器访问数据库服务,不能承受住大量的访问。

在这里插入图片描述

1.1.2 Web2.0 时代

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

在这里插入图片描述

1.1.3 解决 CPU 及内存压力

在这里插入图片描述

在这里插入图片描述

1.1.4 解决 IO 压力

在这里插入图片描述

1.2 NoSQL 数据库
1.2.1 NoSQL 数据库概述

NoSQL(Not Only SQL),意思是不仅仅是 sql,泛指 非关系型的数据库

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

  • 不遵守 SQL 标准
  • 不支持 ACID
  • 远超于 SQL 的性能
1.2.2 NoSQL 适用场景
  • 对数据高并发的读写
  • 海量数据的读写
  • 对数据高可扩展性的
1.2.3 NoSQL 不适用场景
  • 需要事务支持
  • 基于 sql 的结构化查询存储,处理复杂的关系,需要及时查询
  • (用不着 sql 的和用了 sql 也不行的情况,考虑使用 NoSQL)
1.2.4 常见的 NoSQL 数据库
  1. Mem cache

    很早出现的 NoSQL 数据库,数据都在内存中,一般不持久化,支持简单的 key-value 模式,支持类型单一,一般是作为缓存数据库辅助持久化的数据库

  2. Redis

    几乎覆盖了 Memcached 的绝大部分功能,数据库都在内存中,支持持久化,主要用作备份恢复,除了支持简单的 key-value 模式,还支持多种数据结构的存储,比如:list、set、hash、zset等,一般是作为缓存shu’ju’k辅助持久化的数据库。

  3. MongoDB

    高性能、开源、模式自由的文档型数据库,数据库都在内存中,如果内存不足,把不常用的数据保存到硬盘,虽然是 key-value 模式,但是对 value(尤其是 json)提供了丰富的查询功能,支持二进制数据及大型对象,可以根据数据的特点替代 RDBMS,成为独立的数据库,或者配合 RDBMS,存储特定的数据。

1.3 行式存储数据库(大数据时代)
1.3.1 行式数据库

在这里插入图片描述

1.3.2 列式数据库

在这里插入图片描述

1.4 数据库排名:http://db-engines.com/en/ranking

2. Redis6 概述和安装

  • Redis 是一个开源的 key-value 存储系统
  • 支持存储的 value 类型很多,包括 String、list、set、zset(sorted set --有序集合)和 hash
  • 这些数据类型都支持 push/pop、add/remove 及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的
  • 在次基础上, Redis支持各种不同方式的排序
  • 与 memcached 一样,为了保证效率,数据都是缓存在内存中
  • 区别的是 Redis 会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件
  • 并且在此基础上实现了 master-slave(主从) 同步。
2.1 应用场景
2.1.1 配合关系型数据库做高速缓存
  • 高频次,热门访问的数据,降低数据库 IO
  • 分布式架构,做 session 共享

在这里插入图片描述

2.1.2 多样的数据结构存储持久化数据

在这里插入图片描述

2.2 Redis 安装

由于官网并不提供 windows 的安装,所以全程在 linux 系统上使用。

2.2.1 下载

官网:https://redis.io

在这里插入图片描述

2.2.2 安装步骤
  1. 准备工作:下载安装最新版的 gcc 编译器

    安装 redis 需要环境有 C 语言的编译环境

    安装 C 语言的编译环境

    yum install centos-release-scl scl-utils-build

    yum install -y devtoolset-8-toolchain

    scl enable devtoolset-8 bash

    测试 gcc 版本 gcc --version

    使用的 Centos7,使用命令 yum install gcc 即可

  2. 将 Redis 放到 /opt 目录

  3. 解压命令:tar -zxvf redis-release... tar.gz

  4. 解压完成后进入目录 cd redis-release...

  5. 在 redis-release… 目录下再次执行 make命令(只是编译好),编译成 c 文件

    这里只是编译好了,并没有进行安装

    在这里插入图片描述

  6. 执行 make install进行安装

    安装目录:/usr/local/bin

    进入 /usr/local/bin 目录下,有以下几个文件表示安装成功

    在这里插入图片描述

    • redis-benchmark:性能测试工具,可以在自己本子运行,看看自己本子性能如何
    • redis-check-aof:修复有问题的 AOF 文件,rdb和aof
    • redis-check-dump:修复有问题的 dump.rdb 文件
    • redis-sentinel:Redis 集群使用
    • redis-server:Redis 服务器启动命令
    • redis-cli:客户端,操作入口
2.2.3 前台启动(不推荐)

执行命令 redis-server是,出现以下内容表示启动成功,这种启动方式不能退出窗口,所以不推荐使用

在这里插入图片描述

2.2.4 后台启动(推荐)
  1. 进入解压的 redis 的文件夹找到 redis.conf,然后将该文件复制到另一个位置一份(可以不复制,无所谓)

    在这里插入图片描述

  2. 在配置文件中设置 daemonize no 改为 yes,让服务在后台启动,注意:修改的是 etc 目录下的配置文件

    在这里插入图片描述

  3. Reis 启动

    进入 /usr/local/bin 目录下,执行命令 redis-server /etc/redis.conf即可启动

    在这里插入图片描述

  4. 使用客户端访问:redis-cli

    在这里插入图片描述

  5. 测试连通

    在这里插入图片描述

  6. Redis 关闭

    执行命令 redis-cli shutdown 进行关闭

    也可以进入中断后再关闭

    指定端口号关闭:redis-cli -p 6379 shutdown

    也可以通过 kill KID 进行关闭

2.3 Redis 相关知识
2.3.1 端口号的由来

端口 6379 从何而来?一个明星叫 Merz,4 个字母对应的是按键手机的 6379 键

2.3.2 切换操作的库

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

使用命令 select < dbid > 来切换数据库。如:select 3

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

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

flushdb 清空当前库

flushall 通杀全部库

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

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

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

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

在这里插入图片描述

3. 常用五大数据类型

常用命令:http://www.redis.cn/commands.html

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 index—切换数据库 [0~15]

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

flushdb—清空数据库,慎用

flushall—通杀数据库,慎用

3.2 Redis 字符串(String)
3.2.1 简介

​ String 是 Redis 最基本的类型,一个 key 对应的一个 value

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

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

原子性

这里的原子性不是事务中的原子性,是 Redis 中的特有

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

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

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

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

3.2.2 常用命令
  1. set <key> <value> 添加键值对

    *NX:当数据库中 key 不存在时,可以将 key-value 添加数据库

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

    *EX:key 的超时秒数

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

  2. get <key> 查询对应键值对

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

  4. strlen <key> 获得值的长度

  5. setnx <key><value> 只有在 key 不存在时,设置key 的值,set 设置key 如果有则会覆盖之前的值,这个如果存在返回 0 ,不能设置 key

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

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

  8. incrby/decrby <key><step> 将 key 中存储的数字值增减,自定义 step

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

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

  11. msetnx <key1><value1> <key2><value2>...... 同时设置一个或多个 key-value,但是给定的 key 在库中不能存在,如果有一个 key 存在命令就会执行失败,因为 Redis 的原子性,有一个失败就都会失败的。

  12. getrange <key><起始位置><结束位置> 获得值的范围,类似 java 中的 substring,前包,后包

  13. setrange <key><起始位置><value> 用 value 覆写 key 所存储的字符串值,从 起始位置开始 索引从 0 开始

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

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

3.2.3 数据结构

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

在这里插入图片描述

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

3.3 Redis 列表(List)
3.3.1 简介

单键多值

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

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

在这里插入图片描述

3.3.2 常用命令
  1. lpush/rpush <key><value1><value2><value3>... 从左边/右边插入一个或多个值

  2. lpop/rpop <key> 从左边/右边吐出一个值。值在键在,值光键亡

  3. rpoplpush <key1><key2> 从 key1 列表右边吐出一个值,插入到 key2 列表左边

  4. lrange <key><start><stop> 按照索引下标获得元素(从左到右)

    lrange mylist 0 -1 0表示左边第一个,-1 表示右边第一个,(0 -1 表示获取所有)

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

  6. llen <key> 获得列表长度

  7. linsert <key> before|after <value><newvalue> 在 value 的后面插入 newvalue

  8. lrem <key><n><value> 从左边删除 n 个value(从左到右)

  9. lset <key><index><value> 将列表 key 下标为 index 的值替换成value

3.3.3 数据结构

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

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

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

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

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

在这里插入图片描述

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

3.4 Redis 集合(Set)
3.4.1 简介

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

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

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

3.4.2 常用命令
  1. sadd <key><value1><value2>... 将一个或多个 member 元素加入到集合 key 中,已经存在的 memeber 元素将被忽略
  2. smembers <key> 取出该集合的所有值
  3. sismember <key><value> 判断集合 key 是否为含有该 value 值,有 1,没有 0
  4. scard <key> 返回该集合的元素个数
  5. srem <key><value1><value2>... 删除集合中的某个元素
  6. spop <key> 随机从集合中吐出一个值
  7. srandmember <key><n> 随机从该集合中取出 n 个值,不会从集合中删除
  8. smove <source><destination> value 把集合中一个值从一个集合移动到另一个集合
  9. sinter <key1><key2> 返回两个集合的交集元素
  10. sunion <key1><key2> 返回两个集合的并集元素
  11. sdiff <key1><key2> 返回两个集合的差集元素(key1 中的,不包含 key2 中的 )
3.4.3 数据结构

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

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

3.5 Redis 哈希(Hash)
3.5.1 简介

​ Redis hash 是一个键值对集合。

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

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

在这里插入图片描述

3.5.2 常用命令
  1. hset <key><field><value> 给 key 集合中的 field 键赋值 value
  2. hget <key1><field> 从 key1 集合field 取出 value
  3. hmset <key1><field1><value1><field2><value2>... 批量设置 hash 的值
  4. hexists <key1><field> 查看哈希表key 中,给定域 field 是否存在
  5. hkeys <key> 列出该 hash 集合的所有 field
  6. hvals <key> 列出该 hash 集合的所有 value
  7. hincrby <key><field><increment> 为哈希表 key 中的域 field 的值加上增量 1 -1
  8. hsetnx <key><field><value> 将哈希表 key 中的域 field 的值设置为 value,当且仅当域 field 不存在
3.5.3 数据结构

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

3.6 Redis 有序集合 Zset (sorted set)
3.6.1 简介

​ Redis 有序结合 zset 与普通集合 set 非常相似,是一个没有重复元素的字符串集合。不同之处是有序集合的每个成员都关联了一个评分(score),这个评分被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复的。

​ 因为元素是有序的,所以你也可以很快的根据评分或者次序来获取一个范围的元素,访问有序集合的中间元素也是非常快的,因此你能够使用有序集合作为一个没有重复成员的智能列表。

3.6.2 常用命令
  1. zadd <key><score1><value1><score2><value2>... 将一个或者多个 member 元素及其 score 值加入到有序集合 key 当中
  2. zrange <key><start><stop> [WITHSCORES] 返回有序集key中,下标在 start stop 之间的元素,带 WITHSCORES,可以让分数一起和值返回到结果
  3. zrangebyscore key minmax [withscores][limit offset count] 返回有序集 key 中,所有 score 值介于 min 和max 之间(包括等于min 或 max) 的成员。有序集成员按 score 值递增(从小到大)次序排列
  4. zrevrangebyscore key max min [withscores][limit offset count] 同上,改为从大到小排列
  5. zincrby <key><increment><value> 为元素的 score 加上增量
  6. zrem <key><value> 删除该集合下,指定值的元素
  7. zcount <key><min><max> 统计该集合,分数区间内的元素个数
  8. zrank <key><value> 返回该值在集合中的排名,从 0 开始
3.6.3 数据结构

​ 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.4 跳跃表
  1. 简介

    有序集合在生活中比较常见,例如根据成绩对学生进行排名,根据得分对玩家排名等。对于有序集合的底层实现,可以用数组、平衡树、链表等。数组不便元素的插入、删除;平衡数或红黑树虽然效率高但结构复杂;链表查询需要遍历所有效率低。Redis 采用的是跳跃表。跳跃表的效率堪比红黑树,实现远比红黑树简单。

  2. 实例

    对比有序链表和跳跃表,从查询中查询出 51

    • 有序链表

    在这里插入图片描述

    • 跳跃表

    在这里插入图片描述

4. Redis6 配置文件详解

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 表示支持远程访问,yes 表示不支持远程访问只允许本机访问,所以修改为 no

4.3.3 port

端口号,默认为 6379

4.3.4 tcp-backlog

设置 tcp 的backlog,backlog 其实是一个连接队列,backlog 队列总和=未完成三次握手队列+已经完成三次握手队列

在高并发环境下需要一个高 backlog 值来避免慢客户端连接问题。

值可以修改

4.3.5 timeout

默认为 0

当我连接上 redis 后,长时间没有操作,然后间隔了一段时间后再去操作会出现连接超时的问题的。

如果是 0 表示不会超时,默认为 0,单位是 秒。

4.3.6 tcp-keepalive

表示检查心跳时间,心跳时间:默认是 300秒,很长时间没有操作,然后redis 有一个检查机制,如果还在操作就会终止服务,否则继续服务,间隔时间 300秒

4.4 ### GENERAL 通用###
4.4.1 daemonize

是否为后台进程,设置 yes 为后台进程,no 为前台进程

设置yes 后守护线程,后台启动

4.4.2 pidfile

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

每次redis 启动会产生一个 pid 号码,然后会将这个 pid 号设置到一个路径中

pidfile /var/run/redis_6379.pi

4.4.3 loglevel

表示 redis 中的日志的级别,默认是 notice

4个级别

  1. debug:在开发环境中,用 debug 可以看到更加详细的信息
  2. verbose:可以更多有用的信息
  3. notice:生产环境中用
  4. warning:显示一些有用的重要的信息
4.4.4 logfile

表示设置日志的输出文件路径,默认为空,可以自行设置。

4.4.5 databases

设置库的数量,默认 16,默认数据库 0,可以使用 select< dbid >命令在连接上指定数据库 id

4.5 ### SECURITY安全###
4.5.1 在配置文件中设置密码

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

在这里插入图片描述

4.5.2 用命令行设置密码

config get requirepass

config set requirepass "密码"

auth 密码

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,只对设置了过期时间的键
  • allkeys0random:在所有集合 key 中,移除随机的 key
  • volatile-ttl:移除那些 TTL 值最小的 key,即那些最近要过期的 key
  • noeviction: 不进行移除。针对写操作,只是返回错误信息。
4.6.4 maxmemory-samples
  • 设置样本数量,LRU 算法和最小 TTL 算法都并非是精确的算法,而是估算值,所以你可以设置样本的大小,redis 默认会检查这么多个 key 并选择其中 LRU 的那个
  • 一般设置 3 到 7 的数字,数值越小样本越不准确,但性能消耗越小。

5. Redis6 的发布和订阅

5.1 什么是发布和订阅

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

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

5.2 Redis 的发布和订阅

在这里插入图片描述

5.3 发布订阅命令行实现
  1. 打开一个客户端订阅 channel1

    subscribe channel1

    在这里插入图片描述

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

    publish channel1 hello

    返回的 1 表示订阅者的数量

    在这里插入图片描述

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

6. Redis6 新数据类型

6.1 Bitmaps
6.1.1 简介

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

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

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

  1. Bitmaps 本身不是一种数据类型,实际上它就是字符串(key-value),但是它可以对字符串的位进行操作
  2. Bitmaps 单独提供了一套命令,所以在 Redis 中使用 Bitmaps 和使用字符串的方法不太相同。可以把 Bitmaps 想象成一个以位为单位的数组,数组的每个单元只能存储 0 和 1,数组的下标在 Bitmaps 中叫做偏移量。
6.1.2 常用命令
  1. setbit <key><offset><value> 设置 Bitmaps 中某个偏移量的值(0或1)

    *offset:偏移量从 0 开始

    *setbit 键名 时间 offset value,表示时间独立访问用户的 Bitmaps

    例如:每个独立用户是否访问过存放在 Bitmaps 中,将访问的用户记作 1,没有访问的用户记作 0,用偏移量作为用户的 id,设置键的第 offset 个位的值(从 0 算起),假设现在有 11 个用户,userid = 1,3,5 的用户对网站进行了访问,那么当前 Bitmaps 初始化结果如图:

    在这里插入图片描述

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

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

  2. getbit <key><offset> 获取 Bitmaps 中某个偏移量的值

    获取键的第 offset 位的值,从 0 开始计算

    例如:获取 id = 1 的用户是否访问过,返回 0 表示没有访问过,反之访问过

  3. bitcount

    统计字符串被设置为 1 的bit 的数,一般情况下,给定的整个字符串都会被进行计数,通过指定额外的 start 或 end 参数,可以让计数只在特定的位上进行。start 和 end 参数的设置,都可以使用负数值:比如 -1 表示最后一个位,而 -2 表示倒数第二个位,start、end 是指 bit 组的字节的下标数,二者皆包含

    bitcount <key>[start end] 统计字符串从 start 字节到 end 字节比特值为 1 的数量

  4. bittop and(or/not/xor) <destkey> [key...]

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

    例如:

    2021-9-1 日访问网站的userid = 1 2 5 9

    setbit unique:users:20210901 1 1

    setbit unique:users:20210901 2 1

    setbit unique:users:20210901 5 1

    setbit unique:users:20210901 9 1

    2021-9-3 日访问网站的userid = 0 1 4 9

    setbit unique:users:20210903 0 1

    setbit unique:users:20210903 1 1

    setbit unique:users:20210903 4 1

    setbit unique :users:20210903 9 1

    计算两天都访问过的用户 bitop and users:and:20210901_3 user:20210901 users:20210903

6.1.3 Bitmaps 与 set 对比

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

  • set 和 Bitmaps 存储一天活跃用户对比
数据类型每个用户id占用空间需要存储的用户量全部内存量
集合类型64位5000000064位 * 50000000 = 400MB
Bitmaps1位1000000001位 * 100000000 = 12.5MB

​ 明显的是,这种情况下使用 Bitmaps 能节省很多的内存空间,尤其是随着时间推移节省的内存还是非常可观的。

  • set 和 Bitmaps 存储独立用户空间对比
数据类型一天一个月一年
集合类型400MB12GB144GB
Bitmpas12.5MB375MB4.5GB

Bitmaps 并不是绝对的好的,加入网站每天的独立访问用户很少,例如只有10万,那么两者的对比如下,显然,这时候使用 Bitmaps 就不太适合了,因为基本上大部分位都是 0。

  • set 和 Bitmaps 存储一天活跃用户对比(独立用户比较少)
数据类型每个 userid 占用空间需要存储的用户梁全部内存量
集合类型64位10000064位 * 100000 = 800KB
Bitmaps1位1000000001位*100000000 = 12.5MB
6.2 HyperLogLog
6.2.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 键只需要花费 12KB 内存,就可以计算接近 2^64 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比

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

什么是基数?

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

6.2.2 常用命令
  1. pfadd <key><element>[element...] 添加指定元素到 HyperLogLog 中

    将所有元素添加到指定的 HyperLogLog 数据结构中。如果执行命令后 HLL 估计的近似基数发生变化,则返回 1,否则返回 0

  2. pfcount <key>[key...] 计算 HLL 的近似基数,可以计算多个 HLL ,比如用 HLL 存储每天的 UV,计算一周的 UV 可以使用 7 天的 UV 合并计算即可

  3. pfmerge <destkey><sourcekey>[sourcekey ...] 将一个或多个 HLL 合并后的结果存储在另一个HLL 中,比如每月活跃用户可以使用每天的活跃用户来合并计算可得

6.3 Geospatial
6.3.1 简介

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

6.3.2 常用命令
  1. geoadd <key><longitude><latitude><member>[longitude latitude member] 添加地理位置(经度,纬度,名称)

    注:两极无法直接添加,一般会下载城市数据,直接通过 Java 程序一次性导入。有效的经度从 -180 度到 180度。有效的纬度从 -85.0505112878 度到 85.0505112878

    当坐标位置超出指定范围时,该命令将会返回一个错误

    已经添加的数据,是无法再次往里面添加的。

  2. gepos <key><member>[member...] 获得指定地区的坐标值

  3. geodist <key><member1><member2>[m|km|ft|mi] 获取两个位置之间的直线距离

    单位:

    m 表示单位为米,默认

    km 表示单位为千米

    mi 表示单位为英里

    ft 表示单位为英尺

    如果用户没有显式地指定单位参数,那么 GEODIST 默认使用米作为单位

  4. georadius <key><longitude><latitude> radius m|km|ft|mi 以给定的经纬度为中心找出某一半径内的元素,精度 纬度 距离 单位

7. Jedis 操作 Redis6

7.1 Jedis 需要使用的 依赖包

首先创建一个 Maven 工程,然后引入所需要的依赖包

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.2.0</version>
</dependency>

创建 RedisTest 类,测试是否可以连接成功

public static void main(String[] args) {
    // 创建 Jedis 对象,加入 ip 地址和端口与 redis 创建连接
    Jedis jedis = new Jedis("192.168.43.53",6379);
    // 测试连接,如果这里连接不成功可能式虚拟机的防火墙的问题,关掉防火墙
    String ping = jedis.ping();
    System.out.println(ping);
}

结果输出 PONG 表示连接成功

7.2 连接 Redis 注意事项

当执行以上代码连接的时候,如果出现 timeout 的情况可能是虚拟机的防火墙拦截了,需要在虚拟机中关闭防火墙

systemctl status firewalld 查看防火墙状态

systemctl stop firewalld 关闭防火墙

还需要检查 redis.conf 配置文件中是否注释掉了 bind 127.0.0.1 和 protected-mode 模式是否改为 no

7.3 Jedis 常用操作
7.3.1 Jedis-API:key
jedis.set("k1", "v1");
jedis.set("k2", "v2");
jedis.set("k3", "v3");
Set<String> keys = jedis.keys("*");
for (String key : keys) {
    System.out.println(key);
}
System.out.println(jedis.exists("k1"));
System.out.println(jedis.ttl("k1"));
System.out.println(jedis.get("k3"));
7.3.2 Jedis-API:String
jedis.mset("str1","v1","str2","v2","str3","v3");
System.out.println(jedis.mget("str1","str2","str3"));
7.3.3 Jedis-API:List
/**
 * 针对 List 的操作
 */
@Test
public void demo03(){
    Jedis jedis = new Jedis("192.168.43.53", 6379);
    jedis.lpush("key1","java","php","mysql");
    System.out.println(jedis.lrange("key1",0,-1));
}
7.3.4 Jedis-API:set
/**
 * 针对 set 的操作
 */
@Test
public void demo04(){
    Jedis jedis = new Jedis("192.168.43.53", 6379);
    jedis.sadd("name","wangwu","zhangsan","lisi");
    Set<String> name = jedis.smembers("name");
    for (String s : name) {
        System.out.println(s);
    }
}
7.3.5 Jedis-API:hash
/**
 * 针对 hash 的操作
 */
@Test
public void demo05(){
    Jedis jedis = new Jedis("192.168.43.53", 6379);
    HashMap<String, String> map = new HashMap<>();
    map.put("name","zhangsan");
    map.put("age","20");
    jedis.hmset("info",map);
    List<String> info = jedis.hmget("info","name","age");
    System.out.println(info);
}
7.3.6 Jedis-API:zset
/**
 * 针对 zset 的操作
 */
@Test
public void demo06(){
    Jedis jedis = new Jedis("192.168.43.53", 6379);
    jedis.zadd("city",11,"shanghai");
    jedis.zadd("city",22,"beijing");
    jedis.zadd("city",33,"tianjin");
    Set<String> city = jedis.zrange("city", 0, -1);
    for (String s : city) {
        System.out.println(s);
    }
}

8. Redis_Jedis_实例

8.1 完成一个手机验证码功能

要求:

  1. 输入手机号,点击发送后随机生成 6 位数字码,2 分钟有效
  2. 输入验证码,点击验证,返回成功或失败
  3. 每个手机号每天只能输入 3 次
public class AuthCodeTest {    
	// 1. 输入手机号,点击发送后随机生成 6 位数字码,2 分钟有效    
	// 2. 输入验证码,点击验证,返回成功或失败    
	// 3. 每个手机号每天只能输入 3 次    
	public static void main(String[] args) {        
		//String code = getCode();        
		//System.out.println(code);        
		verifyCode("121212121");        
		//getRedisCode("121212121","493473");    
	}    
	// 3. 验证码校验    
	public static void getRedisCode(String phone, String code) {        
		// 从 redis 中获取验证码        
		Jedis jedis = new Jedis("192.168.43.53", 6379);        
		String codeKey = "verifyCode" + phone + ":code";        
		String s = jedis.get(codeKey);        
		// 判断        
		if (s.equals(code)) {            
			System.out.println("成功");        
		} else {            
			System.out.println("失败");        
		}    
	}    
	// 2 每个手机每天只能发送三次,验证码放到 redis 中,设置过期时间    
	public static void verifyCode(String phone) {        
		// 连接 redis        
		Jedis jedis = new Jedis("192.168.43.53", 6379);        
		// 拼接 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("今天的发送次数已经超过 3 次");            
			jedis.close();            
			return;        
		}        
		// 发送的验证码放到 redis        
		String vcode = getCode();        
		jedis.setex(codeKey, 120, vcode);        
		jedis.close();    
		}    
		// 1 生成 6 位数验证码    
		public static String getCode() {        
			Random random = new Random();        
			String codes = "";        
			for (int i = 0; i < 6; i++) {            
				codes += random.nextInt(10);        
			}        
				return codes;    
			}
		}

9. Redis6 与 SpringBoot 整合

9.1 创建项目,加入 redis 依赖
<!--redis-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>2.5.4</version>
</dependency>
<!--spring2.x 继承 redis 所需 common-pool2-->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.11.1</version>
</dependency>
9.2 application.properties 配置 redis 配置
# Redis 服务器地址
spring.redis.host=192.168.43.53
# 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
9.3 RedisConfig.java 配置类
  • 固定内容
@EnableCaching  // 开启缓存
@Configuration
public class RedisConfig extends CachingConfigurerSupport {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        RedisSerializer 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<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        // 解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // 配置序列化(解决乱码的问题),过期时间 600 秒
        RedisCacheConfiguration 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();
        return cacheManager;
    }
}
9.4 RedisController.java
@RestController
@RequestMapping("/redis")
public class RedisController {

    @Autowired
    private RedisTemplate<String,Object> redisTemplate;

    @GetMapping
    public String testRedis(){
        // 设置值到 redis
        redisTemplate.opsForValue().set("name","zhangsan");
        // 取出设置的值
        String name = (String) redisTemplate.opsForValue().get("name");
        return name;
    }
}

然后运行后通过浏览器访问 localhost:8080/redis 就可以看到 zhangsan

10. Redis6 的事务操作

10.1 Redis 的事务定义

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

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

在这里插入图片描述

10.2 Multi、Exec、discard

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

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

在这里插入图片描述

在这里插入图片描述

10.3 事务的错误处理
  • 组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消

在这里插入图片描述

在这里插入图片描述

  • 如果执行阶段某个命令报出来错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚

在这里插入图片描述

在这里插入图片描述

10.4 为什么要使用事务

假设一个场景:有很多人有你的账户,同时去参加双十一抢购

10.5 事务冲突问题
10.5.1 案例

一个请求想给金额减 8000

一个请求想给金额减 5000

一个请求想给金额减 1000

在这里插入图片描述

10.5.2 悲观锁(Pessimistic Lock)

在这里插入图片描述

第一个人操作的时候 10000 进行上锁,需要花费 8000 < 10000,然后可以使用后解锁

第二个人操作的时候 2000 进行上锁,然后发现2000 < 5000 ,所以不进行操作

第三个人操作的时候 2000 进行上锁,需要花费 1000 < 2000,然后解锁使用

上锁之后别人是不能进行操作的,等着解锁了别人才能操作

  • 这个锁机制就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会 block 直到它拿到锁。传统的关系型数据库里面就用到了很多这种锁机制,比如:行锁、表锁等,读锁、写锁等,都是在操作之前先上锁。
10.5.3 乐观锁(Optimistic Lock)

在这里插入图片描述

在操作过程中会有对数据库中的数据进行一个版本号的绑定

开始绑定版本号 v1.0

操作数据的时候所有人都能得到版本的序号,版本都是 1.0,这是第一个人手比较快,然后减掉 8000后先修改版本号为 v1.1 ,数据库的数据为 2000,数据库和版本号同步进行更新,然后第二个人的时候检查 一下自己得到的数据版本号是否与数据库中数据的版本号相等,如果相等则可以操作,如果版本号不一致就不能操作

  • 这个锁的机制就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis 就是利用这种 check-and-set 机制实现事务的。
10.5.4 WATCH key [key …]

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

  • watch 监视,乐观锁

在这里插入图片描述

10.5.5 unwatch

取消 watch 命令对所有 key 的监视

如果在执行 watch 命令之后,exec 命令或 discard 命令先被执行了的话,那么就不需要执行 unwatch了

10.6 Redis 事务三特性
  • 单独的隔离性

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

  • 没有隔离级别的概念

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

  • 不保证原子性

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

11. Redis事务-秒杀案例

11.1 简单秒杀案例

在这里插入图片描述

  1. 创建一个 SpringBoot Web 项目,导入相关的依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--redis-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>2.5.4</version>
</dependency>
<!--spring2.x 继承 redis 所需 common-pool2-->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.11.1</version>
</dependency>
  1. 在配置文件中配置 redis 和 thymeleaf
# thymeleaf 关闭缓存
spring.thymeleaf.cache=false

# Redis 服务器地址
spring.redis.host=192.168.43.53
# Redis 服务器端口
spring.redis.port=6379
# Redis 数据库索引,默认为 0
spring.redis.database=0
# 连接超时时间 毫秒
spring.redis.timeout=10000000
# 连接池最大连接数(负数表示没有限制)
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 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<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        // 解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // 配置序列化(解决乱码的问题),过期时间 600 秒
        RedisCacheConfiguration 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();
        return cacheManager;
    }
}
  1. 在 templates 文件下创建 html 文件,模拟秒杀页面
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>秒杀活动进行中······</h1>
<button id="click">点击我</button>
</body>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>
<script>
    $('#click').click(function () {
        $.ajax({
            url: "/kill/1001"
            , type: "get"
        })
    })
</script>
</html>
  1. 编写 Controller 类,完成调用执行秒杀的服务
@Controller
public class RedisController {

    @Autowired
    public SecondsKillImpl secondsKill;

    @RequestMapping("/")
    public String index() {
        return "index";
    }

    @ResponseBody
    @RequestMapping("/kill/{pid}")
    public String secondsKill(@PathVariable String pid, Model model) {
        Random random = new Random();
        // 随机生成用户 id
        int userId = random.nextInt(9999) + 1000;
        // 调用秒杀的服务
        secondsKill.doSecondsKill(String.valueOf(userId), pid);
        return "index";
    }
}
  1. 秒杀实现类
/**
 * 秒杀实现
 */
@Service
public class SecondsKillImpl {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // 秒杀过程
    public boolean doSecondsKill(String uid, String proid) {
        //3. 拼接 key
        // 3.1 库存key
        String kcKey = "sk:" + proid + ":kc";
        // 3.2 秒杀成功用户 key
        String userKey = "sk:" + proid + ":user";

        //4. 获取库存,如果库存 null,秒杀还没有开始
        Object kc = redisTemplate.opsForValue().get(kcKey);
        if (kc == null) {
            System.out.println("秒杀还没有开始,请稍等");
            return false;
        }
        //5. 判断是否重复秒杀操作
        Boolean member = redisTemplate.opsForSet().isMember(userKey, uid);
        if (member) {
            // 表示用户已经秒杀成功了,不能重复秒杀
            System.out.println("不能重复秒杀");
            return false;
        }
        //6. 判断如果库存数量<1,表示商品没有了,秒杀结束
        int i = Integer.parseInt(kc.toString());
        System.out.println(i);
        System.out.println("===");
        if (i <= 0) {
            System.out.println("秒杀结束了");
            return false;
        }
        //7. 秒杀过程
        // 7.1 库存-1
        redisTemplate.opsForValue().decrement(kcKey);
        // 7.2 秒杀成功用户添加到秒杀清单中
        redisTemplate.opsForSet().add(userKey, uid);

        System.out.println("秒杀成功");
        return true;
    }
}
  1. 秒杀之前需要在 redis 中加入库存的数量

    set sk:1001:kc 10

    然后运行,点击秒杀

11.2 模拟高并发

在 Centos7 中使用 ab工具 模拟测试

  1. 安装

    yum install htppd-tools

  2. 参数

    在这里插入图片描述

    -n 表示请求次数

    -c 表示并发数量

    -p postfile(将提交参数放到一个 postfile 文件中)

    在这里插入图片描述

    -T 用 post/get提交需要设置类型–application/x-www-form-urlencoded

  3. ab -n 1000 -c 100 -T application/x-www-form-urlencoded http://192.168.43.177:8080/kill/1001

    执行命令后会向接口发送1000个请求,100 个并发请求

  4. 查看秒杀结果

    从结果不难看出,当秒杀结束的时候还有秒杀成功的,并且秒杀成功的用户已经超出了库存的数量,这就是超卖的情况

    在这里插入图片描述

    查看库存情况:库存的数量已经变为了负数

    在这里插入图片描述

11.3 解决超卖情况

在这里插入图片描述

  • 使用乐观锁解决超卖问题

修改上面的代码,不适用 redisTemplates,使用 Jedis

  1. 依赖
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.2.0</version>
</dependency>
<!--spring2.x 继承 redis 所需 common-pool2-->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.11.1</version>
</dependency>
  1. properties 文件同上

  2. RedisConfig 类

@SpringBootConfiguration
@PropertySource(value = {"classpath:application.properties"})
public class RedisConfig {
    @Value("${spring.redis.lettuce.pool.max-active}")
    private Integer maxTotal;
    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private Integer port;

    public JedisPoolConfig jedisPoolConfig(){    //这个是修改redis性能的时候需要的对象
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal(maxTotal);
        return jedisPoolConfig;
    }

    @Bean  //这个注解注入工厂的名称是方法名
    public JedisPool jedisPool(){
        JedisPoolConfig jedisPoolConfig = jedisPoolConfig();
        return new JedisPool(jedisPoolConfig,host,port);
    }
}
  1. Controller 同上
  2. 实现类
/**
 * 秒杀实现
 */
@Service
public class SecondsKillImpl {

    @Autowired
    public JedisPool jedisPool;

    // 秒杀过程
    public boolean doSecondsKill(String uid, String proid) {

        // 连接 redis
        Jedis jedis = jedisPool.getResource();

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

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

        //4. 获取库存,如果库存 null,秒杀还没有开始
        Object kc = jedis.get(kcKey);
        if (kc == null) {
            System.out.println("秒杀还没有开始,请稍等");
            jedis.close();
            return false;
        }
        //5. 判断是否重复秒杀操作
        Boolean sismember = jedis.sismember(userKey, uid);
        if (sismember) {
            // 表示用户已经秒杀成功了,不能重复秒杀
            System.out.println("不能重复秒杀");
            jedis.close();
            return false;
        }
        //6. 判断如果库存数量<1,表示商品没有了,秒杀结束
        int i = Integer.parseInt(kc.toString());
        if (i <= 0) {
            System.out.println("秒杀结束了");
            jedis.close();
            return false;
        }
        // 开启事务
        Transaction multi = jedis.multi();
        // 库存 - 1
        multi.decr(kcKey);
        // 秒杀成功用户添加到秒杀清单中k
        multi.sadd(userKey, uid);
        // 执行
        List<Object> result = multi.exec();
        if (result == null || result.size() == 0) {
            System.out.println("秒杀失败");
            jedis.close();
            return false;
        }
        System.out.println("秒杀成功");
        return true;
    }
}

然后模拟测试不会出现超卖的情况了。

11.4 解决库存遗留问题

由于乐观锁的版本比较的特性,如果仓库中的库存数量较多,抢购用户较多,则会出现库存遗留的问题

加入2000个人抢购,假设第一个人抢购后将数据版本号修改为 1.1,然后其他的用户再去比较版本号的时候就会不一样,所以会出现库存有商品但却有遗留的问题。

11.4.1 LUA 脚本

Lua 是一个小巧的脚本语言,Lua 脚本可以容易的被 C/C++ 代码调用,也可以反过来调用 C/C++ 函数,Lua 并没有提供强大的库,一个完整的 Lua 解释器不过 200k,所以 Lua 不适合作为开发独立应用程序的语言,而是作为嵌入式语言脚本。

很多应用程序、游戏使用 Lua 作为自己的嵌入式脚本语言,以此来实现可配置性、可扩展性。

11.4.2 LUA 脚本在 Redis 中的优势

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

LUA 脚本是类似 redis 事务,有一定的原子性,不会被其他命令插队,可以完成一些 redis 事务性的操作。

但是注意 redis 的 lua 脚本功能,只能在 redis 2.6 以上的版本才能使用。

利用 Lua 脚本淘汰用户,解决超卖问题。

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

在做秒杀操作时,有两步操作,一个是库存减1,一个是用户列表加1,两个操作写到一个脚本中,当lua 脚本交给 redis 的直接执行,中间不能有干预,只有执行完脚本后别人才能干预。

使用lua脚本就相当于是悲观锁

在这里插入图片描述

11.4.3 代码实现

将之前代码中创建商品key和用户key 的操作、判断库存的操作、判断用户是否存在的操作、库存-1的操作、添加用户的操作等加入到 Lua 脚本中。

  • 返回 2 表示当前用户已经秒杀过了
  • 0 表示秒杀结束
  • 1 表示秒杀成功
  • 注意:代码中有错误未修改
@Service
public class SecondsKillByScript {

    @Autowired
    public JedisPool jedisPool;

    private static final org.slf4j.Logger logger = LoggerFactory.getLogger(SecondsKillByScript.class);

    public void doSkillByScript(String uid, String prodid){
        Jedis jedis = jedisPool.getResource();

        String secKillScript ="local userid=KEYS[1];\r\n" +
                "local prodid=KEYS[2];\r\n" +
                "local qtkey='sk:'..prodid..\":kc\";\r\n" +
                "local usersKey='sk:'..prodid..\":user\";\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" ;
    /*    String sha1=  jedis.scriptLoad(secKillScript);
        Object result= jedis.evalsha(sha1, 2, uid,prodid);*/
        Object result = jedis.eval(secKillScript, 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();
    }
}

12. Redis6 持久化

Redis 提供了2 种不同形式的持久化方式(RDB AND AOF)

Redis 是一个基于内存的数据库,它的数据存储再内存中,也可以写到硬盘中,这一过程叫做持久化。

12.1 RDB(Redis DataBase)
12.1.1 RDB 是什么

再指定的时间间隔内将内存中的**数据集快照(当前某个时间点的数据)**写入磁盘,也就是行话讲的 Snapshot 快照,它恢复时是将快照文件直接读到内存中。

12.1.2 备份是如何执行的

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

这种技术叫做写时复制技术

在这里插入图片描述

12.1.3 Fork

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

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

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

12.1.4 RDB 持久化流程

在这里插入图片描述

12.1.5 关于持久化的配置文件内容

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

  • dbfilename dump.rdb 这里是配置 rdb文件,启动后会自动生成名为 dump.rdb 的文件

在这里插入图片描述

  • dir ./ 生成的 dump.rdb 文件的路径

在这里插入图片描述

在这里插入图片描述

  • stop-writes-on-bgsave-error

    当 Redis 无法写入磁盘的话,直接关掉 Redis的写操作,推荐yes

在这里插入图片描述

  • rdbcompression yes

    对于存储到磁盘中的快照,可以设置是否进行压缩存储,yes 是压缩,如果是的话,redis 会采用 LZF 算法进行压缩

    如果你不想消耗 CPU 来进行压缩的话,可以设置为关闭此功能,推荐 yes

在这里插入图片描述

  • rdbchecksum

    检查完整性,意思就是在持久化之前检查一下数据是否有损坏,如果有就不进行持久化,在存储快照后,还可以让 redis 使用 CRC64 算法来进行数据校验,但是这样做会增加大约 百分之10 的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能,推荐 yes。

在这里插入图片描述

  • save

    格式:save 秒钟,写操作次数

    RDB 是整个内存的压缩过的 Snapshot,RDB 的数据结构,可以配置复合的快照触发条件,默认是 1 分钟内改了 1 万次,或 5 分钟内改了 10次,或 15 分钟内改了 1 次。

    假如在 15 分钟内有1 个 key 发生变化,则会同步一次

    假如在 5 分钟内有 10 个 key 发生变化,则会同步一次

    假如在 1 分钟有 1000 个 key 发生变化,则会同步一次

    总的来说就是,key 发生变化的越多,同步的时间也间隔越短

    在这里插入图片描述

  • save VS bgsave

    save:save 时只管保存,其他不管,全部阻塞。手动保存,不建议

    bgsave::Redis 会在后台异步进行快照操作,快照同时还可以响应客户端请求。

    可以通过 lastsave 命令获取最后一次成功执行快照的时间。

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

在这里插入图片描述

12.1.7 缺点
  • Fork 的时候,内存中的数据被克隆了一份,大致 2 倍的膨胀性需要考虑
  • 虽然 Redis 在 fork 时使用了 写时拷贝技术,但是如果数据庞大时还是比较消耗性能
  • 在备份周期在一定间隔时间做一次备份,所以如果 Redis 意外 down 掉的话,就会丢失最后一次快照后的所有修改。
12.1.8 模拟备份恢复过程
  1. 在 20 秒内执行 set k1 v1 创建 3 个以上的数据(因为在配置文件中设置 save )

    下面的 命令执行后,如果留意的话可以看到 /usr/local/bin 下的 dump.rdb 文件大小会有变化的,执行命令 20 秒后数据已经持久化好了

在这里插入图片描述

  1. 上面的命令执行完后 20 后数据已经持久化了,然后 exit 退出客户端,将 /usr/local/bin 目录下的 dump.conf 文件复制一个新的文件,然后将 dump.rdb 文件删除

在这里插入图片描述

  1. 然后停掉 redis 服务器

    执行命令 ps -ef | grep redis 查看服务器的kid,然后使用 kill -9 kid 命令停止服务,然后我们在重启启动 redis 服务器,进入客户端,可以看到数据库中是没有 key 的。

在这里插入图片描述

  1. 恢复数据

    按照上面的操作停掉 redis 的服务,然后执行命令 mv r.rdb dump.rdb 将备份的文件改名为删除的文件名,然后重新启动服务进入客户端,可以看到刚才设置的 key 有回来了。

    注:如果有数据丢失,是在设置 key 的时候第一次 20 秒执行够了3 个命令,然后再一个 20秒没有执行够 3 条命令,所以最后一次保存记录会丢失。

在这里插入图片描述

12.2 AOF(Append Only File)
12.2.1 AOF 是什么

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

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

在这里插入图片描述

12.2.3 AOF 默认不开启

可以再 redis.conf 中配置文件名称,默认为 appendonly.aof

AOF 文件的保存路径,同 RDB 的路径一致

在配置文件中找到 appendonly ,默认是不开启的,需要改为 yes

appendfilename 是生成的文件的名字,可以进行修改,路径与 .rdb 文件差不多

修改配置后需要重启redis服务器(找到进程然后杀掉)

在这里插入图片描述

修改完 appendonly 为 yes 后,重启redis服务器,在文件中可以看到 appendonly.aof 文件

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

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

12.2.5 AOF 启动/修复/恢复
  • AOF 的备份机制和性能虽然和 RDB 不同,但是备份和恢复的操作同 RDB 一样,都是拷贝备份文件,需要恢复时再拷贝到 Redis 工作目录下,启动系统即加载

  • 正常恢复

    修改默认的 appendonly no ,改为 yes

    将有数据的 aof 文件复制一份保存到对应目录

    恢复:重启 redis 然后重新加载

总结:AOF 的备份恢复过程与 RDB 一样,只需要把生成的 appendonly.aof 文件还原回来就可以进行数据的恢复

  • 异常恢复

    修改默认的 appendonly no 改为 yes

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

    备份被写坏的 AOF 文件

    恢复:重启 redis,然后重新加载

  1. 修改redis 生成的 appendonly.aof 文件

在这里插入图片描述

  1. 然后重启 redis 服务器就会发现失败了,因为在启动的时候会读取 aof 的文件,将文件重新加载,但是刚才进行了修改,所以没有办法启动,连接拒绝

在这里插入图片描述

  1. 修复操作

    在 /usr/lcoa/bin 文件夹下执行命令 redis-check-aof --fix appendonly.aof ,然后查看 aof 文件就会发现,刚才的修改没有了,修复成功,重启服务器就可以看到内容了。

在这里插入图片描述

在这里插入图片描述

12.2.6 AOF 同步频率设置
  • appendfsync always

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

  • appendfsync everysec

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

  • appendfsync no

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

12.2.7 Rewrite 压缩
  1. 是什么

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

  2. 重写原理,如何实现重写

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

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

    如果 no-appendfsync-on-rewrite=no,还是会把数据往磁盘里刷,但是遇到重触发机制,何时重写

    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 或 bgrewritedaof 在运行,如果有,则等待命令结束后再继续执行
    2. 主进程 fork 出子进程执行重写操作,保证主进程不会阻塞
    3. 子进程遍历 redis 内存中数据到临时文件,客户端的写请求同时写入 aof_buf 缓冲区和 aof_rewrite_buf 重写缓冲区保证原 AOF 文件完整以及新 AOF 文件生成期间的新的数据修改动作不会丢失
    4. 1 子进程写完新的 AOF 文件后,向主进程发信号,父进程更新统计信息;2 主进程把 aof_rewrite_buf 中的数据写入到新的 AOF 文件。
    5. 使用新的 AOF 文件覆盖旧的 AOF 文件,完成 AOF 重写。
12.2.8 优点
  • 备份机制更稳健,丢失数据概率更低
  • 可读的日志文本,通过操作 AOF 稳健,可以处理误操作。

在这里插入图片描述

12.2.9 缺点
  • 比起 RDB 占用更多的磁盘空间
  • 恢复备份速度要慢
  • 每次读写都同步的话,有一定的性能压力
  • 存在个别 bug,造成恢复不能
12.3 总结
  • AOF 文件时一个只进行追加的日志文件
  • redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写
  • AOF 文件有序地保存了对数据库执行的所有写入操作,这些写入操作以 Redis 协议的格式保存,因此 AOF 文件的内容非常容易被人读懂,对文件进行分析也很轻松
  • 对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积
  • 根据所使用的 fsync 策略,AOF 的速度可能会慢于 RDB
12.3.1 用那个好

官方推荐两个都启用

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

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

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

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

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

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

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

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

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

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

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

  • 性能建议

    因为 RDB 文件只用作后备用途,建议只在 Slave 上持久化 RDB 文件,而且只要 15 分钟备份一次就够了,只保留 save 900 1 这条规则。

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

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

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

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

13. Redis6 的主从复制

13.1 主从复制是什么?

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1bA6oQvt-1633405101656)(D:\notes\Redis\Redis基础知识点总结.assets\image-20210918150410319.png)]

13.2 能干啥?
  • 读写分离,性能扩展

假如只有一台 Redis 服务器,然后读和写都在一台服务器中进行,服务器的压力会很大

上图,写操作都在 主服务器中进行,执行完后再复制到从服务器中,再从服务器中专门做读操作,这样可以分担服务器压力。

  • 容灾快速恢复

上图中,当进行读操作的时候,假如第一个服务器挂掉了,根据一定的策略去第二台服务器中进行读操作,假如第二台服务器也挂掉了,根据策略依次进行下一个服务器的读操作,以此类推。再配置服务器中,都是一主多从的效果。在主从复制中是只能有一台主服务器的,不能有多个主服务器。

为了防止主服务器挂掉,一般采用集群的方式,关联两个及以上的主从结构的服务器。

13.3 配置一主两从
  1. 创建一个 myredis 的文件在根目录下

    mkdir myredis

  2. 复制 redis.conf 到 myredis 文件夹中

    进入 myredis

    cp /etc/redis.conf /myredis

  3. 不能都使用一个端口号,配置一主两从的结果,创建 3 个配置文件

    redis6379.conf

    redis6380.conf

    redis6381.conf

    在这里插入图片描述

  4. 在 3 个配置文件中写入内容

    在 3 个文件中分别写入以下内容

    include/myredis/redis.conf

    pidfile /var/run/redis_6379.pid

    port 6379

    dbfilename dump6379.rdb

    在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述

  5. 启动 3 个服务器

    在这里插入图片描述

    启动后可以通过命令 ps -ef | grep redis 命令查看 3 个进程是否启动成功

    在这里插入图片描述

  6. 打开 3 个终端

    分别使用命令 redis-cli -p 端口 启动客户端

    然后执行命令 info replication,3个主机结果与下图一样,都是 master,slaves 为 0(没有从机)

    在这里插入图片描述

  7. 在从机上执行命令 slaveof master-ip port

    设端口号为 6380 和 6381 的服务器为从机,分别执行完以上命令后使用 info replication分别查看信息的变化

    • 主机,角色为 master,从机个数 2

    在这里插入图片描述

    • 从机,角色为 slave,两台从机一样

    在这里插入图片描述

  8. 测试

    在 主机 中设置值,可以查看

    在这里插入图片描述

    在 从机 中可以查看主机加入的值,但是不能写入,或报错 READONLY You can’t write against a read only replica.

    在这里插入图片描述

  9. 设置主从结构永久生效

    主机挂掉,重启就可以了

    从机重启需要重新设置:slaveof 127.0.0.1 6379,否则从服务器的 角色变为 master

    可以将配置增加到文件中,永久生效

13.4 常用 3 招
13.4.1 一主二仆
  • 从服务器挂掉

在主服务器增加值的时候,其中一台从服务器挂掉了,然后主服务器又新增加了几个值。然后将挂掉的服务器重启后角色变为 master,需要重新执行命令slaveof 127.0.0.1 6379设置为从服务器,然后从服务器会将主服务器的所有添加的值重新复制一下。

在这里插入图片描述

  • 主服务器挂掉

当主服务器挂掉后从服务器不会发生任何变化,主服务器重启后从服务器还是会认主服务器

在这里插入图片描述

13.4.2 薪火相传

主服务器将数据信息同步到一台从服务器上,从服务器又将数据同步到另一台服务器上,这个过程称为薪火相传。但是当主服务器同步到从服务器后,从服务器挂掉了,后面的服务器就不能进行信息的同步了。

特点:与一主二仆类似,主服务器挂掉重启还是主服务器,从服务器挂掉需要重新定义为从服务器

在这里插入图片描述

在这里插入图片描述

13.4.3 反客为主

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

当主服务器挂掉之后,执行命令 slaveof no one 将从机变为主机

在这里插入图片描述

主服务器在线上运行的过程成挂掉了,而运营人员不能及时发现会丢失数据的,所以需要让从服务器在主服务器挂掉的时候自动升为主服务器,需要用到 哨兵模式

13.5 复制原理
  1. slave从服务器启动成功连接到 master 后会发送一个 sync 要进行数据同步的消息命令,告诉主服务器现在要做数据同步了

  2. master 接到从服务器发来的同步消息命令后,启动后台的存盘进程,把主服务器数据进行持久化 rdb 文件,然后把 rdb 文件发送给从服务器,从服务器拿到 rdb 文件进行读取。(收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,mster 将传送整个数据文件到 slave,以完成一次完全同步)

  3. 每次主服务器进行写操作后,和从服务器进行数据同步,(这次是主服务器主动发起的,上面是从服务器发起的同步。)

  • 全量复制:slave 服务在接收到数据库文件数据后,将其存盘并加载到内存中
  • 增量复制:master 继续将新的所有收集到的修改命令依次传给 slave,完全同步
  • 但是只要是重新连接 master ,一次完全同步(全量复制)将被自动执行。
13.6 哨兵模式
13.6.1 是什么?

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

就是说给从机给主机派一个哨兵时刻监控主机的运行状态,如果主机挂掉了则立刻通知从机,让从由从机转换为主机。

在这里插入图片描述

13.6.2 使用步骤,模拟切换
  1. 首先还是启动3 个服务器,构建成 一主二仆 的模式

    在这里插入图片描述

  2. 在存储配置文件的目录 myredis 下创建 sentinel.conf 文件,名字不能有错。

  3. 配置哨兵,填写以下内容

    sentinel monitor mymaster 127.0.0.1 6379 1

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

    1 的解释:就是说有3 台从机,设为 1 表示当主机挂掉以后只要有 1 台从机说可以切换就可以切换,假设为 2 表示主机挂掉后需要有 2 台从机同意切换才可以切换。

  4. 启动哨兵

    执行命令 redis-sentinel sentinel.conf

    在这里插入图片描述

  5. 当主机挂掉,从机选举产生出新的主机

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

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

    原主机重启后会变为 从机

    在这里插入图片描述

    挂掉的主机重启后会变为从机状态

    在这里插入图片描述

13.6.3 主从复制缺陷–复制延时

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

13.6.4 故障恢复

在这里插入图片描述

优先级在 redis.conf 中默认:replica-priority 100,值越小优先级越高

在这里插入图片描述

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

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

14. Redis6 集群

14.1 问题

容量不够,redis 如何进行扩容?

并发写操作,redis 如何分摊?

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

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

解决以上问题的方式

  • 代理主机模式

在这里插入图片描述

当客户端请求包含用户就通过代理服务器访问用户服务器

当客户端请求包含订单就通过代理服务器访问订单服务器

当客户端请求包含商品就通过代理服务器访问商品服务器

缺点:为了防止各个服务器挂掉都需要加上从机,这样的话可以保证正常运行,但是需要多台服务器

  • 无中心化集群

任何一个服务器都可以作为集群的入口

在这里插入图片描述

14.2 什么是集群

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

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

14.3 搭建集群
  1. 进入 /myredis 文件夹,将 rdb、aof 文件都删除掉

  2. 创建6 个 redis端口号.conf 文件,并写入内容

    • redis6379.conf redis6380.conf
    • redis6381.conf redis6389.conf
    • redis6390.con fredis6391.conf

    include /myredis/redis.conf

    pidfile "/var/run/redis_端口号.pid"

    port 端口号

    dbfilename "dump端口号.rdb"

  3. redis cluster 配置修改

    cluster-enabled yes 打开集群模式

    cluster-config-file nodes-端口号.conf 设定节点配置文件名

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

    在这里插入图片描述

  4. 启动 redis 的6个服务

    在这里插入图片描述

    启动节点后每个节点的配置文件也生成出来了

    在这里插入图片描述

  5. 将 6 个节点合并成一个集群

    组合之前,确保所有 redis 实例启动,nodes-xxxx.conf 文件都生成正常。

    进入 redis 安装目录的 src 目录下

    在这里插入图片描述

    然后在 src 目录下执行命令 redis-cli --cluster create --cluster-replicas 1 192.168.43.53:6379 192.168.43.53:6380 192.168.43.53:6381 192.168.43.53:6389 192.168.43.53:6390 192.168.43.53:6391

    • 注意,这里不能用 127.0.0.1,要用真实的 ip 地址
    • redis-cli --cluster 表示集群操作
    • –cluster-replicas 1 表示采用最简单的方式配置集群,一台主机,一台从机,正好 3 组

    在这里插入图片描述

  6. 测试集群,使用命令 redis-cli -c -p 6379 ,连接的时候用任何一个端口号都是可以的,-c 采用集群策略连接,设置数据会自动切换到相应的写主机

14.4 查看集群信息

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

执行上面的命令之后可以清晰的看到是以那个端口启动的,主机和从机分别是什么,分别对应的主从机结构。

在这里插入图片描述

14.5 redis cluster 如何分配 6 个节点?

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

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

分配原则尽量保证每个主数据库运行在不同的 ip 地址,每个从库和主库不在一个 ip 地址上。就是每个库包括主库和从库的每一个库都放在一个服务器系统中,防止某一个服务器系统中断连同这其他的从库一块挂掉。

14.6 什么是 slots

slots(插槽)主要只有一个目的,就是把设置的值平均分摊到不同的集群的主机中,让主机平均分担压力。

在这里插入图片描述

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

集群中的每个节点负责处理一部分插槽。举个例子,如果一个集群可以有主节点,

其中:

节点 A 负责处理 0 号至5460 号插槽

节点 B 负责处理 5460号至 10922 号插槽

节点 C 负责处理 10923 号至 16383 号插槽

在这里插入图片描述

查看集群中的插槽信息,执行命令 cluster nodes

在这里插入图片描述

14.7 向集群中添加数据
14.7.1 使用 set 添加一个 key

在使用set命令保存值的时候,会计算相对应的插槽值,然后根据插槽值进入相对应的主机进行操作。

在这里插入图片描述

14.7.2 使用 mset 添加多个值会报错

当我们想向集群中使用 mset 添加多个值的时候会报错误:CROSSSLOT Keys in request don't hash to the same slot,表示添加多个值不能够计算出相对应的插槽的值

在这里插入图片描述

解决方法:将要添加的值设置为组的形式进行添加,通过组的 key 来进行插槽

在这里插入图片描述

14.7.3 查询集权中的值

cluster keyslot key 返回 key 对应的插槽的值。

cluster countkeysinslot <slot> 计算插槽值中有几个 key

cluster getkeysinslot <slot><count> 返回插槽中count 数量的slot 插槽中的键。

在这里插入图片描述

14.8 故障恢复

如果主节点下线?从节点能否自动升为主节点

主节点恢复后,主从关系怎么变化?主节点回来变成从机

如果所有某一段插槽的主从节点都宕掉,redis 服务器是否还能继续?

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

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

​ redis.conf 中的参数 cluster-require-full-coverage

在这里插入图片描述

14.9 集群的 Jedis 开发

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

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

/**
 * Jedis 操作集群
 */
public class RedisClusterDemo {

    public static void main(String[] args) {
        // 创建对象
        HostAndPort hostAndPort = new HostAndPort("192.168.43.53",6379);
        JedisCluster jedisCluster = new JedisCluster(hostAndPort);
        // 操作
        jedisCluster.set("name","zhangsan");

        String name = jedisCluster.get("name");
        System.out.println(name);
        // 关闭连接
        jedisCluster.close();
    }

}
14.10 Redis 集群好处
  • 实现扩容
  • 分摊压力
  • 无中心化配置相对简单
14.11 Redis 集权不足

多键操作是不被支持的

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

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

15. Redis6 应用问题解决

15.1 缓存穿透
15.1.1 问题描述

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

解释:

  1. 应用服务器压力突然变大
  2. redis 命中率降低
  3. 一直查询数据库

应用服务器压力突然变大,用户通过web浏览器访问服务器(应用服务器),正常情况下会先查询缓存,查不到(redis命中率降低去查询数据库),在数据库中查到数据再放到缓存中,当缓存中大部分数据都不存在,所有数据都去查询数据库,数据库最终承受不住访问的压力就会崩溃。

这个时候 redis 内部一直是平稳运行的,但缓存没有起到作用,而是一直去访问数据库,这个情况就是缓存穿透。

产生缓存穿透原因:

  1. redis 查询不到数据,大部分未命中
  2. 操作中出现很多非正常的 url 访问(假如一些恶意攻击),目的不是得到数据,而是破坏数据库。

在这里插入图片描述

15.1.2 解决方案

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

解决方案:

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

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

  3. 采用布隆过滤器:(布隆过滤器(Bloom Filter))是1970 年由布隆提出的。它实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数)。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。

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

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

15.2 缓存击穿
15.2.1 问题描述

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

解释

  1. 数据库访问压力瞬时增加
  2. redis 里面没有出现大量 key 过期
  3. redis 正常运行,压力并没有变大,但是数据库已经崩溃了。

原因

​ redis 中某一个key 过期了,但是这个时候有大量的请求访问中都用到了这个已经过期的key,所以出现缓存击穿的问题。

在这里插入图片描述

15.2.2 解决方案

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

解决问题:

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

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

  3. 使用锁:

    • 在缓存失效的时候(判断拿出的值是否为空),不是立即去 load db
    • 先使用缓存工具的某些带成功操作返回值的操作(比如 Redis 的 setnx)去 set 一个 mutex key
    • 当操作返回成功时,再进行 load db 的操作,并回设缓存,最后删除 mutex key;
    • 当操作返回失败,证明有线程在 load db,当线程睡眠一段时间再重试整个 get 缓存的方法

    在这里插入图片描述

15.3 雪崩问题
15.3.1 问题描述

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

​ 缓存雪崩与缓存击穿的区别在于这里针对很多 key 缓存,前者则是某一个 key 正常访问。

解释

​ 数据库压力变大,应用程序就变慢了,redis 会有大量的访问等待,最终造成数据库崩溃了,应用崩溃了,redis 也崩溃了,就算重启效果也不会很明显

造成雪崩的原因:在极少的时间段内,查询大量key 的集中过期的情况,造成雪崩问题,当key 过期后缓存访问不到就会到数据库,大量的访问就会击垮数据库,造成服务器压力过大至崩溃。

在这里插入图片描述

15.3.2 解决方案

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

解决方案:

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

  2. 使用锁或队列:

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

  3. 设置过期表示更新缓存

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

  4. 将缓存失效时间分散开

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

15.4 分布式锁
15.4.1 问题描述

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

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

  1. 基于数据库实现分布式锁
  2. 基于缓存 Redis 等
  3. 基于 Zookeeper

每一种分布式锁解决方案都各有各的优缺点:

  1. 性能:redis 最高
  2. 可靠性:zookeeper 最高

下面使用中基于 redis 实现分布式锁

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

在redis 的设置值的命令中有一个 setnx key vlaue,这个命令就是针对分布式锁操作的命令,setnx 设置锁除了第一次设置之外不能进行重新赋值,要想设置锁就需要将其删除重新执行 setnx 命令。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

这样设置锁的时候如果没有删除掉就会一直占用这个key,别的想要使用这个key 的操作就必须等待其释放

解决这种情况可以在设置key 的时候设置过期时间,在规定的时间后进行删除key,不让他长时间占用key 影响后面的操作,解决锁不释放的问题。

  1. 使用 setnx 上锁,通过 del 释放锁

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

  3. 上锁之后突然出现异常,无法设置过期时间了,解决:

    上锁的时候同时设置过期时间

    set key value nx ex 10 即上锁又设置了过期时间

setnx key value 对 key 上锁

expire key 10 执行命令给key 设置过期时间 10秒

ttl key 可以查看还有多长时间

15.4.3 代码实现
@GetMapping("/testLock")
public void testLock() {
    // 1. 获取锁 setnx
    Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111");
    // 2. 获取锁成功,查询 num 的值
    if (lock) {
        Object value = redisTemplate.opsForValue().get("num");
        System.out.println(value);
        // 2.1 判断 num 为空返回 return
        if (value == null) {
            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);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

对于上面的代码,如果上锁后中间出现了问题就不能释放锁了,可以给锁设置一个过期时间

下面代码中将锁设置为 3 秒以后过期。

Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111",3, TimeUnit.SECONDS);
15.4.4 锁误删

锁误删的场景:假设设置锁的过期时间为 10 秒,有3 个访问,a、b、c。然后a 请求首先抢到锁,然后1. 上锁,2. 具体操作,操作的时候服务器突然卡顿,然后 b 就抢到锁,b 进行具体操作,然后b 正在操作的时候 a 反应过来了继续进行操作,但此时如果 a 执行完毕,b 也执行完毕,那这个锁应该由谁进行释放?锁是 a 抢到的,a 要释放,b 也抢到了锁并且也进行了具体操作,b 也要释放,所以会产生 二义性。

在这里插入图片描述

15.4.5 使用 UUID 防误删

在上锁的时候我们为每一个操作生成一个唯一的 uuid 的值,每台服务器都有自己唯一的值

  1. 通过 uuid 标识不同的服务器请求
  2. 释放锁的时候,首先判断当前的 uuid 和要释放锁的 uuid 是否一样,一样释放,否则不释放
  3. 我的锁只能我自己释放,我也不会释放别人的锁

在这里插入图片描述

在这里插入图片描述

public void testLock() {
    
    // 生成一个访问的 uuid 作为唯一的锁
    String uuid = UUID.randomUUID().toString();
    // 1. 获取锁 setnx
    Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,3, TimeUnit.SECONDS);
    // 2. 获取锁成功,查询 num 的值
    if (lock) {
        Object value = redisTemplate.opsForValue().get("num");
        System.out.println(value);
        // 2.1 判断 num 为空返回 return
        if (value == null) {
            return;
        }
        // 2.2 有值就转换成  int
        int num = Integer.parseInt(value + "");
        // 2.3 把 redis 的 num 加 1
        redisTemplate.opsForValue().set("num", ++num);
        // 2.4 释放锁 del
        // 判断 uuid 值是否一样
        String lockUUid = (String) redisTemplate.opsForValue().get("lock");
        if (lockUUid.equals(uuid)) {
            redisTemplate.delete("lock");
        }
    } else {
        // 3. 获取锁失败,每个 0.1 秒再获取
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
15.4.6 删除操作缺乏原子性

在执行操作的时候缺乏原子性,上锁之后再解锁,解锁之后别人才能进行正常操作,但是目前没有原子性,会产生一些问题。

问题描述:两个操作 a,b,当 a 操作的时候 1.上锁,2. 具体操作,3. 释放锁。在释放锁的时候使用 uuid 比较,是相同的进行删除操作,正在删除还没有删除的时候,碰巧这个锁到了过期时间了,锁会自动释放,当锁释放时 b 获取到了这个锁,然后 b 进行具体操作,这是 a 还没有结束,仍然进行删除操作,a 就释放掉了 b 的锁。这个问题就是因为目前操作中没有原子性造成的,在操作过程中会互相干扰。

在这里插入图片描述

15.4.7 使用 LUA 脚本解决缺乏原子性问题
@GetMapping("/testLock")
public void testLockLua() {
    // 1. 声明一个 uuid ,将作为一个 value放入我们的 key 所对应的值中
    String uuid = UUID.randomUUID().toString();
    // 2. 定义一个锁:lua 脚本可以使用同一把锁,来实现删除
    String skuId = "25";    // 访问 skuId 为 25 号商品 100000008888839
    String locKey = "lock:" + skuId;    // 锁住的是每个商品的数据
    // 3. 获取锁 setnx
    Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 3, TimeUnit.SECONDS);
    // 第一种:lock 与过期时间中间不写任何代码
    if (lock) {
        // 执行的业务逻辑开始
        // 获取缓存中的 num 数据
        Object value = redisTemplate.opsForValue().get("num");
        // 判断 num 为空返回 return
        if (value == null) {
            return;
        }
        // 不是空 如果说在这里出现了异常,那么 delete 就删除失败,也就是说永远存在
        int num = Integer.parseInt(value + "");
        // 使 num 每次 +1 放入缓存
        redisTemplate.opsForValue().set("num", ++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(100);
            // 睡醒之后,调用方法
            testLockLua();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
15.4.8 总结

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

  • 互斥性:在任意时刻,只有一个客户端能持有锁
  • 不会发生死锁:即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
  • 解铃还须系铃人:加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解开了
  • 加锁和解锁必须具有原子性

16. Redis6 新功能

16.1 ACL
16.1.1 简介

​ Redis ACL 是 Access Control List(访问控制列表)的缩写,该功能允许根据可以执行的命令和可以访问的键来限制某些连接

​ 在 Redis 5 版本之前,Redis 安全规则只有密码控制还有通过 rename 来调整高危命令比如 flushdb,keys *,shutdown 等。Redis 6 则提供 ACL 的功能对用户进行更细粒度的权限控制:

  1. 接入权限:用户名和密码
  2. 可以执行的命令
  3. 可以操作的 key

参考官网:https://redis.io/topics/acl

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

    数据说明

    在这里插入图片描述

  2. 使用 acl cat 命令

    • 查看添加权限指令类别(具体的命令有那些)

    在这里插入图片描述

    • acl cat string

      在这里插入图片描述

  3. 使用 acl whoami 命令查看当前用户

    在这里插入图片描述

  4. 使用 aclsetuser 命令创建和编辑用户 ACL

    • ACL 规则

    下面是有效 ACL 规则的列表。某些规则只是用于激活或删除标志,或对用户 ACL 执行给定更改的单个单词。其他规则是字符前缀,它们与命令或类别名称、键模式等连接在一起。

    在这里插入图片描述

    在这里插入图片描述

    • 通过命令创建新用户默认权限

      acl setuser user1

      在这里插入图片描述

      在上面的示例中,我根本没有指定任何规则。如果用户不存在,这将使用 just created 的默认属性来创建用户。如果用户已经存在,则上面的命令将不执行任何操作。

    • 设置有用户名、密码、ACL权限、并启用的用户

      acl setuser user2 on >password ~cached:* +get

      +get 表示这个用户只能使用 get 命令

      在这里插入图片描述

    • 切换用户,验证权限

      在这里插入图片描述

16.2 IO 多线程
16.2.1 简介

​ Redis 6 终于支持多线程了,告别单线程了吗?

​ IO 多线程其实指客户端交互部分的网络 IO 交互处理模块多线程,而非执行命令多线程。Redis 6 执行命令依然是单线程。

16.2.2 原理架构

​ Redis 6 加入多线程,但是 Memcached 这种从 IO 处理到数据访问多线程的实现模式有些差异。Redis 多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程。之所以这么设计是不想因为多线程而变得复杂,需要去控制 key、lua、事务,lpush/lpop 等等的并发问题。

多线程 IO 默认也是不开启的,需要再配置文件中配置

io-threads-do-reads yes

io-threads 4

在这里插入图片描述

16.3 工具支持 Cluster

​ 之前老版本 Redis 想要搭建集群需要单独安装 ruby 环境,Redis 5 将 redis-trib.rb 的功能集成到 redis-cli。另外官方 redis-benchmark 工具开始支持 cluster 模式了,通过多线程的方式对多个分片进行压测。

命令:redis-benchmark --help查看帮助

在这里插入图片描述

16.4 Redis 新功能持续更新

​ Redis 6 新功能还有:

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

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

  3. Proxy 集群代理模式:Proxy 功能,让 Cluster 拥有像单例一样的接入方式,降低大家使用 cluster 的门槛。不过需要注意的是代理不改变 Cluster 的功能限制,不支持的命令还是不会支持,比如跨 slot 的多个key 操作

  4. Modules API

    Redis 6 中模块 API 开发进展非常大,因为 Redis Labs 为了开发复杂的功能,从一开始就用上了 Redis 模块。Redis 可以变成一个框架,利用 Modules 来构建不同系统,而不需要从头开始写然后还要 BSD 许可。Redis 一开始就是一个向编写各种系统开放的平台。

視頻學習地址: https://www.bilibili.com/video/BV1Rv41177Af?p=1

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值