自学内容网 自学内容网

Redis数据库

Redis数据库

在这里插入图片描述

第一章 Redis简介

Redis是一个基于 C 语言开发的开源数据库(BSD 许可),存储Key-Value 键值对数据,与传统数据库不同的是 Redis 的数据是存储在内存中的(内存数据库),读写速度非常快,被广泛应用于缓存方向。

Redis 内置了多种数据类型实现(比如 String、Hash、Sorted Set、Bitmap、HyperLogLog、GEO)。

Redis 支持事务(不推荐使用)、持久化、Lua 脚本、多种开箱即用的集群方案。

Redis VS Memcache

Memcache
  • 很早出现的NoSql数据库
  • 数据存储在内存中,数据不能持久化
  • 支持简单的key-value模式,数据类型单一
  • 一般是作为缓存数据库辅助持久化的数据库
redis
  • 几乎覆盖了Memcached的绝大部分功能

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

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

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

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

  • 原子操作 。要么成功,要么失败

  • 支持各种不同方式的排序

  • Redis读取的速度是110000次/s,写的速度是81000次/s;

    一般像 MySQL 这类的数据库的 QPS 大概在 4k 左右(4 核 8g) ,但是使用 Redis 缓存之后很容易达到 5w+,甚至能达到 10w+

    QPS(Query Per Second):服务器每秒可以执行的查询次数;

  • 支持master-slave(主从)同步

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

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

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

Why is Redis so fast?

在这里插入图片描述

(1)基于内存

Redis 是一种基于内存的数据库,数据存储在内存中,数据的读写速度非常快,因为内存访问速度比硬盘访问速度快得多。

(2)单线程模型

Redis 使用单线程模型,所有操作都是在一个线程内完成的,不需要进行线程切换和上下文切换,大大提高了 Redis 的运行效率和响应速度。

(3)多路复用 I/0 模型

Redis 在单线程的基础上,采用了I/0 多路复用技术,实现了单个线程同时处理多个客户端连接的能力,从而提高了 Redis 的并发性能。

(4)高效的数据结构

Redis 提供了多种高效的数据结构,如哈希表、有序集合、列表等,这些数据结构都被实现得非常高效,能够在 O(1)的时间复杂度内完成数据读写操作。

应用场景

(1)配合关系型数据库做高速缓存

  • 高频次,热门访问的数据,降低数据库IO

    一般来讲,把经常进行查询,不经常修改,不是特别重要的数据放到redis作为缓存。

    比如首页数据,由于首页数据变化不是很频繁,而且首页访问量相对较大,所以可以把首页接口数据缓存到redis缓存中,减少数据库压力和提高访问速度。

  • 分布式架构,做session共享
    在这里插入图片描述
    (2)多样的数据结构存储持久化数据
    在这里插入图片描述
    (3)分布式锁。通常情况下,基于 Redisson 来实现分布式锁。

Redis安装与启动

官网:http://redis.io

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

redis安装,参考教程:https://www.runoob.com/redis/redis-install.html

【第一步】启动redis服务器,使用 cd 命令切换目录到redis目录中

#redis.windows.conf可以省略,如果省略,会启用默认的。
redis-server.exe redis.windows.conf

tips:可以把 redis 的路径加到系统的环境变量

问题:输入以上命令报下面错,可以直接输入命令启动客户端

 Creating Server TCP listening socket 127.0.0.1:6379: bind: No error

【第二步】启动客户端,在redis目录下另启一个 cmd 窗口,原来的不要关闭,不然就无法访问服务端了 可选

redis-cli.exe -h 127.0.0.1 -p 6379

Redis基本命令

1)检测 redis 服务是否启动

PING

在这里插入图片描述

2)关闭当前连接

QUIT
shutdown

3)获取 redis 服务器的信息

INFO

4)清空当前库

flushdb

5)通杀全部库

flushall

6)查看当前数据库的key的数量

dbsize

