前言
Redis(Remote Dictionary Server ),即远程字典服务,是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。
本篇学习笔记根据B站【狂神说系列视频】课程整理,仅用作自己学习~
Redis命令手册
Redis中文网
Redis命令手册
Redis教程
一、数据演变
1、单机Mysql时代
存在问题:
- 数据量增加到一定程度,单机数据库就放不下了
- 数据的索引(B+ Tree),一个机器内存也存放不下
- 访问量变大后(读写混合),一台服务器承受不住。
2、Memcached(缓存) + Mysql + 垂直拆分(读写分离)
网站80%的情况都是在读,每次都要去查询数据库的话就十分的麻烦!所以说我们希望减轻数据库的压力,我们可以使用缓存来保证效率!
优化过程经历了以下几个过程:
- 优化数据库的数据结构和索引(难度大)
- 文件缓存,通过IO流获取比每次都访问数据库效率略高,但是流量爆炸式增长时候,IO流也承受不了
- MemCache,当时最热门的技术,通过在数据库和数据库访问层之间加上一层缓存,第一次访问时查询数据库,将结果保存到缓存,后续的查询先检查缓存,若有直接拿去使用,效率显著提升。
3、分库分表 + 水平拆分 + Mysql集群
- 之前的Mysql引擎:MySAM(表锁)十分影响效率
- 后来的Mysql引擎:Innodb(行锁)
4、最近的年代
如今信息量井喷式增长,各种各样的数据出现(用户定位数据,图片数据等),
大数据的背景下关系型数据库(RDBMS)无法满足大量数据要求。Nosql数据库就能轻松解决这些问题。
二、NoSQL 概述
为什么要用NoSQL ?
用户的个人信息,社交网络,地理位置。用户自己产生的数据,用户日志等等爆发式增长!
这时候我们就需要使用NoSQL数据库的,Nosql可以很好的处理以上的情况!
什么是Nosql
NoSQL = Not Only SQL(不仅仅是SQL)Not Only Structured Query Language
-
关系型数据库:列+行,同一个表下数据的结构是一样的。
-
非关系型数据库:数据存储没有固定的格式,并且可以进行横向扩展。
NoSQL泛指非关系型数据库,随着web2.0互联网的诞生,传统的关系型数据库很难对付web2.0时代!尤其是超大规模的高并发的社区,暴露出来很多难以克服的问题,NoSQL在当今大数据环境下发展的十分迅速,Redis是发展最快的。
Nosql特点
- 方便扩展(数据之间没有关系,很好扩展!)
- 大数据量高性能(Redis一秒可以写8万次,读11万次,NoSQL的缓存记录级,是一种细粒度的缓存,性能会比较高!)
- 数据类型是多样型的!(不需要事先设计数据库,随取随用)
- 传统的 RDBMS 和 NoSQL的区别:
传统的 RDBMS(关系型数据库)
- 结构化组织
- SQL
- 数据和关系都存在单独的表中 row col
- 操作,数据定义语言
- 严格的一致性
- 基础的事务
- ...
Nosql
- 不仅仅是数据
- 没有固定的查询语言
- 键值对存储,列存储,文档存储,图形数据库(社交关系)
- 最终一致性
- CAP定理和BASE
- 高性能,高可用,高扩展
- ...
了解:3V + 3高
大数据时代的3V :主要是描述问题的
- 海量Velume
- 多样Variety
- 实时Velocity
大数据时代的3高 : 主要是对程序的要求
-
高并发
-
高可扩
-
高性能
真正在公司中的实践:NoSQL + RDBMS 一起使用才是最强的。
Nosql的四大分类
KV键值对
- 新浪:Redis
- 美团:Redis + Tair
- 阿里、百度:Redis + Memcache
文档型数据库(bson数据格式)
- MongoDB(掌握)
- 基于分布式文件存储的数据库。C++编写,用于处理大量文档。
- MongoDB是RDBMS和NoSQL的中间产品。MongoDB是非关系型数据库中功能最丰富的,NoSQL中最像关系型数据库的数据库。
- ConthDB
列存储数据库
- HBase(大数据必学)
- 分布式文件系统
图关系数据库
用于广告推荐,社交网络:Neo4j、InfoGrid
分类 | 举例 | 典型应用场景 | 数据模型 | 优点 | 缺点 |
---|---|---|---|---|---|
键值对(key-value) | Tokyo Cabinet/Tyrant, Redis, Voldemort, Oracle BDB | 内容缓存,主要用于处理大量数据的高访问负载,也用于一些日志系统等等。 | Key 指向 Value 的键值对,通常用hash table来实现 | 查找速度快 | 数据无结构化,通常只被当作字符串或者二进制数据 |
列存储数据库 | Cassandra, HBase, Riak | 分布式的文件系统 | 以列簇式存储,将同一列数据存在一起 | 查找速度快,可扩展性强,更容易进行分布式扩展 | 功能相对局限 |
文档型数据库 | CouchDB, MongoDb | Web应用(与Key-Value类似,Value是结构化的,不同的是数据库能够了解Value的内容) | Key-Value对应的键值对,Value为结构化数据 | 数据结构要求不严格,表结构可变,不需要像关系型数据库一样需要预先定义表结构 | 查询性能不高,而且缺乏统一的查询语法。 |
图形(Graph)数据库 | Neo4J, InfoGrid, Infinite Graph | 社交网络,推荐系统等。专注于构建关系图谱 | 图结构 | 利用图结构相关算法。比如最短路径寻址,N度关系查找等 | 很多时候需要对整个图做计算才能得出需要的信息,而且这种结构不太好做分布式的集群 |
三、Redis 概述
Redis是什么?
Redis(Remote Dictionary Server ),即远程字典服务。
是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。
与memcached一样,为了保证效率,数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。
Redis能该干什么?
内存存储、持久化,内存是断电即失的,所以需要持久化(RDB、AOF)
- 高效率、用于高速缓存
- 发布订阅系统(简单的消息队列)
- 地图信息分析
- 计时器、计数器(浏览量)
- 。。。
Redis特性
-
多样的数据类型
-
持久化
-
集群
-
事务
-
…
Windows环境安装Redis
Github下载:https://github.com/dmajkic/redis
-
解压安装包
-
开启redis-server.exe
-
启动redi-cli.exe测试
Linux环境安装Redis
-
下载安装包!
-
解压Redis的安装包!程序一般放在
/opt
目录下
-
进入解压后的文件,查看redis的配置文件
- 安装基本环境
yum install gcc-c++
# 然后进入redis目录下执行
make
# 然后执行
make install
- redis默认安装路径
/usr/local/bin
- 将redis的配置文件复制到 程序安装目录
/usr/local/bin/configfill
下
- redis默认不是后台启动的,需要修改配置文件!
- 通过制定的配置文件启动redis服务
- 使用
redis-cli
连接指定的端口号测试,Redis的默认端口6379
- 可以查看一下redis的进程是否开启
- 关闭Redis服务:
shutdown
Redis性能测试
redis-benchmark:Redis官方提供的性能测试工具,参数选项如下:
测试:
# 测试:100个并发连接 100000请求
redis-benchmark -h localhost -p 6379 -c 100 -n 100000
Redis 的基本知识
redis默认有16个数据库
默认使用的第0个;16个数据库为:DB 0~DB 15
默认使用DB 0 ,可以使用select n
切换到DB n,dbsize
可以查看当前数据库的大小,与key数量相关。
keys *
:查看当前数据库中所有的key。
flushdb
:清空当前数据库中的键值对。
flushall
:清空所有数据库的键值对。
Redis是单线程的,Redis是基于内存操作的。
所以Redis的性能瓶颈不是CPU,而是机器内存和网络带宽。
那么为什么Redis的速度如此快呢,性能这么高呢?QPS达到10W+
Redis为什么单线程还这么快?
- 误区1:高性能的服务器一定是多线程的?
- 误区2:多线程(CPU上下文会切换!)一定比单线程效率高!
核心:Redis是将所有的数据放在内存中的,所以说使用单线程去操作效率就是最高的,多线程(CPU上下文会切换:耗时的操作!),对于内存系统来说,如果没有上下文切换效率就是最高的,多次读写都是在一个CPU上的,在内存存储数据情况下,单线程就是最佳的方案。
四、5大数据类型
Redis是一个开源(BSD许可),内存存储的数据结构服务器,可用作数据库,高速缓存和消息队列代理。它支持 字符串 、哈希表、列表、集合、有序集合,位图,hyperloglogs等数据类型。内置复制、Lua脚本、LRU收回、事务以及不同级别磁盘持久化功能,同时通过Redis Sentinel提供高可用,通过Redis Cluster提供自动分区。
Redis-key
在redis中无论什么数据类型,在数据库中都是以key-value形式保存,通过进行对Redis-key的操作,来完成对数据库中数据的操作。
常用命令:
exists key
:判断键是否存在del key
:删除键值对move key db
:将键值对移动到指定数据库expire key second
:设置键值对的过期时间type key
:查看value的数据类型
127.0.0.1:6379> EXISTS name #判断键是否存在
(integer) 1 #1表示存在
127.0.0.1:6379> EXISTS sex
(integer) 0 #0表示不存在
127.0.0.1:6379> move name 1 # 将键值对移动到指定数据库
(integer) 1
127.0.0.1:6379> keys *
1) "age"
127.0.0.1:6379> EXPIRE age 10 # 设置键值对的过期时间
(integer) 1
127.0.0.1:6379> ttl age # 查看key的过期剩余时间,-2表示已过期
(integer) -2
127.0.0.1:6379> get age # 过期的key 会被自动delete
(nil)
127.0.0.1:6379> TYPE name # 查看name的数据类型
string
127.0.0.1:6379> TYPE age # 查看age的数据类型
string
127.0.0.1:6379> del name # 删除键值对
(integer) 1
127.0.0.1:6379> keys *
1) "age"
关于TTL
命令
Redis的key,通过TTL命令返回key的过期时间,一般来说有3种:
- 当前key没有设置过期时间,所以会返回-1.
- 当前key有设置过期时间,而且key已经过期,所以会返回-2.
- 当前key有设置过期时间,且key还没有过期,故会返回key的正常剩余时间.
关于重命名RENAME
和RENAMENX
RENAME key newkey
:修改 key 的名称RENAMENX key newkey
:仅当 newkey 不存在时,将 key 改名为 newkey 。
String(字符串)
String类似的使用场景:value除了是字符串还可以是数字,用途举例:
- 计数器(incrby)
- 统计多单位的数量:uid:123666:follow 0
- 粉丝数
- 对象存储缓存
常用命令:
-
APPEND key value
:向指定的key的value后追加字符串
-
STRLEN key
:获取key保存值的字符串长度
-
DECR/INCR key
:将指定key的value数值进行+1/-1(仅对于数字)
-
INCRBY/DECRBY key n
:按指定的步长对数值进行加减
-
INCRBYFLOAT key n
:为数值加上浮点型数值
-
GETRANGE key start end
:按起止位置获取字符串(闭区间,起止位置都取)
-
SETRANGE key offset value
:用指定的value 替换key中 offset开始的值
-
SETEX key seconds value
:set 键值对并设置过期时间,单位为秒
-
SETNX key value
:仅当key不存在时进行set
-
MSET key1 value1 [key2 value2..]
:批量set键值对
-
MGET key1 [key2..]
:批量获取多个key保存的值
-
MSETNX key1 value1 [key2 value2..]
:批量设置键值对,仅当参数中所有的key都不存在时执行(原子性操作,都成功或都失败)
-
GETSET key value
:如果不存在key,则返回nil;如果存在,将给定 key 的值设为 value ,并返回 key 的旧值(old value)。
-
PSETEX key milliseconds value
:和 SETEX 命令相似,但它以毫秒为单位设置 key 的生存时间,
-
设置对象
List(列表)
Redis列表是简单的字符串列表,按照插入顺序排序。
你可以添加一个元素到列表的头部(左边)或者尾部(右边)
一个列表最多可以包含 232 - 1 个元素 (4294967295, 每个列表超过40亿个元素)。
列表,经过规则定义可以将其变为队列、栈、双端队列等
如图Redis中List是可以进行双端操作的,所以命令也就分为了LXXX和RLLL两类,有时候L也表示List例如LLEN
常用命令:
-
LPUSH/RPUSH key value1[value2..]
:从左边(头部)/右边(尾部)向列表中PUSH值(一个或者多个)。
-
LRANGE key start end
:获取list 起止元素(索引从左往右 递增)
-
LPOP/RPOP key
:从最左边/最右边移除值 并返回
-
LINDEX key index
:通过索引获取列表元素
-
LLEN key
:查看列表长度
-
LREM key count value
:List中是允许value重复的count > 0
:从头部开始搜索 然后删除指定的value 至多删除count个count < 0
:从尾部开始搜索…count = 0
:删除列表中所有的指定value。
-
LTRIM key start end
:通过下标截取指定范围内的列表
-
RPOPLPUSH source destination
:将列表的尾部(右)最后一个值弹出,并返回,然后加到另一个列表的头部
-
LSET key index value
:通过索引为元素更新值(列表必须存在,索引必须存在值,不存在会报错)
-
LINSERT key BEFORE|AFTER pivot value
:在指定列表元素的前/后 插入value
-
BLPOP/BRPOP key1[key2] timout
:移出并获取列表的第一个/最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
-
BRPOPLPUSH source destination timeout
:和RPOPLPUSH
功能相同,但是会把从旧列表弹出的元素加入到新的列表中,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
总结:
-
list实际上是一个链表,before Node after , left, right 都可以插入值
-
如果key不存在,则创建新的链表
-
如果key存在,新增内容
-
如果移除了所有值,空链表,也代表不存在
-
在两边插入或者改动值,效率最高!修改中间元素,效率相对较低
应用:
- 消息队列(先进先出:Lpush Rpop)
- 栈(先进后出:Lpush Lpop )
Set(集合)
Redis的Set是string类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。
Redis 中 集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。
集合中最大的成员数为 232 - 1 (4294967295, 每个集合可存储40多亿个成员)。
常用命令:
-
SADD key member1[member2..]
:向集合中无序增加一个/多个成员
-
SMEMBERS key
:返回集合中所有的成员
-
SISMEMBER key member
:查询member元素是否是集合的成员
-
SCARD key
:获取集合的成员数
-
SREM key member1[member2..]
:移除集合中一个/多个成员
-
SRANDMEMBER key [count]
:随机返回集合中count个成员,count缺省值为1
-
SPOP key [count]
:随机移除并返回集合中count个成员,count缺省值为1
-
SMOVE source destination member
:将source集合的成员member移动到destination集合
-
SDIFF key1[key2..]
:返回所有集合的差集 key1- key2 - …
-
SDIFFSTORE destination key1[key2..]
:在SDIFF
的基础上,将结果保存到集合中(如果成员存在,会覆盖)。
-
SINTER key1 [key2..]
:返回所有集合的交集
-
SINTERSTORE destination key1[key2..]
:在SINTER
的基础上,存储结果到集合中。如果成员存在,会覆盖
-
SUNION key1 [key2..]
:返回所有集合的并集(例:微博的共同关注)
-
SUNIONSTORE destination key1 [key2..]
:在SUNION
的基础上,存储结果到及和张。如果成员存在,会覆盖
应用:
- 微博等社交软件:A用户B用户的共同关注、共同爱好、推荐好友
Hash(哈希)
Redis hash 是一个string类型的field和value的映射表,hash特别适合用于存储对象。
Set就是一种简化的Hash,只变动key,而value使用默认值填充。可以将一个Hash表作为一个对象进行存储,表中存放对象的信息。
应用:
- 保存经常变更的信息,例如用户信息,存储对象的信息
常用命令:
-
HSET key field value
:将哈希表 key 中的字段 field 的值设为 value 。重复设置同一个field会覆盖,返回0
-
HMSET key field1 value1 [field2 value2..]
:同时将多个 field-value (域-值)对设置到哈希表 key 中。
-
HSETNX key field value
: 只有在字段 field 不存在时,设置哈希表字段的值。
-
HEXISTS key field
:查看哈希表 key 中,指定的字段是否存在。
-
HGET key field value
: 获取存储在哈希表中指定字段的值
-
HMGET key field1 [field2..]
:获取所有给定字段的值
-
HGETALL key
:获取在哈希表key 的所有字段和值
-
HKEYS key
:获取哈希表key中所有的字段
-
HLEN key
:获取哈希表中字段的数量
-
HVALS key
:获取哈希表中所有值
-
HDEL key field1 [field2..]
:删除哈希表key中一个/多个field字段
-
HINCRBY key field n
:为哈希表 key 中的指定字段的整数值加上增量n,并返回增量后结果 一样只适用于整数型字段
-
HINCRBYFLOAT key field n
:为哈希表 key 中的指定字段的浮点数值加上增量 n。
-
HSCAN key cursor [MATCH pattern] [COUNT count]
:迭代哈希表中的键值对。
Zset(有序集合)
和set不同的是Zset中每个元素都会关联一个double类型的分数(score)。redis正是通过分数来为集合中的成员进行从小到大的排序。
score相同:按字典顺序排序
有序集合的成员是唯一的,但分数(score)却可以重复。
应用案例:
- set排序 存储班级成绩表 工资表排序!
- 普通消息,1.重要消息 2.带权重进行判断
- 排行榜应用实现,取Top N测试
常用命令:
-
ZADD key score member1 [score2 member2]
:向有序集合添加一个或多个成员,或者更新已存在成员的分数
-
ZCARD key
:获取有序集合的成员数
-
ZCOUNT key min max
:计算在有序集合中指定区间score的成员数
-
ZINCRBY key n member
:有序集合中对指定成员的分数加上增量 n
-
ZSCORE key member
:返回有序集中,成员的分数值
-
ZRANK key member
:返回有序集合中指定成员的索引
-
ZRANGE key start end
:通过索引区间返回有序集合成指定区间内的成员
-
ZRANGEBYLEX key min max
:通过字典区间返回有序集合的成员
-
ZRANGEBYSCORE key min max
:通过分数返回有序集合指定区间内的成员==-inf 和 +inf分别表示最小最大值,只支持开区间()==
-
ZLEXCOUNT key min max
:在有序集合中计算指定字典区间内成员数量
-
ZREM key member1 [member2..]
:移除有序集合中一个/多个成员
-
ZREMRANGEBYLEX key min max
:移除有序集合中给定的字典区间的所有成员
-
ZREMRANGEBYRANK key start stop
:移除有序集合中给定的排名区间的所有成员
-
ZREMRANGEBYSCORE key min max
:移除有序集合中给定的分数区间的所有成员
-
ZREVRANGE key start end
:返回有序集中指定区间内的成员,通过索引,分数从高到底
-
ZREVRANGEBYSCORRE key max min
:返回有序集中指定分数区间内的成员,分数从高到低排序
-
ZREVRANGEBYLEX key max min
:返回有序集中指定字典区间内的成员,按字典顺序倒序
-
ZREVRANK key member
:返回有序集合中指定成员的排名,有序集成员按分数值递减(从大到小)排序
-
ZINTERSTORE destination numkeys key1 [key2 ..]
:计算给定的一个或多个有序集的交集并将结果集存储在新的有序集合 key 中,numkeys:表示参与运算的集合数,将score相加作为结果的score -
ZUNIONSTORE destination numkeys key1 [key2..]
:计算给定的一个或多个有序集的交集并将结果集存储在新的有序集合 key 中 -
ZSCAN key cursor [MATCH pattern\] [COUNT count]
:迭代有序集合中的元素(包括元素成员和元素分值)
五、3种特殊数据类型
Geospatial(地理位置)
使用经纬度定位地理坐标并用一个有序集合zset保存,所以zset命令也可以使用
实际应用:
- 朋友的定位,附近的人
- 打车距离计算
有效经纬度
- 有效的经度从-180度到180度。
- 有效的纬度从-85.05112878度到85.05112878度。
常用命令:
-
geoadd key longitud(经度) latitude(纬度) member [..]
:将具体经纬度的坐标存入一个有序集合
-
geopos key member [member..]
: 获取集合中的一个/多个成员坐标
-
geodist key member1 member2 [unit]
: 返回两个给定位置之间的距离。默认以米作为单位。
指定单位的参数 unit 必须是以下单位的其中一个:-
m 表示单位为米。
-
km 表示单位为千米。
-
mi 表示单位为英里。
-
ft 表示单位为英尺。
-
-
georadius key longitude latitude radius m|km|mi|ft [WITHCOORD][WITHDIST] [WITHHASH] [COUNT count]
: 以给定的经纬度为中心, 返回集合包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。
-
GEORADIUSBYMEMBER key member radius...
: 功能与GEORADIUS相同,只是中心位置不是具体的经纬度,而是使用结合中已有的成员作为中心点。
-
geohash key member1 [member2..]
: 返回一个或多个位置元素的Geohash表示。使用Geohash位置52点整数编码,返回11个字符的Geohash字符串。
Hyperloglog(基数统计)
什么是基数?
数据集中不重复的元素的个数。
Redis HyperLogLog 是用来做基数统计的算法;
HyperLogLog的优点:在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。
因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog
不能像集合那样,返回输入的各个元素。其底层使用string数据类型
应用场景:
-
网页的访问量(UV):一个用户多次访问,也只能算作一个人。(如果允许容错的情况下使用Hyperloglog)
-
传统实现,存储用户的id,然后每次进行比较。当用户变多之后这种方式及其浪费空间,而我们的目的只是计数,Hyperloglog就能帮助我们利用最小的空间完成。
常用命令:
-
PFADD key element1 [elememt2..]
: 添加指定元素到 HyperLogLog 中
-
PFCOUNT key [key]
: 返回给定 HyperLogLog 的基数估算值。
-
PFMERGE destkey sourcekey [sourcekey..]
: 将多个 HyperLogLog 合并为一个 HyperLogLog
BitMaps(位图)
位存储,信息状态只有 0 和 1
Bitmap是一串连续的2进制数字(0或1),每一位所在的位置为偏移(offset),在bitmap上可执行AND,OR,XOR,NOT以及其它位操作。
应用场景:
- 签到统计(是否打卡、是否签到…)
- 状态统计(是否登陆、是否在线…)
常用命令:
-
setbit key offset value
: 为指定key的offset位设置值
-
getbit key offset
: 获取offset位的值
-
bitcount key [start end]
: 统计字符串被设置为1的bit数,也可以指定统计范围按字节
-
bitop operration destkey key[key..]
: 对一个或多个保存二进制位的字符串 key 进行位元操作,并将结果保存到 destkey 上。 -
BITPOS key bit [start] [end]
: 返回字符串里面第一个被设置为1或者0的bit位。start和end只能按字节,不能按位
五、事务
Redis事务没有隔离级别的概念
Redis单条命令是保证原子性的,但是事务不保证原子性!
- Redis事务本质:一组命令的集合。
----------------- 队列 set set set 执行 -------------------
事务中每条命令都会被序列化,执行过程中按顺序执行,不允许其他命令进行干扰。
- 一次性
- 顺序性
- 排他性
Redis事务操作过程
- 开启事务(
multi
) - 命令入队
- 执行事务(
exec
)
所有事务中的命令在加入时都没有被执行,直到提交时才会开始执行(Exec)一次性完成。
-
正常执行事务
-
放弃事务(
discurd
)
事务错误
代码语法错误(编译时异常)所有的命令都不执行
代码逻辑错误 (运行时异常) 其他命令可以正常执行 -----> 所以不保证事务原子性
事务监控
悲观锁:
- 很悲观,认为什么时候都会出现问题,无论做什么都会加锁
乐观锁:
-
很乐观,认为什么时候都不会出现问题,所以不会上锁!更新数据的时候去判断一下,在此期间是否有人修改过这个数据
-
获取version
-
更新的时候比较version
-
Redis使用
watch key
监控指定数据
测试:
-
单线程下执行
-
多线程执行
-
unwatch
命令解锁获取最新值,然后再加锁进行事务。
六、Jedis
使用Java来操作Redis,Jedis是Redis官方推荐使用的Java连接redis的客户端。
配置Jedis连接远程服务器
- 导入相关依赖
<!--导入jredis的包-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.2.0</version>
</dependency>
<!--fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.70</version>
</dependency>
- 修改redis的配置文件(连接远程服务器上的redis为例)
- 修改访问配置
- 防火墙配置
firewall-cmd --zone=public --add-port=6379/tcp --permanet #开放6379端口
systemctl restart firewalld.service #重启防火墙服务
- 阿里云安全组配置,开放6379端口
-
重启redis-server
-
测试连通
public class TestPing {
public static void main(String[] args) {
// 1. 获取jedis对象
Jedis jedis = new Jedis("118.31.116.86",6379);
System.out.println(jedis.ping());
}
}
通过jedis处理事务
public class TestTX {
public static void main(String[] args) {
Jedis jedis = new Jedis("118.31.116.86", 6379);
//清空数据库
jedis.flushDB();
//创建一个fastjson对象
JSONObject jsonObject = new JSONObject();
jsonObject.put("hello","world");
jsonObject.put("mysql","redis");
String jsonString = jsonObject.toJSONString();
//开启事务
Transaction multi = jedis.multi();
//jedis.watch("user1");
try {
multi.set("user1",jsonString);
multi.set("user2",jsonString);
int i = 1/0;
//执行事务
multi.exec();
} catch (Exception e) {
//如果出现异常则放弃事务
multi.discard();
e.printStackTrace();
} finally {
System.out.println(jedis.get("user1"));
System.out.println(jedis.get("user2"));
//关闭连接
jedis.close();
}
}
}
七、SpringBoot整合Redis
springboot 2.x后 ,原来使用的 Jedis 被 lettuce 替换。
jedis:采用的直连,多个线程操作的话,是不安全的。如果要避免不安全,使用jedis pool连接池!更像BIO模式
lettuce:采用netty,实例可以在多个线程中共享,不存在线程不安全的情况!可以减少线程数据了,更像NIO模式
测试使用
- 导入相关依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
- 编写application.yaml配置文件
# 配置服务器端口号
spring:
redis:
host: 118.31.116.86
port: 6379
- 测试类测试
@SpringBootTest
class Redis02SpringbootApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
@Test
void contextLoads() {
//获取redis数据库连接对象
/* RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
connection.flushAll();
connection.flushDb();*/
redisTemplate.opsForValue().set("key1","hello");
System.out.println(redisTemplate.opsForValue().get("key1"));
}
}
概览源码
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)//redis可以配置的所有东西
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {
//默认的RedisTemplate没有太多设置,我们需要自定义RedisTemplate覆盖它
//我们大多情况下要使用RedisTemplate<string, Object>
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
//因为string类型是redis中最常用的,所以单独准备了一个StringRedisTemplate
@Bean
@ConditionalOnMissingBean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
定制RedisTemplate
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplates(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
template.setConnectionFactory(factory);
//json序列化配置
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);
//String序列化配置
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
自定义Redis工具类
八、Redis.conf配置文件
容量单位不区分大小写,G和GB有区别
可以使用 include 组合多个配置文件
网络配置
通用配置
日志输出
持久化规则
由于Redis是基于内存的数据库,需要将数据由内存持久化到文件中
- 持久化方式:RDB、AOF
RDB文件相关
设置密码
客户端连接相关
maxmemory-policy 六种方式
- volatile-lru:只对设置了过期时间的key进行LRU(默认值)
- allkeys-lru : 删除lru算法的key
- volatile-random:随机删除即将过期key
- allkeys-random:随机删除
- volatile-ttl : 删除即将过期的
- noeviction : 永不过期,返回错误
AOF相关
九、Redis持久化
RDB(Redis Databases)
什么是RDB?
在指定时间间隔后,将内存中的数据集快照写入数据库 ;在恢复时候,直接读取快照文件,进行数据的恢复 ;
默认情况下, Redis 将数据库快照保存在名字为dump.rdb的二进制文件中。文件名可以在配置文件中进行自定义。
工作原理
在进行 RDB
的时候,redis
的主线程是不会做 io
操作的,主线程会 fork
一个子线程来完成该操作;
- Redis 调用forks。同时拥有父进程和子进程。
- 子进程将数据集写入到一个临时 RDB 文件中。
- 当子进程完成对新 RDB 文件的写入时,Redis 用新 RDB 文件替换原来的 RDB 文件,并删除旧的 RDB 文件。
这种工作方式使得 Redis 可以从写时复制(copy-on-write)机制中获益(因为是使用子进程进行写操作,而父进程依然可以接收来自客户端的请求。)
触发机制
- save的规则满足的情况下,会自动触发rdb原则
- 执行flushall命令,也会触发我们的rdb原则
- 退出redis,也会自动产生rdb文件
save
使用 save
命令,会立刻对当前内存中的数据进行持久化 ,但是会阻塞,也就是不接受其他操作了;
由于 save
命令是同步命令,会占用Redis的主进程。若Redis数据非常多时,save命令执行速度会非常慢,阻塞所有客户端的请求。
flushall命令
flushall
命令也会触发持久化 ;
bgsave
bgsave
是异步进行,进行持久化的时候,redis 还可以将继续响应客户端请求 ;
bgsave和save对比
save | bgsave | |
---|---|---|
IO类型 | 同步 | 异步 |
阻塞? | 是 | 是(阻塞发生在fock(),通常非常快) |
复杂度 | O(n) | O(n) |
优点 | 不会消耗额外的内存 | 不阻塞客户端命令 |
缺点 | 阻塞客户端命令 | 需要fock子进程,消耗内存 |
bgsave优点:
- 适合大规模的数据恢复
- 对数据的完整性要求不高
bgsave缺点:
- 需要一定的时间间隔进行操作,如果redis意外宕机了,这个最后一次修改的数据就没有了。
- fork进程的时候,会占用一定的内容空间。
恢复rdb文件
- 将rdb文件放到redis的启动目录下,redis启动的时候就会自动检查dump.rdb,恢复其中的数据
- 查看dump.rdb文件需要存在的目录:
AOF(Append Only File)
什么是AOF?
以日志的形式来记录每个写的操作,将Redis执行过的所有指令记录下来(读操作不记录),只许追加文件但不可以改写文件;
redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。
快照功能(RDB)并不是非常耐久(durable): 如果 Redis 因为某些原因而造成故障停机, 那么服务器将丢失最近写入、以及未保存到快照中的那些数据。 从 1.1 版本开始, Redis 增加了一种完全耐久的持久化方式: AOF 持久化。
如何使用AOF?
-
修改配置文件:
-
然后重启redis,就可以生效了!
如果这个aof文件有错误,这时候redis是启动不起来的,我需要修复这个aof文件
redis给我们提供了一个工具redis-check-aof
使用命令:redis-check-aof --fix
可以修复aof文件,但会损失一部分数据
重写规则说明
AOF的优缺点
优点
- 每一次修改都会同步,文件的完整性会更加好
- 没秒同步一次,可能会丢失一秒的数据
- 从不同步,效率最高
缺点
- 相对于数据文件来说,aof远远大于rdb,修复速度比rdb慢!
- Aof运行效率也要比rdb慢,所以我们redis默认的配置就是rdb持久化
RDB和AOP选择
对比
RDB | AOF | |
---|---|---|
启动优先级 | 低 | 高 |
体积 | 小 | 大 |
恢复速度 | 快 | 慢 |
数据安全性 | 丢数据 | 根据策略决定 |
如何选择使用哪种持久化方式?
一般来说, 如果想达到足以媲美 PostgreSQL 的数据安全性, 你应该同时使用两种持久化功能。
如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失, 那么你可以只使用 RDB 持久化。
有很多用户都只使用 AOF 持久化, 但并不推荐这种方式: 因为定时生成 RDB 快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快。
总结:
- 如果只做缓存,数据在服务器运行时候存在,可以不使用持久化;
- 如果条件允许,可以两个持久化方式同时开启
十、Redis发布与订阅
Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。
下图展示了频道 channel1 , 以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 之间的关系:
当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端:
常用命令
-
PSUBSCRIBE pattern [pattern..]
:订阅一个或多个符合给定模式的频道。 -
PUNSUBSCRIBE pattern [pattern..]
: 退订一个或多个符合给定模式的频道。 -
PUBSUB subcommand [argument[argument]]
: 查看订阅与发布系统状态。
-
PUBLISH channel message
: 向指定频道发布消息
-
SUBSCRIBE channel [channel..]
: 订阅给定的一个或多个频道。
-
UNSUBSCRIBE channel [channel..]
: 退订一个或多个频道
订阅发布的原理
每个 Redis 服务器进程都维持着一个表示服务器状态的 redis.h/redisServer 结构, 结构的 pubsub_channels 属性是一个字典, 这个字典就用于保存订阅频道的信息;
其中,字典的键为正在被订阅的频道, 而字典的值则是一个链表, 链表中保存了所有订阅这个频道的客户端。
客户端订阅,就被链接到对应频道的链表的尾部,退订则就是将客户端节点从链表中移除。
缺点
- 如果一个客户端订阅了频道,但自己读取消息的速度却不够快的话,那么不断积压的消息会使redis输出缓冲区的体积变得越来越大,这可能使得redis本身的速度变慢,甚至直接崩溃。
- redis发布订阅和数据传输可靠性有关,如果在订阅方断线,那么他将会丢失所有在短线期间发布者发布的消息。
实际应用
- 消息订阅:公众号订阅,微博关注等等(起始更多是使用消息队列来进行实现)
- 多人在线聊天室。
稍微复杂的场景,我们就会使用消息中间件MQ处理。
十一、Redis主从复制
主从复制的概念
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。
前者称为主节点(Master/Leader),后者称为从节点(Slave/Follower);
数据的复制是单向的!只能由主节点复制到从节点(主节点以写为主、从节点以读为主)。
默认情况下,每台Redis服务器都是主节点,一个主节点可以有0个或者多个从节点,但每个从节点只能由一个主节点。
- 主从复制,读写分离,一主二仆:
主从复制的作用
- 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余的方式。
- 故障恢复:当主节点故障时,从节点可以暂时替代主节点提供服务,是一种服务冗余的方式
- 负载均衡:在主从复制的基础上,配合读写分离,由主节点进行写操作,从节点进行读操作,分担服务器的负载;尤其是在多读少写的场景下,通过多个从节点分担负载,提高并发量。
- 高可用基石:主从复制还是哨兵和集群能够实施的基础。
redis集群环境搭建
为什么使用集群?
- 单台服务器难以负载大量的请求
- 单台服务器故障率高,系统崩坏概率大
- 单台服务器内存容量有限。(单台Redis最大使用内存不应该超过20G)
环境配置:
-
查看当前库的信息:
info replication
-
复制多个配置文件。每个配置文件修改以下信息:端口号、pid文件名、日志文件名、rdb文件名
-
启动单机多服务Redis集群
-
一主二从配置
默认情况下,每台Redis服务器都是主节点;我们一般情况下只用配置从机就好了!
一主(79)二从(80,81)
使用命令SLAVEOF host port
就可以为从机配置主机了。
-
在主机上也可以查看从机状态
注意:命令配置是暂时的,在真实的项目中,我们要去配置文件中永久配置
使用规则
-
主机负责写,从机只能读,主机的数据从机也可以读取
-
当主机断电宕机后,默认情况下从机的角色不会发生变化 ,集群中只是失去了写操作,当主机恢复以后,又会连接上从机恢复原状。
-
当从机断电宕机后,若不是使用配置文件配置的从机,再次启动后会作为主机,是无法获取之前主机的数据的,若此时重新配置称为从机,又可以获取到主机的所有数据。这里就要提到一个复制原理。
复制原理
Slave (从机)启动成功连接到 master (主机)后会发送一个sync同步命令
Master 接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行
完毕之后,master将传送整个数据文件到slave,并完成一次完全同步。
全量复制:而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。
增量复制:Master 继续将新的所有收集到的修改命令依次传给slave,完成同步
但是只要是重新连接master,一次完全同步(全量复制)将被自动执行
层层链路
此时80既是79的从机,也是81的主机
当79宕机后,80可以继续进行写操作
此时,80和81还是只能进行读取操作,不能写入;
当真正的主机79宕机后,可以使用命令slaveof no one
将80独立为主机,此时80就可以作为主机进行写入操作
等到真正的主机79修复后,也不能再变为集群的主机了
哨兵模式
概述
主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。Redis从2.8开始正式提供了Sentinel(哨兵) 架构来解决这个问题。
哨兵模式能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库。
哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。
这里的哨兵有两个作用
- 通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器。
- 当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。
然而一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。
假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线。
当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover[故障转移]操作。
切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。
测试哨兵模式!
目前的状态是 一主二从!
-
新建一个哨兵配置文件 sentinel.conf
-
启动哨兵:
redis-sentinel configfill/sentinel.conf
-
主机宕机后
如果主机此时回来了,只能归并到新的主机下,当做从机,这就是哨兵模式的规则!
哨兵模式的优缺点
优点:
- 哨兵集群,基于主从复制模式,所有的主从配置优点,它全有
- 主从可以切换,故障可以转移,系统的可用性就会更好
- 哨兵模式就是主从模式的升级,手动到自动,更加健壮!
缺点:
- Redis 很难在线扩容的,集群容量一旦到达上限,在线扩容就十分麻烦!
- 实现哨兵模式的配置其实是很麻烦的,里面有很多选择!
哨兵模式的全部配置!
# Example sentinel.conf
# 哨兵sentinel实例运行的端口 默认26379,如果有多个哨兵,配置不同端口
port 26379
# 哨兵sentinel的工作目录
dir /tmp
# 哨兵sentinel监控的redis主节点的 ip port
# master-name 可以自己命名的主节点名字 只能由字母A-z、数字0-9 、这三个字符".-_"组成。
# quorum 配置多少个sentinel哨兵统一认为master主节点失联 那么这时客观上认为主节点失联了
# sentinel monitor <master-name> <ip> <redis-port> <quorum>
sentinel monitor mymaster 127.0.0.1 6379 2
# 当在Redis实例中开启了requirepass foobared 授权密码 这样所有连接Redis实例的客户端都要提供密码
# 设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码
# sentinel auth-pass <master-name> <password>
sentinel auth-pass mymaster MySUPER--secret-0123passw0rd
# 指定多少毫秒之后 主节点没有应答哨兵sentinel 此时 哨兵主观上认为主节点下线 默认30秒
# sentinel down-after-milliseconds <master-name> <milliseconds>
sentinel down-after-milliseconds mymaster 30000
# 这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行 同步,这个数字越小,完成failover所需的时间就越长,但是如果这个数字越大,就意味着越 多的slave因为replication而不可用。可以通过将这个值设为 1 来保证每次只有一个slave 处于不能处理命令请求的状态。
# sentinel parallel-syncs <master-name> <numslaves>
sentinel parallel-syncs mymaster 1
# 故障转移的超时时间 failover-timeout 可以用在以下这些方面:
#1. 同一个sentinel对同一个master两次failover之间的间隔时间。
#2. 当一个slave从一个错误的master那里同步数据开始计算时间。直到slave被纠正为向正确的master那里同步数据时。
#3.当想要取消一个正在进行的failover所需要的时间。
#4.当进行failover时,配置所有slaves指向新的master所需的最大时间。不过,即使过了这个超时,slaves依然会被正确配置为指向master,但是就不按parallel-syncs所配置的规则来了
# 默认三分钟
# sentinel failover-timeout <master-name> <milliseconds>
sentinel failover-timeout mymaster 180000
# SCRIPTS EXECUTION
#配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知相关人员。
#对于脚本的运行结果有以下规则:
#若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10
#若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行。
#如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。
#一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个SIGKILL信号终止,之后重新执行。
#通知型脚本:当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等),将会去调用这个脚本,这时这个脚本应该通过邮件,SMS等方式去通知系统管理员关于系统不正常运行的信息。调用该脚本时,将传给脚本两个参数,一个是事件的类型,一个是事件的描述。如果sentinel.conf配置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则sentinel无法正常启动成功。
#通知脚本
# shell编程
# sentinel notification-script <master-name> <script-path>
sentinel notification-script mymaster /var/redis/notify.sh
# 客户端重新配置主节点参数脚本
# 当一个master由于failover而发生改变时,这个脚本将会被调用,通知相关的客户端关于master地址已经发生改变的信息。
# 以下参数将会在调用脚本时传给脚本:
# <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port>
# 目前<state>总是“failover”,
# <role>是“leader”或者“observer”中的一个。
# 参数 from-ip, from-port, to-ip, to-port是用来和旧的master和新的master(即旧的slave)通信的
# 这个脚本应该是通用的,能被多次调用,不是针对性的。
# sentinel client-reconfig-script <master-name> <script-path>
sentinel client-reconfig-script mymaster /var/redis/reconfig.sh # 一般都是由运维来配置!
十二、Redis缓存穿透和雪崩
Redis缓存的使用,极大的提升了应用程序的性能和效率,特别是数据查询方面。
但同时,它也带来了一些问题。其中,最要害的问题,就是数据的一致性问题,从严格意义上讲,这个问题无解。如果对数据的一致性要求很高,那么就不能使用缓存。
另外的一些典型问题就是,缓存穿透、缓存雪崩和缓存击穿。目前,业界也都有比较流行的解决方案。
缓存穿透(查不到需要的数据)
概念
缓存穿透的概念很简单,用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于是向持久层数据库查询。发现也没有,于是本次查询失败。
当用户很多的时候,缓存都没有命中(秒杀!),于是都去请求了持久层数据库。这会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透。
解决方案
布隆过滤器
布隆过滤器是一种数据结构,对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃,从而避免了对底层存储系统的查询压力;
缓存空对象
当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源;
缓存空对象存在的问题:
- 占空间:如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键;
- 容易引发数据不一致:即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。
缓存击穿(同一数据访问量太大或缓存过期)
概述
这里需要注意和缓存击穿的区别,缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
当某个key在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来查询最新数据,并且回写缓存,会导使数据库瞬间压力过大。
解决方案
设置热点数据长时间内不过期(例如微博热搜)
从缓存层面来看,没有设置过期时间,所以不会出现热点 key 过期后产生的问题。
加互斥锁(setnx)
分布式锁:使用分布式锁,保证对于每个key同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可。
这种方式将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大。
缓存雪崩
概念
缓存雪崩,是指在某一个时间段,缓存集中过期失效!
比如马上就要到双十二零点,很快就会迎来一波抢购,这波商品时间比较集中的放入了缓存,假设缓存一个小时。那么到了凌晨一点钟的时候,这批商品的缓存就都过期了。
而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。
其实集中过期,倒不是非常致命,
比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网。因为自然形成的缓存雪崩,一定是在某个时间段集中创建缓存,这个时候,数据库也是可以顶住压力的。无非就是对数据库产生周期性的压力而已(可应对)。
而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知的,很有可能瞬间就把数据库压垮(不好应对)。
解决方案
redis高可用
这个思想的含义是,既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。(异地多活!)
限流降级
这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
数据预热
数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。
在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。