7)切换数据库

 select   <dbid>

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

Redis键(key)

redis常见数据类型操作命令:http://www.redis.cn/commands.html

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

第二章 Redis数据类型

Redis 共有 5 种基本数据结构:String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)。这 5 种数据结构是直接提供给用户使用的,是数据的保存形式,其底层实现主要依赖这 8 种数据结构:简单动态字符串(SDS)、LinkedList(双向链表)、Dict(哈希表/字典)、SkipList(跳跃表)、Intset(整数集合)、ZipList(压缩列表)、QuickList(快速列表)

Redis 基本数据结构的底层数据结构实现如下:

StringListHashSetZset
SDSLinkedList/ZipList/QuickListDict、ZipListDict、IntsetZipList、SkipList

Redis 3.2 之前,List 底层实现是 LinkedList 或者 ZipList。 Redis 3.2 之后,引入了 LinkedList 和 ZipList 的结合 QuickList,List 的底层实现变为 QuickList。从 Redis 7.0 开始, ZipList 被 ListPack 取代。

在 Redis 官网上找到 Redis 数据结构非常详细的介绍:

Redis字符串(String)

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

String类型是二进制安全的,意味着Redis的string可以包含任何数据,比如字符串、整数、浮点数、图片(图片的 base64 编码或者解码或者图片的路径)、序列化后的对象。

String的数据结构为简单动态字符串(Simple Dynamic String,缩写SDS),是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配.
在这里插入图片描述
如图中所示,内部为当前字符串实际分配的空间capacity一般要高于实际字符串长度len,当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次只会多扩1M的空间。

常用命令
命令介绍
SET key value设置指定 key 的值
SETNX key value只有在 key 不存在时,设置 key 的值
SETEX key 过期时间 value设置键值的同时,设置过期时间,单位秒。
GET key获取指定 key 的值
MSET key1 value1 [key2 value2 …]设置一个或多个指定 key 的值
MGET key1 [key2 …]获取一个或多个指定 key 的值
STRLEN key返回 key 所储存的字符串值的长度
INCR key [步长]将 key 中储存的数字值+步长,默认为1
DECR key [步长]将 key 中储存的数字值-步长,默认为1
EXISTS key判断指定 key 是否存在
DEL key(通用)删除指定的 key
EXPIRE key seconds(通用)给指定 key 设置过期时间

更多 Redis String 命令以及详细使用指南,请查看 Redis 官网对应的介绍:https://redis.io/commands/?group=stringopen in new window

应用场景

(1)需要存储常规数据的场景

  • 举例:缓存 session、token、图片地址、序列化后的对象(相比较于 Hash 存储更节省内存)。

(2)需要计数的场景

  • 举例:用户单位时间的请求数(简单限流可以用到)、页面单位时间的访问数。
  • 相关命令:SETGETINCRDECR

(3)分布式锁

利用 SETNX key value 命令可以实现一个最简易的分布式锁(存在一些缺陷,通常不建议这样实现分布式锁)

Redis列表(List)

单键多值

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

底层实际是个双向链表,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差。
在这里插入图片描述

常用命令
命令介绍
RPUSH key value1 value2 …在指定列表的尾部(右边)添加一个或多个元素
LPUSH key value1 value2 …在指定列表的头部(左边)添加一个或多个元素
LSET key index value将指定列表索引 index 位置的值设置为 value
LPOP key移除并获取指定列表的第一个元素(最左边)
RPOP key移除并获取指定列表的最后一个元素(最右边)
LLEN key获取列表元素数量
LRANGE key start end获取列表 start 和 end 之间 的元素

更多 Redis List 命令以及详细使用指南,请查看 Redis 官网对应的介绍:https://redis.io/commands/?group=listopen in new window

补充:

通过 RPUSH/LPOP 或者 LPUSH/RPOP实现队列

通过 RPUSH/RPOP或者LPUSH/LPOP 实现栈

数据结构

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

1)列表元素较少的情况下会使用一块连续的内存存储,这个结构是ziplist,即压缩列表,它将所有的元素紧挨着一起存储,分配的是一块连续的内存。

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

普通的链表需要的附加指针空间太大,会比较浪费空间,比如这个列表里存的只是int类型的数据,结构上还需要两个额外的指针prev和next。
在这里插入图片描述
Redis将链表和ziplist结合起来组成了quicklist,即将多个ziplist使用双向指针串起来使用,这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。

应用场景

(1)信息流展示

  • 举例:最新文章、最新动态。
  • 相关命令:LPUSHLRANGE

(2)消息队列

Redis List 数据结构可以用来做消息队列,只是功能过于简单且存在很多缺陷,不建议这样做。相对来说,Redis 5.0 新增加的一个数据结构 Stream 更适合做消息队列一些,只是功能依然非常简陋。和专业的消息队列相比,还是有很多欠缺的地方比如消息丢失和堆积问题不好解决。

Redis集合(Set)

Redis set存储不重复、无序的集合,底层是一个value为null的hash表,添加,删除,查找的复杂度都是O(1)

常用命令
命令介绍
SADD key member1 member2 …向指定集合添加一个或多个元素
SMEMBERS key获取指定集合中的所有元素
SCARD key获取指定集合的元素数量
SISMEMBER key member判断指定元素是否在指定集合中
SINTER key1 key2 …获取给定所有集合的交集
SINTERSTORE destination key1 key2 …将给定所有集合的交集存储在 destination 中
SUNION key1 key2 …获取给定所有集合的并集
SUNIONSTORE destination key1 key2 …将给定所有集合的并集存储在 destination 中
SDIFF key1 key2 …获取给定所有集合的差集
SDIFFSTORE destination key1 key2 …将给定所有集合的差集存储在 destination 中
SPOP key count随机移除并获取指定集合中一个或多个元素
SRANDMEMBER key count随机获取指定集合中指定数量的元素

更多 Redis Set 命令以及详细使用指南,请查看 Redis 官网对应的介绍:https://redis.io/commands/?group=setopen in new window

数据结构

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

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

应用场景

(1)需要存放的数据不能重复的场景

  • 举例:网站 UV 统计(数据量巨大的场景还是 HyperLogLog更适合一些)、文章点赞、动态点赞等场景。

    UV:全称Unique Visitor,也叫独立访客量,是指通过互联网访问、浏览这个网页的自然人。1天内同一个用户多次访问该网站,只记录1次。
    PV:全称Page View,也叫页面访问量或点击量,用户每访问网站的一个页面,记录1次PV,用户多次打开页面,则记录多次PV。往往用来衡量网站的流量。

(2)需要获取多个数据源交集、并集和差集的场景

  • 举例:共同好友(交集)、共同粉丝(交集)、共同关注(交集)、好友推荐(差集)、音乐推荐(差集)、订阅号推荐(差集+交集) 等场景。

(3)需要随机获取数据源中的元素的场景

  • 举例:抽奖系统、随机点名等场景。

Redis哈希(Hash)

Redis hash 是一个键值对集合。

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

例如:用户ID为查找的key,存储的value用户对象包含姓名,年龄,生日等信息,如果用普通的key/value结构来存储,主要有以下2种存储方式:
在这里插入图片描述
最佳方法:通过 key(用户ID) + field(属性标签) 就可以操作对应属性数据了,既不需要重复存储数据,也不会带来序列化和并发修改控制的问题。

常用命令
命令介绍
HSET key field value设置指定哈希表中指定字段的值
HSETNX key field value只有指定字段不存在时设置指定字段的值
HMSET key field1 value1 field2 value2 …同时将一个或多个 field-value (域-值)对设置到指定哈希表中
HGET key field获取指定哈希表中指定字段的值
HMGET key field1 field2 …获取指定哈希表中一个或者多个指定字段的值
HGETALL key获取指定哈希表中所有的键值对
HEXISTS key field查看指定哈希表中指定的字段是否存在
HDEL key field1 field2 …删除一个或多个哈希表字段
HLEN key获取指定哈希表中字段的数量
HINCRBY key field increment对指定哈希中的指定字段做运算操作(正数为加,负数为减)

更多 Redis Hash 命令以及详细使用指南,请查看 Redis 官网对应的介绍:https://redis.io/commands/?group=hashopen in new window

数据结构

Hash类型对应的数据结构是两种:ziplist(压缩列表),hashtable(哈希表)

当field-value长度较短且个数较少时,使用ziplist,否则使用hashtable。

应用场景

对象数据存储场景

举例:用户信息、商品信息、文章信息、购物车信息。

Redis有序集合Zset(sorted set)

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

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

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

常用命令
命令介绍
ZADD key score1 member1 score2 member2 …向指定有序集合添加一个或多个元素
ZCARD KEY获取指定有序集合的元素数量
ZSCORE key member获取指定有序集合中指定元素的 score 值
ZINTERSTORE destination numkeys key1 key2 …将给定所有有序集合的交集存储在 destination 中,对相同元素对应的 score 值进行 SUM 聚合操作,numkeys 为集合数量
ZUNIONSTORE destination numkeys key1 key2 …求并集,其它和 ZINTERSTORE 类似
ZDIFFSTORE destination numkeys key1 key2 …求差集,其它和 ZINTERSTORE 类似
ZRANGE key start end获取指定有序集合 start 和 end 之间的元素(score 从低到高)
ZREVRANGE key start end获取指定有序集合 start 和 end 之间的元素(score 从高到底)
ZREVRANK key member获取指定有序集合中指定元素的排名(score 从大到小排序)

更多 Redis Sorted Set 命令以及详细使用指南,请查看 Redis 官网对应的介绍:https://redis.io/commands/?group=sorted-setopen in new window

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

数据结构

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

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

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

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

跳跃表(跳表)

有序集合在生活中比较常见,例如根据成绩对学生排名,根据得分对玩家排名等。

对于有序集合的底层实现,可以用数组、平衡树、链表等,数组不便元素的插入、删除;平衡树或红黑树虽然效率高但结构复杂;

链表查询需要遍历效率低。

Redis采用的是跳跃表。跳跃表效率堪比红黑树,实现远比红黑树简单。

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

1)有序链表
在这里插入图片描述
要查找值为51的元素,需要从第一个元素开始依次查找、比较才能找到。共需要6次比较。

2)跳跃表
在这里插入图片描述
从第2层开始,1节点比51节点小,向后比较。

21节点比51节点小,继续向后比较,后面就是NULL了,所以从21节点向下到第1层

在第1层,41节点比51节点小,继续向后,61节点比51节点大,所以从41向下

在第0层,51节点为要查找的节点,节点被找到,共查找4次。

从此可以看出跳跃表比有序链表效率要高

应用场景

(1)需要随机获取数据源中的元素根据某个权重进行排序的场景

  • 举例:各种排行榜比如直播间送礼物的排行榜、朋友圈的微信步数排行榜、王者荣耀中的段位排行榜、话题热度排行榜等等。
  • 相关命令:ZRANGE (从小到大排序)、 ZREVRANGE (从大到小排序)、ZREVRANK (指定元素排名)。

(2)需要存储的数据有优先级或者重要程度的场景 比如优先级任务队列。

举例:优先级任务队列。

Redis新数据类型

Bitmaps

现代计算机用二进制(位) 作为信息的基础单位, 1个字节等于8位, 例如“abc”字符串是由3个字节组成,但实际在计算机存储时将其用二进制表示, “abc”分别对应的ASCII码分别是97、 98、 99, 对应的二进制分别是01100001、 01100010和01100011,如下图
在这里插入图片描述
Redis提供了Bitmaps这个“数据类型”可以实现对的操作:

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

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

常用命令
命令介绍
SETBIT key offset value设置指定 offset 位置的值
GETBIT key offset获取指定 offset 位置的值
BITCOUNT key start end获取 start 和 end 之前值为 1 的元素个数
BITOP operation destkey key1 key2 …对一个或多个 Bitmap 进行运算,可用运算符有 AND, OR, XOR 以及 NOT

注意:redis的setbit设置或清除的是bit位置,而bitcount计算的是byte位置。

应用场景

需要保存状态信息(0/1 即可表示)的场景;比如:适合存储日活跃用户比较多的情况
在这里插入图片描述
在这里插入图片描述

HyperLogLog

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

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

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

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

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

以上的方案结果精确,但随着数据不断增加,导致占用空间越来越大,对于非常大的数据集是不切实际的,能否降低一定的精度来平衡存储空间?Redis推出了HyperLogLog

HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。

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

常用命令

HyperLogLog 相关的命令非常少,最常用的也就 3 个。

命令介绍
PFADD key element1 element2 …添加一个或多个元素到 HyperLogLog 中
PFCOUNT key1 key2获取一个或者多个 HyperLogLog 的唯一计数。
PFMERGE destkey sourcekey1 sourcekey2 …将多个 HyperLogLog 合并到 destkey 中,destkey 会结合多个源,算出对应的唯一计数。
应用场景

数量量巨大(百万、千万级别以上)的计数场景。

Geospatial

Geospatial index(地理空间索引,简称 GEO) 主要用于存储地理位置信息,基于 Sorted Set 实现。GEO,Geographic,地理信息的缩写,元素的2维坐标,在地图上就是经纬度。

通过 GEO 我们可以轻松实现两个位置距离的计算、获取指定位置附近的元素等功能。

常用命令
命令介绍
GEOADD key longitude1 latitude1 member1 …添加一个或多个元素对应的经纬度信息到 GEO 中
GEOPOS key member1 member2 …返回给定元素的经纬度信息
GEODIST key member1 member2 M/KM/FT/MI返回两个给定元素之间的距离
GEORADIUS key longitude latitude radius distance获取指定位置附近 distance 范围内的其他元素,支持 ASC(由近到远)、DESC(由远到近)、Count(数量) 等参数
GEORADIUSBYMEMBER key member radius distance类似于 GEORADIUS 命令,只是参照的中心点是 GEO 中的元素
应用场景

需要管理使用地理空间数据的场景

举例:附近的人。

第三章 Redis配置文件

Redis 的配置文件位于 Redis 安装目录下,文件名为 redis.conf(Windows 名为 redis.windows.conf)。

redis.conf 配置项说明:

1)bind

默认情况bind=127.0.0.1只能接受本机的访问请求;不写或者注释掉,无限制接受任何ip地址的访问

生产环境写你应用服务器的地址;服务器是需要远程访问的,所以需要将其注释掉

2)protected-mode

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

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

3)port

端口号,默认 6379

4)timeout

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

5)tcp-keepalive

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

6)loglevel

指定日志记录级别,Redis总共支持四个级别:debug、verbose、notice、warning,默认为notice

7)databases 16

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

8)maxclients

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

9)maxmemory

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

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

10)maxmemory-policy

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

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

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

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

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

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

11)maxmemory-samples

设置样本数量,LRU算法和最小TTL算法都并非是精确的算法,而是估算值,所以你可以设置样本的大小,redis默认会检查这么多

个key并选择其中LRU的那个。

一般设置3到7的数字,数值越小样本越不准确,但性能消耗越小。

第四章 Redis的发布和订阅

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

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

1)客户端订阅频道,如下图
在这里插入图片描述

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

在这里插入图片描述

发布订阅命令行实现

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

SUBSCRIBE channel1

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

publish channel1 hello

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

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

第五章 Redis数据持久化

官方文档地址:https://redis.io/topics/persistence

RDB

RDB(Redis DataBase)持久化:在指定的时间间隔内,将内存中的数据快照Snapshot写入磁盘,数据恢复时,将快照文件直接读到内存里。

Redis 提供了两个命令来生成 RDB 快照文件,

(1)save : 同步保存操作,会阻塞 Redis 主线程;

(2)bgsave : fork 出一个子进程,子进程执行,不会阻塞 Redis 主线程,默认选项。

快照持久化是 Redis 默认采用的持久化方式,可查看redis.windows.conf配置文件,

#在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发bgsave命令创建快照。
save 900 1 

#在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发bgsave命令创建快照。
save 300 10          

#在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发bgsave命令创建快照。
save 60 10000       

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

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

AOF

AOF(Append Only File)持久化:以日志的形式来记录每个写操作增量保存),将Redis执行过的所有写指令记录下来(读操作不记录), 只许追加文件但不可以改写文件。

AOF 文件的保存位置和 RDB 文件的位置相同,都是通过 dir 参数设置的,默认的文件名是 appendonly.aof

与快照持久化相比,AOF 持久化的实时性更好。默认情况下 Redis 没有开启 AOF方式的持久化(Redis 6.0 之后已经默认是开启了),可以通过 appendonly 参数开启。

appendonly yes

AOF 持久化功能的实现可以分为 5 步:

  1. 命令追加(append):所有的写命令会追加到 AOF 缓冲区中。
  2. 文件写入(write):将 AOF 缓冲区的数据写入到 AOF 文件中。这一步需要调用write函数(系统调用),write将数据写入到了系统内核缓冲区之后直接返回了(延迟写)。注意!!!此时并没有同步到磁盘。
  3. 文件同步(fsync):AOF 缓冲区根据对应的持久化方式( fsync 策略)向硬盘做同步操作。这一步需要调用 fsync 函数(系统调用), fsync 针对单个文件操作,对其进行强制硬盘同步,fsync 将阻塞直到写入磁盘完成后返回,保证了数据持久化。
  4. 文件重写(rewrite):随着 AOF 文件越来越大,需要定期对 AOF 文件进行重写,达到压缩的目的。
  5. 重启加载(load):当 Redis 重启时,可以加载 AOF 文件进行数据恢复。

在这里插入图片描述

fsync策略

fsync用于强制刷新系统内核缓冲区(同步到到磁盘),确保写磁盘操作结束才会返回。

(1)appendfsync always:主线程调用 write 执行写操作后,后台线程( aof_fsync 线程)立即会调用 fsync 函数同步 AOF 文件(刷盘),fsync 完成后线程返回,这样会严重降低 Redis 的性能(write + fsync)。

(2)appendfsync everysec:主线程调用 write 执行写操作后立即返回,由后台线程( aof_fsync 线程)每秒钟调用 fsync 函数(系统调用)同步一次 AOF 文件(write+fsyncfsync间隔为 1 秒)

(3)appendfsync no:主线程调用 write 执行写操作后立即返回,让操作系统决定何时进行同步,Linux 下一般为 30 秒一次(write但不fsyncfsync 的时机由操作系统决定)。

对比

(1)RDB 文件存储的内容是经过压缩的二进制数据, 保存着某个时间点的数据集,文件很小,适合做数据的备份,灾难恢复。AOF 文件存储的是每一次写命令,类似于 MySQL 的 binlog 日志,通常会比 RDB 文件大很多。

(2)RDB 的数据安全性不如 AOF,没有办法实时或者秒级持久化数据。生成 RDB 文件的过程是比较繁重的, 虽然 BGSAVE 子进程写入 RDB 文件的工作不会阻塞主线程,但会对机器的 CPU 资源和内存资源产生影响,严重的情况下甚至会直接把 Redis 服务干宕机。

第六章 Jedis

Jedis是Redis的一款Java语言的开源客户端连接工具

目前Redis官网推荐使用的 Java客户端有三款:Jedis、lettuce、Redisson

引入jedis依赖

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

相关api

  • key
  • string
  • list
  • set
  • hash
  • zset

第七章 SpringBoot集成redis

引入spring-boot-starter-data-redis依赖

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

在这里插入图片描述
在Springboot2.x之后,原来使用的jedis被替换为lettuce,可切换至jedis(可选) 方法:引入jedis+修改配置文件。

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>
spring:
  redis:
      client-type: jedis

NOTE:

1)RedisAutoConfiguration自动配置类,自动配置了RedisTemplateStringRedisTemplate,可操纵redis

2)RedisProperties --> spring.redis.xxx

redis相关配置

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

StringRedisTemplate与RedisTemplate

StringRedisTemplate与RedisTemplate区别

public class RedisTemplate<K, V> extends xxx{...}
public class StringRedisTemplate extends RedisTemplate<String, String> {...}

RedisTemplate:k- v 都是对象

StringRedisTemplate:k-v 都是字符串

相关API

  • opsForValue() 操作字符串
  • opsForList() 操作list数据结构
  • opsForSet() 操作set数据结构
  • opsForZSet() 操作有序set
  • opsForHash() 操作Hash数据结构 object
  • opsForHyperLogLog() 操作HyperLogLog

NOTE:推荐将redis的相关方法封装为RedisUtil工具类,便于整个项目复用。

Spring Boot缓存注解

@Cacheable

根据方法对其返回结果进行缓存,下次请求时,如果缓存存在,则直接读取缓存数据返回;如果缓存不存在,则执行方法,并把返回的结果存入缓存中。

一般用在查询方法上

查看源码,属性值如下:

属性/方法名解释
value缓存名,必填,它指定了你的缓存存放在哪块命名空间
cacheNames与 value 差不多,二选一即可
key可选属性,可以使用 SpEL 标签自定义缓存的key

原理:
在这里插入图片描述

@CachePut

使用该注解标志的方法,每次都会执行,并将结果存入指定的缓存中。其他方法可以直接从响应的缓存中读取缓存数据,而不需要再去查询数据库。

一般用在新增方法上

查看源码,属性值如下:

属性/方法名解释
value缓存名,必填,它指定了你的缓存存放在哪块命名空间
cacheNames与 value 差不多,二选一即可
key可选属性,可以使用 SpEL 标签自定义缓存的key
@CacheEvict

使用该注解标志的方法,会清空指定的缓存。

一般用在更新或者删除方法上

查看源码,属性值如下:

属性/方法名解释
value缓存名,必填,它指定了你的缓存存放在哪块命名空间
cacheNames与 value 差不多,二选一即可
key可选属性,可以使用 SpEL 标签自定义缓存的key
allEntries是否清空所有缓存,默认为 false。如果指定为 true,则方法调用后将立即清空所有的缓存
beforeInvocation是否在方法执行前就清空,默认为 false。如果指定为 true,则在方法执行前就会清空缓存
快速入门

【第一步】编写redis配置类

@EnableCaching+@Configuration

RedisConfig.java

@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
                          .SerializationPair
                          .fromSerializer(jackson2JsonRedisSerializer))
                          .disableCachingNullValues();
        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
        return cacheManager;
    }
}

【第二步】在接口中添加redis缓存

注意:key里面有对单引号 ‘’

@Service
public class CrmBannerServiceImpl extends ServiceImpl<CrmBannerMapper, CrmBanner> implements CrmBannerService {
    //查询所有banner
    @Cacheable(value = "banner",key = "'bannerList'")
    @Override
    public List<CrmBanner> selectAllBanner() {
        //根据id进行降序排列,显示排列之后前两条记录
        QueryWrapper<CrmBanner> wrapper = new QueryWrapper<>();
        wrapper.orderByDesc("id");
        //last方法,拼接sql语句
        wrapper.last("limit 2");
        List<CrmBanner> list = baseMapper.selectList(null);
        return list;
    }
    
    @CacheEvict(value = "banner", allEntries=true)
    @Override
    public void updateBannerById(CrmBanner banner) {
        baseMapper.updateById(banner);
    }
    
    @CacheEvict(value = "banner", allEntries=true)
    @Override
    public void saveBanner(CrmBanner banner) {
        baseMapper.insert(banner);
    }
}

【第五步】测试,查看redis数据库,发现redis添加了key
在这里插入图片描述
tips:

通过源码查看到key生成的规则
在这里插入图片描述

Redis实现分布式锁

@SpringBootTest
public class DistributeLockRedisTest {
    private static final Logger log= LoggerFactory.getLogger(DistributeLockRedisTest.class);

    @Autowired
    private  RedisTemplate redisTemplate;

    @Test
    public void testLock(){
        // UUID防误删
        String uuid = UUID.randomUUID().toString();
        String account="aaa-bbb";
        String lockKey= String.format("lock:%s", account);

        //1、获取锁,设置锁有效时间
        Boolean lock = redisTemplate.opsForValue()
                       .setIfAbsent(lockKey, uuid,2, TimeUnit.SECONDS);

        Object num_value=null;

        //2、获取锁成功,执行相关业务
        if(lock){
            Object value = redisTemplate.opsForValue().get("num");
            if(Objects.isNull(value)){
                log.error("数据为空");
            }
            int num = Integer.parseInt(value+"");
            redisTemplate.opsForValue().set("num", ++num);
            num_value=redisTemplate.opsForValue().get("num");
            log.info("num:{}",num_value);
            //3、释放锁,使用redis执行lua脚本,保证删除的原子性
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
            redisScript.setScriptText(script);
            redisScript.setResultType(Long.class);
            // 第一个是script脚本 ,第二个需要判断的key,第三个就是key所对应的值。
            redisTemplate.execute(redisScript, Collections.singletonList(lockKey), uuid);
        }else{
            // 获取锁失败、每隔0.5秒再获取,重试
            try {
                Thread.sleep(500);
                testLock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

NOTE:lua脚本,将复杂的或者多步的redis操作,写为一个lua脚本,一次提交给redis执行,减少反复连接redis的次数,提升性能。LUA脚本是类似redis事务,有一定的原子性,不会被其他命令插队,可以完成一些redis事务性的操作。

注意:redis的lua脚本功能,只有在Redis 2.6以上的版本才可以使用。

第八章 解决方案

缓存穿透

缓存穿透是指查询一个一定不存在的数据,这个数据在缓存中查询不到,就会从数据库中查询。如果这个不存在的查询大量出现,就会导致所有的请求都落到数据库上,造成数据库压力过大。

这种情况通常是由攻击者利用非法参数或不存在的数据ID进行攻击导致的。

解决方案:

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

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

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

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

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

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

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

缓存击穿

缓存击穿指的是某个key在有效期过后的一瞬间,有大量的并发请求过来,这些请求发现缓存过期了,就会直接打到后端数据库,造成数据库压力过大。

通常发生在热点key过期的时候

解决方案:

(1)使用互斥锁(Mutex)来控制只有一个线程或者进程去加载数据,其他请求则等待缓存更新完成后再获取数据。

缓存雪崩

缓存雪崩是指在某一个时间段,缓存中大批量的数据集中失效,导致所有请求都到达数据库,造成数据库压力过大,甚至宕机。

通常是由于缓存服务器重启或者机器宕机及部署的应用程序突然流量增大导致的。

分布式锁

如果是在单机的情况下,使用synchronizedLock保证线程安全是没有问题的。

如果在分布式的环境中,即某个应用如果部署了多个节点,每一个节点可以使用synchronizedLock保证线程安全,但不同的节点之间,没法保证线程安全。解决方式:分布式锁

分布式锁有很多种,比如:数据库分布式锁,zookeeper分布式锁,redis分布式锁等。


原文地址:https://blog.csdn.net/weixin_44490884/article/details/142650178

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!