参考内容:
-
B站尚硅谷Redis视频教程 《Redis 6 入门到精通 超详细 教程》
-
B张黑马程序员Redis视频教程 《黑马程序员Redis入门到实战教程,全面透析redis底层原理+redis分布式锁+企业解决方案+redis实战》
-
优快云博主 程序员囧辉 博文《(22条消息) 全网最硬核 Redis 高频面试题解析(2021年最新版)_程序员囧辉的博客-优快云博客_redis高频面试题》
-
优快云博主 张维鹏 博文 《Redis的五种数据结构的底层实现原理》
参考内容:
- B站尚硅谷Redis视频教程 《Redis 6 入门到精通 超详细教程》
Redis笔记
1 Redis基础介绍
-
redis启动
- 前台启动 ---->redis-server
- 后台启动
- 修改配置文件,将/opt/reids/redis.conf文件中的daemonize 的no值修改为yes,即支持后台启动
- redis-server /opt/redis/redis.conf即可后台启动(也可将redis.conf复制到其它的目录,启动时使用该路径即可)
-
访问redis客户端
- [root@localhost ~]# redis-cli
- 127.0.0.1:6379>[username] password
- OK
-
redis的关闭
-
进入客户端后使用命令shutdown
-
找到redis-server的线程号,直接杀死该线程
-
-
相关知识
- 单线程及多路IO复用
- 支持持久化
- 支持多种数据类型
- 非关系型数据库
-
redis相关命令
-
键相关命令
命令 作用 keys 查看当前数据库的所有键 set key value 添加键值 exists key 判断某个key是否存在 type key 查看key的类型 del key 删除指定的key数据 unlink key 根据value选择非阻塞删除 expire key seconds 给指定的Key设置过期时间 ttl key 查看当前key还有多少秒过期,-1表示永不过期,-2表示已过期 select nums 切换数据库,redis默认有16个数据库,默认使用0号数据库 dbsize 产看当前数据库key的数量 flushdb 清空当前库 flushall 清空所有的库
-
-
redisObject类型
typedef struct redisObject { unsigned type:4; //对象的数据类型 unsigned encoding:4; //对象的编码格式 unsigned lru:LRU_BITS; /* lru time (relative to server.lruclock) */ int refcount; void *ptr; //指向真正的数据 } robj;
- type的五种取值
- OBJ_STRING
- OBJ_LIST
- OBJ_SET
- OBJ_ZSET
- OBJ_HASH
- encoding的取值
- OBJ_ENCODING_RAW: 最原生的表示方式。其实只有string类型才会用这个encoding值(表示成sds)。
- OBJ_ENCODING_INT: 表示成数字。实际用long表示。
- OBJ_ENCODING_HT: 表示成dict。
- OBJ_ENCODING_ZIPMAP: 是个旧的表示方式,已不再用。在小于Redis 2.6的版本中才有。
- OBJ_ENCODING_LINKEDLIST: 也是个旧的表示方式,已不再用。
- OBJ_ENCODING_ZIPLIST: 表示成ziplist。
- OBJ_ENCODING_INTSET: 表示成intset。用于set数据结构。
- OBJ_ENCODING_SKIPLIST: 表示成skiplist。用于sorted set数据结构。
- OBJ_ENCODING_EMBSTR: 表示成一种特殊的嵌入式的sds。
- OBJ_ENCODING_QUICKLIST: 表示成quicklist。用于list数据结构。
- 作用
- 为多种数据类型提供一种统一的表示方式。
- 允许同一类型的数据采用不同的内部表示,从而在某些情况下尽量节省内存。
- 支持对象共享和引用计数。当对象被共享的时候,只占用一份内存拷贝,进一步节省内存。
2 Redis数据类型
2.1 字符串类型(String)
2.1.1 相关命令
命令 | 作用 |
set key value | 添加键值对 |
get key | 查询对应键的值 |
append key value | 将给定的value追加到原value的末尾 |
strlen key | 获取当前键的值的长度 |
setnx key value | 当key不存在时,设置key的值 |
incr key | 将键值增加1,只能对数字值进行操作,若当前key不存在则将Key的值设为1 |
decr key | 将键值减少1,只能对数字值进行操作,若当前key不存在则将Key的值设为-1 |
incrby / decrby key step | 与incr和decr一致,可以自定义步长 step |
mset key1 value1 key2 value2 … | 同时设置一个或者多个键值对 |
mget key1 key2… | 同时获取多个value |
msetnx key1 value1 key2 value2… | 同时设置一个或者多个键值对,当且仅当所有的Key都不存在时才会设置 |
getrange key start end | 获取值的范围,与java中的substring类似,前后都是闭包 |
setrange key start value | 用value的值覆盖从start开始的值,有几位覆盖几位 |
setex key seconds value | 设置键值的同时设置过期时间 |
getset key value | 得到旧的值,同时用value替换原有的值 |
2.1.2 数据结构
- type = OBJ_STRING
- encoding = OBJ_ENCODING_RAW: string采用原生的表示方式,即用sds来表示
- encoding = OBJ_ENCODING_EMBSTR: string采用一种特殊的嵌入式的sds来表示
- encoding = OBJ_ENCODING_INT: string采用数字的表示方式,实际上是一个long型。
- String类型时二进制安全的,意味着redis的String可以包含任何类型的数据
- 简单动态字符串(Simple Dynamical String),需要时才扩容,类似于Java中的ArrayList,1M以下增加1倍,1M以上一次增加1M,最大512M。
- SDS保存了字符串的长度
- SDS可以预分配空间,修改SDS时先检查SDS空间是否足够,不够会先扩展SDS的空间,防止缓存溢出。
2.2 列表(List)
2.2.1 相关命令
命令 | 作用 |
lpush/rpush key1 value1 key2 value2 | 从左边/右边插入一个或者多个值 |
lpop/rpop key | 从左边或者右边弹出一个值,值在键在,值光键亡 |
lrange key start stop | 按照索引下标获得元素,从左到右,0表示左边第一个,-1表示右边第一个 |
lindex key index | 根据index的值获取元素(从左到右) |
llen key | 获取链表的长度 |
linsert key before/after value newvalue | 在value的前面或者后面插入newvalue |
lrem key n value | 从左边删除n个指定的value |
lset key index value | 将index处的值替换为value |
2.2.2 数据结构
- 快速链表 quicklist(宏观上是双向链表)
- 数据少时是ziplist,元素挨着存储,分配的内存空间是连续的
- 由表头和N个entry节点和压缩列表尾部标识符zlend组成的一个连续的内存块。然后通过一系列的编码规则,提高内存的利用率,主要用于存储整数和比较短的字符串。可以看出在插入和删除元素的时候,都需要对内存进行一次扩展或缩减,还要进行部分数据的移动操作,这样会造成更新效率低下的情况
- 数据少时是ziplist,元素挨着存储,分配的内存空间是连续的
- 数据量较多时改为quicklist,多个ziplist使用双向指针穿起来
2.3 Set
2.3.1 常用命令
命令 | 作用 |
SADD key member | 向set中添加一个或多个元素 |
SISMEMBER key member | 判断一个元素是否存在于set中 |
SREM key member | 移除set中的指定元素 |
SCARD key | 返回set中元素的个数 |
SMEMBERS | 获取set中的所有元素 |
SINTER key1 key2 | 求key1与key2的交集 |
SDIFF key1 key2 | 求key1与key2的差集 |
SUNION key1 key2 | 求key1和key2的并集 |
2.3.2 数据结构
-
intset:集合中都是整数,且数据量不超过512个,使用intset(有序且不重复的连续空间)
- String类型的set集合,底层是value为null的hash表,dict的结构
2.4 Hash
2.4.1 常用命令
命令 | 作用 |
HSET key field value | 添加或者修改hash类型key的filed的值 |
HGET key field | 获取一个hash类型的key的field的值 |
HMSET key field value [field value …] | 设置一个hash类型的key的多个field的值 |
HMGET key field [field …] | 获取一个hash类型的key的多个field的值 |
HGETALL key | 获取一个hash类型的key的所有field和value |
HKEYS key | 获取一个hash类型的key的所有field |
HVALS key | 获取一个hash类型的key的所有value |
HSETNX key field value | 设置filed-value,不存在就设置,存在则无效 |
2.4.2 数据结构
-
field : value(类似HashMap<String,Object>)
-
set数据较少:ziplist
- 键的个数小于512,值的大小不超过64字节
-
数据较多: hashtable
2.5 有序集合Zset(sorted set)
2.5.1常用命令
命令 | 作用 |
ZADD key score member | 添加一个或多个元素到sorted set ,如果已经存在则更新其score值 |
ZREM key member | 删除sorted set中的一个指定元素 |
ZSCORE key member | 获取sorted set中的指定元素的score值 |
ZRANK key member | 获取sorted set 中的指定元素的排名 |
ZCARD key | 获取sorted set中的元素个数 |
ZCOUNT key min max | 统计score值在给定范围内的所有元素的个数 |
ZINCRBY key increment member | 让sorted set中的指定元素自增,步长为指定的increment值 |
ZRANGE key min max | 按照score排序后,获取指定排名范围内的元素 |
ZRANGEBYSCORE key min max | 按照score排序后,获取指定score范围内的元素 |
ZDIFF、ZINTER、ZUNION | 求差集、交集、并集 |
2.5.2数据结构
- 有序的set集合,每个元素关联一个评分(score),元素唯一,但是不同元素的score可以重复
- ziplist
- 键值对个数小于128,ziplist数据项小于256
- 集合中每个数据的大小都要小于64字节
- 类似于Map<String,Double>
- hash表关联value和score
- 跳跃表用于给value排序,根据score的范围获取元素列表
2.6 Redis底层数据结构
2.6.1 redisObject类型
typedef struct redisObject {
unsigned type:4; //对象的数据类型
unsigned encoding:4; //对象的编码格式
unsigned lru:LRU_BITS; /* lru time (relative to server.lruclock) */
int refcount;
void *ptr; //指向真正的数据
} robj;
- type的五种取值
- OBJ_STRING, OBJ_LIST, OBJ_SET, OBJ_ZSET, OBJ_HASH
- encoding的取值
- OBJ_ENCODING_RAW: 最原生的表示方式。其实只有string类型才会用这个encoding值(表示成sds)。
- OBJ_ENCODING_INT: 表示成数字。实际用long表示。
- OBJ_ENCODING_HT: 表示成dict。
- OBJ_ENCODING_ZIPMAP: 是个旧的表示方式,已不再用。在小于Redis 2.6的版本中才有。
- OBJ_ENCODING_LINKEDLIST: 也是个旧的表示方式,已不再用。
- OBJ_ENCODING_ZIPLIST: 表示成ziplist。
- OBJ_ENCODING_INTSET: 表示成intset。用于set数据结构。
- OBJ_ENCODING_SKIPLIST: 表示成skiplist。用于sorted set数据结构。
- OBJ_ENCODING_EMBSTR: 表示成一种特殊的嵌入式的sds。
- OBJ_ENCODING_QUICKLIST: 表示成quicklist。用于list数据结构。
- 作用
- 为多种数据类型提供一种统一的表示方式。
- 允许同一类型的数据采用不同的内部表示,从而在某些情况下尽量节省内存。
- 支持对象共享和引用计数。当对象被共享的时候,只占用一份内存拷贝,进一步节省内存。
2.6.2 SDS(Simple Dynamic String)
- sds特点
- 可以动态扩展内存
- 采用预分配空间的方式减少内存的频繁分配
- 二进制安全
- 不同的编码实现
- type = OBJ_STRING
- encoding = OBJ_ENCODING_RAW: string采用原生的表示方式,即用sds来表示
- encoding = OBJ_ENCODING_EMBSTR: string采用一种特殊的嵌入式的sds来表示
- encoding = OBJ_ENCODING_INT: string采用数字的表示方式,实际上是一个long型。
- type = OBJ_STRING
- 完整结构由如下两个在内存地址上前后相连的两部分组成
- header
- 字符串的长度(len) 表示字符串的真正长度,不包含NULL结束符
- 最大容量(alloc) 表示字符串的最大容量,不包含最后多余的一个字节
- flags 表示Header的类型,一共五种,固定用一个字节的第三位表示
- 字符数组
- 数组的长度等于最大容量+1,为了当字符串的实际长度等于最大容量时,末尾仍可以由一个字节存放NULL结束符
- 真正的字符串数据后还有一个NULL结束符,为了和C语言中的字符串兼容
- header
2.6.3 dict
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;
} dictEntry;
typedef struct dictType {
unsigned int (*hashFunction)(const void *key);
void *(*keyDup)(void *privdata, const void *key);
void *(*valDup)(void *privdata, const void *obj);
int (*keyCompare)(void *privdata, const void *key1, const void *key2);
void (*keyDestructor)(void *privdata, void *key);
void (*valDestructor)(void *privdata, void *obj);
} dictType;
/* This is our hash table structure. Every dictionary has two of this as we
* implement incremental rehashing, for the old to the new table. */
typedef struct dictht {
dictEntry **table;
unsigned long size;//总是2的指数
unsigned long sizemask;//size-1 用于将key的hash映射到对应的bucket
unsigned long used;//已有的数据个数
} dictht;
typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2];
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
int iterators; /* number of iterators currently running */
} dict;
- 包含两个哈希表,当发生重哈希时,数据从第一个哈希表向第二个哈希表迁移
2.6.4 ziplist
- ziplist使用一块连续的内存空间来存储数据,并采用可变长的编码方式,支持不同类型和大小的数据的存储,比较节省内存。而且数据存储在一片连续的内存空间,读取的效率也非常高。
2.6.5 quicklistt
- quicklist是一个双向链表,而且是一个基于ziplist的双向链表,quicklist的每个节点都是一个ziplist
- ziplist长度的确定
- 参数值大于0表示ziplist中的元素个数
- 参数值小于0表示ziplist的内存大小
-
通过如下的参数确定
list-max-ziplist-size -2
2.6.6 intset
- intset是一个由整数组成的有序集合,从而便于进行二分查找,用于快速地判断一个元素是否属于这个集合。它在内存分配上与ziplist有些类似,是连续的一整块内存空间,而且对于大整数和小整数(按绝对值)采取了不同的编码,尽量对内存的使用进行了优化
2.6.7 skiplist
- 跳表是一种可以进行二分查找的有序链表,采用空间换时间的设计思路,跳表在原有的有序链表上面增加了多级索引(例如每两个节点就提取一个节点到上一级),通过索引来实现快速查找。跳表是一种动态数据结构,支持快速的插入、删除、查找操作,时间复杂度都为O(logn),空间复杂度为 O(n)。跳表非常灵活,可以通过改变索引构建策略,有效平衡执行效率和内存消耗
- skiplist与hashtable,AVL的比较
- skiplist和各种平衡树(如AVL、红黑树等)的元素是有序排列的,而哈希表不是有序的。因此,在哈希表上只能做单个key的查找,不适宜做范围查找。所谓范围查找,指的是查找那些大小在指定的两个值之间的所有节点。
- 在做范围查找的时候,平衡树比skiplist操作要复杂。在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。而在skiplist上进行范围查找就非常简单,只需要在找到小值之后,对第1层链表进行若干步的遍历就可以实现。
- 平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而skiplist的插入和删除只需要修改相邻节点的指针,操作简单又快速。
- 从内存占用上来说,skiplist比平衡树更灵活一些。一般来说,平衡树每个节点包含2个指针(分别指向左右子树),而skiplist每个节点包含的指针数目平均为1/(1-p),具体取决于参数p的大小。如果像Redis里的实现一样,取p=1/4,那么平均每个节点包含1.33个指针,比平衡树更有优势。
- 查找单个key,skiplist和平衡树的时间复杂度都为O(log n),大体相当;而哈希表在保持较低的哈希值冲突概率的前提下,查找时间复杂度接近O(1),性能更高一些。所以我们平常使用的各种Map或dictionary结构,大都是基于哈希表实现的。
- 从算法实现难度上来比较,skiplist比平衡树要简单得多。
- Redis中的zset
typedef struct zskiplistNode {
robj *obj;
double score;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned int span;
} level[];
} zskiplistNode;
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist
2.7 Bitmaps
常用命令 | 作用 |
SETBIT key offset value | 向指定位置存入0或者1 |
GETBIT key offset | 获取指定位置的bit |
BITCOUNT key [start end] | 获取指定范围内1的个数 |
BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment] | 操作指定位置的bit type指定符号数(u为无符号,i为有符号) |
BITPOS key bit [start] [end] | 获取指定范围内第一个出现的1 |
2.8 HyperLogLog(基数计算)
- UV:全称Unique Visitor,也叫独立访客量,是指通过互联网访问、浏览这个网页的自然人。1天内同一个用户多次访问该网站,只记录1次。
- PV:全称Page View,也叫页面访问量或点击量,用户每访问网站的一个页面,记录1次PV,用户多次打开页面,则记录多次PV。往往用来衡量网站的流量。
- HyperLogLog通常用于基数统计。使用少量固定大小的内存,来统计集合中唯一元素的数量。统计结果不是精确值,而是一个带有0.81%标准差(standard error)的近似值。所以,HyperLogLog适用于一些对于统计结果精确度要求不是特别高的场景,例如网站的UV统计。
常用命令 | 作用 |
PFADD key element… | 添加数据到HyperLogLog |
PFCOUNT key | 统计HyperLogLog中的个数,有一定的误差 |
PFMERGE destkey sourcekey… | 将多个HyperLogLog合并为一个 |
2.9 Geospatial(地理信息,经纬度)
常用命令 | 作用 |
GEOADD key longitude latitude member | 添加一个或者多个位置的经纬度信息到某个集合 |
GEODIST key member1 member2 [m|km] | 返回集合中两个位置的距离 |
GEOHASH key member … | 返回某个位置的hash |
GEOPOS key member … | 返回某个位置的经纬度信息 |
GEOSERACH key [FROMMEMBER member] [FROMLONLAT longitude latitude] [BYRADIUS radius m|km] [ASC|DESC] [COUNT count [ANY]] [WITHCOORD] [WITHDIST] [WITHHASH] | 在指定范围内搜索member,并按照与指定点之间的距离排序后返回。范围可以是圆形或矩形 |
2.10 Stream
-
基于Stream可以实现一个相对完善的消息队列
-
常用命令
-
发送消息
-
- 读取消息
- 消费者组:将多个消费者放到一个组里,监控同一个队列
- 消息分流:队列中的消息会分流给组内的不同消费者,而不是重复消费,从而加快消息处理的速度
- 消息标示:消费者组会维护一个标示,记录最后一个被处理的消息,哪怕消费者宕机重启,还会从标示之后读取消息。确保每一个消息都会被消费
- 消息确认:消费者获取消息后,消息处于pending状态,并存入一个pending-list。当处理完成后需要通过XACK来确认消息,标记消息为已处理,才会从pending-list移除。
消费者组命令 | 作用 |
XGROUP CREATE key groupname ID|$ [MKSTREAM] | 创建消费者组 |
ID指定队列中消息位置,0表示第一个,$表示最后一个 | |
XGROUP DESTORY key groupName | 删除指定的消费者组 |
XGROUP CREATECONSUMER key groupname consumername | 给指定的消费者组添加消费者 |
XGROUP DELCONSUMER key groupname consumername | 删除消费者组中的指定消费者 |
-
group:消费组名称
-
consumer:消费者名称,如果消费者不存在,会自动创建一个消费者
-
count:本次查询的最大数量
-
BLOCK milliseconds:当没有消息时最长等待时间
-
NOACK:无需手动ACK,获取到消息后自动确认
-
STREAMS key:指定队列名称
-
ID:获取消息的起始ID:
-
“>”:从下一个未消费的消息开始
-
其它:根据指定id从pending-list中获取已消费但未确认的消息,例如0,是从pending-list中的第一个消息开始
-
-
-
确认已经处理过的消息
3 Redis客户端
3.1 Jedis
-
Jedis基本使用
package com.lzx;
import redis.clients.jedis.Jedis;
public class JedisTest {
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1", 6379);
String result = jedis.ping();
System.out.println(result);
jedis.close();
}
}
- Jedis连接池
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
public class JedisConnectionFactory {
private static final JedisPool jedisPool;
static{
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(8);
jedisPoolConfig.setMaxIdle(8);
jedisPoolConfig.setMinIdle(0);
jedisPool = new JedisPool(jedisPoolConfig,"127.0.0.1",6379,1000);
}
public static Jedis getJedis(){
return jedisPool.getResource();
}
}
3.2 Spring Data Redis
- 使用步骤
-
引入相关的依赖
-
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
- 在application.yml中配置Redis信息
spring:
redis:
host: 127.0.0.1
port: 6379
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
max-wait: 1000
- 注入RedisTemplate
@Autowired
private RedisTemplate redisTemplate;
@Test
void testString(){
redisTemplate.opsForValue().set("name","jack");
Object name = redisTemplate.opsForValue().get("name");
System.out.println("name"+name);
}
- RedisTemplate序列化方式
- RedisTemplate可以接受任意的Object作为值写入Redis,但是在写入之前会把Object序列化为字节形式,默认采用JDK序列化
- 可读性较差,内存占用大
- 自定义RedisTemplate的序列化方式
- 定义配置类,更改RedisTemplate的值和键的序列化方式
- RedisTemplate可以接受任意的Object作为值写入Redis,但是在写入之前会把Object序列化为字节形式,默认采用JDK序列化
@Configuration
public class RedisConfigure {
@Bean
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory connectionFactory){
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
template.setKeySerializer(RedisSerializer.string());
template.setHashKeySerializer(RedisSerializer.string());
template.setValueSerializer(jsonRedisSerializer);
template.setHashValueSerializer(jsonRedisSerializer);
return template;
}
}
- 在存放对象时,为了能够得到反序列化的对象,jackson序列化器会额外存放对象的class类信息,带来额外的内存开销
get user:1001
{"@class":"com.lzx.domain.User","name":"张飞","age":34}
- 为了节省内存,可以使用StringRedisTemplate对象,只能存放String类型的键和值,StringRedisTemplate的key和value默认使用StringRedisSerializer序列化器,当我们需要存放对象时,自己手动进行序列化,取出对象时,手动进行反序列化。
- 注入StringRedisTemplate类,手动实现序列化和反序列化
package com.lzx;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.lzx.domain.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.StringRedisTemplate;
@SpringBootTest
public class StringRedisTemplateTest {
@Autowired
private StringRedisTemplate stringRedisTemplate;
//序列化类
private static final ObjectMapper mapper = new ObjectMapper();
@Test
void testStringRedisTemplate() throws JsonProcessingException {
User user = new User("张三", 18);
String jsonString = mapper.writeValueAsString(user);
stringRedisTemplate.opsForValue().set("user:1002",jsonString);
String s = stringRedisTemplate.opsForValue().get("user:1002");
User res = mapper.readValue(s, User.class);
System.out.println(res);
}
}
4 Redis充当缓存
4.1 缓存更新策略
-
内存淘汰:低一致性需求
-
超时剔除:
-
主动更新:
-
实践方案
- 读操作
- 缓存命中,直接返回
- 缓存未命中则查询数据库,并写入缓存,设定超时时间
- 写操作
- 先写数据库,再删除缓存
- 确保数据库与缓存操作的原子性
- 读操作
4.2 缓存问题
4.2.1 缓存穿透
-
请求的数据在缓存和数据库中都不存在
-
解决方法:
-
接口校验
- 用户权限校验
- 数据合法性校验
-
缓存空对象
- 实现简单,维护方便
- 额外的内存消耗,造成短期的不一致
-
布隆过滤
- 内存占用较少,没有多余的key
- 实现复杂,存在误判
-
4.2.2 缓存雪崩
- 同一时间缓存中大量的Key失效或者Redis服务宕机
- 给不同的key的TTL添加随机值
- 利用Redis集群提高服务的可用性
- 给缓存业务添加降级限流策略
- 给业务添加多级缓存
4.2.3 缓存击穿
-
热点key问题,一个热点key(被高并发访问的且缓存重建业务较复杂)忽然失效
-
互斥锁
- 没有额外内存消耗,保证一致性,实现简单
- 线程需要等待,性能受到影响,有死锁风险
-
逻辑过期
- 线程无需等待,性能较好
- 不保证一致性,有额外的内存消耗,实现复杂
5 Redis的事务锁机制
5.1事务操作
5.1.1 事务操作流程
- multi 组队阶段 (开启事务)
- 添加命令1
- 添加命令2
- discard 抛弃命令队列(回滚事务)
- exec 执行阶段 (提交事务)
5.1.2 事务的错误处理
- 当添加命令阶段出现错误,当前命令队列将会被清空,无法执行
- 当执行命令阶段出现错误,发生错误的命令不会成功执行,队列中其它命令均可以正常执行
5.1.3 事务冲突
- 悲观锁:每次操作都上锁,一次只能一个操作
- 乐观锁:添加版本号,实际操作时判断版本号是否一致。适用于多读的应用类型,可以提高吞吐量。
- watch key 在开启事务之前执行watch key指令,可以监视一个或者多个key,如果在事务执行之前这些被监视的key发生过改动,那么事务将会被打断。
5.1.4 Redis事务三个特性
- 单独的隔离操作
- 没有隔离级别的概念
- 不保证原子性
6 Redis的持久化
6.1 Redis持久化之RDB (Redis Database Backup file)
- 在指定的时间间隔内将内存中的数据集快照写入磁盘,恢复时直接将快照文件读到内存里
- RDB持久化在四种情况下会执行:
- 执行save命令
- save命令
执行下面的命令,可以立即执行一次RDB:
save命令会导致主进程执行RDB,这个过程中其它所有命令都会被阻塞。只有在数据迁移时可能用到
- save命令
- 执行bgsave命令
- 这个命令执行后会开启独立进程完成RDB,主进程可以持续处理用户请求,不受影响。
-
Redis停机时会执行一次save命令,实现RDB持久化
-
触发RDB条件时
- 执行save命令
# 900秒内,如果至少有1个key被修改,则执行bgsave , 如果是save "" 则表示禁用RDB
save 900 1
save 300 10
save 60 10000
- 执行原理:bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据。完成fork后读取内存数据并写入 RDB 文件。fork采用的是copy-on-write技术:
- 当主进程执行读操作时,访问共享内存;
- 当主进程执行写操作时,则会拷贝一份数据,执行写操作。
- 执行过程:
- Redis单独创建(fork)一个子进程进行持久化
- 先将数据写入到一个临时文件中,等到持久化过程都结束了在用这个临时文件替换上次持久化好的文件
- 持久化文件的相关信息
- 在redis.conf中可以进行配置,默认为dump.rdb
- 文件的默认保存路径是当前路径 ./
# 是否压缩 ,建议不开启,压缩也会消耗cpu,磁盘的话不值钱
rdbcompression yes
# RDB文件名称
dbfilename dump.rdb
# 文件保存的路径目录
dir ./
- 快照的策略
- save seconds nums 在多少秒内写入了多少次就会触发快照
- save时只管保存,其他不管,全部阻塞
- rdb文件的恢复
- 先把rdb文件拷贝到其他位置
- 再将拷贝的文件放到工作目录下
- 启动redis,redis会加载rdb文件中的数据
- 优势
- 适合大规模的数据恢复
- 对数据完整性和一致性要求不高的更加适合
- 节省磁盘空间
- 恢复速度快
- 劣势
- RDB 在服务器故障时容易造成数据的丢失。RDB 允许我们通过修改 save point 配置来控制持久化的频率。但是,因为 RDB 文件需要保存整个数据集的状态, 所以它是一个比较重的操作,如果频率太频繁,可能会对 Redis 性能产生影响。所以通常可能设置至少5分钟才保存一次快照,这时如果 Redis 出现宕机等情况,则意味着最多可能丢失5分钟数据
- RDB 保存时使用 fork 子进程进行数据的持久化,如果数据比较大的话,fork 可能会非常耗时,造成 Redis 停止处理服务N毫秒。如果数据集很大且 CPU 比较繁忙的时候,停止服务的时间甚至会到一秒。
- Linux fork 子进程采用的是 copy-on-write 的方式。在 Redis 执行 RDB 持久化期间,如果 client 写入数据很频繁,那么将增加 Redis 占用的内存,最坏情况下,内存的占用将达到原先的2倍。刚 fork 时,主进程和子进程共享内存,但是随着主进程需要处理写操作,主进程需要将修改的页面拷贝一份出来,然后进行修改。极端情况下,如果所有的页面都被修改,则此时的内存占用是原先的2倍
6.2 Redis持久化之AOF (Append Only File)
- 以日志的形式来记录每个写操作,将redis执行过的所有的写指令记录下来,只允许追加文件但是不可以改写文件,redis重启时会将日志文件中记录的写指令重新执行一遍以完成数据的恢复工作。
- 持久化流程
- 写命令被追加到AOF缓冲区内
- AOF缓冲区根据AOF缓冲策略【always,everysec,no】将操作同步到磁盘的AOF文件中
# 表示每执行一次写命令,立即记录到AOF文件 appendfsync always # 写命令执行完先放入AOF缓冲区,然后表示每隔1秒将缓冲区数据写到AOF文件,是默认方案 appendfsync everysec # 写命令执行完先放入AOF缓冲区,由操作系统决定何时将缓冲区内容写回磁盘 appendfsync no
- AOF文件大小超过重写策略或者手动重写时,会通过bgrewirteaof命令对AOF文件重写,来压缩AOF文件容量
# AOF文件比上次文件 增长超过多少百分比则触发重写 auto-aof-rewrite-percentage 100 # AOF文件体积最小多大以上才触发重写 auto-aof-rewrite-min-size 64mb
-
redis服务重启时,会重新加载AOF文件中的写指令来达到数据恢复的目的
- aof文件的相关信息
- 可以在redis.conf配置文件中修改文件名称,默认名称是appendonly.aof
- aof文件的保存路径与rdb文件的保存路径一致,都是当前路径
# 是否开启AOF功能,默认是no appendonly yes # AOF文件的名称 appendfilename "appendonly.aof"
- AOF的启动与异常恢复
- 将配置文件中的appendonly no修改为yes
- 通过 redis-check-aof --fix appendonly.aof可以恢复异常的appendonly.aof文件
- AOF的同步频率
- appendsync always
- 始终同步,每次redis的写入都会立刻记入日志
- 性能较差但是数据完整性较好
- appendsync everysec
- 每秒同步,每秒计入日志一次
- 如果宕机,本秒的数据可能丢失
- appendsync no
- 不主动同步,将同步时机交给操作系统
- appendsync always
- 重写压缩(rewrite)
- no-appendfsync-on-rewrite
- yes时,当重写时,客户端的新的写操作只会写入aof_buf,不会同步到aof文件,用户的请求不会阻塞,但是如果宕机会丢失这段时间的缓存数据
- no时,当重写时,还会继续将数据写入aof文件,但是可能会发生阻塞,数据安全,但是性能低
- 重写流程
- 触发重写机制,判断当前是否有bgsave或者bgrewriteaof在运行,如果有则等待该命令结束后再继续执行
- 主进程fork子进程进行重写操作,保证主进程不会阻塞
- 子进程遍历redis内存中数据到临时文件,客户端的写请求同时写入aof_buf缓冲区和aof_rewrite_buf重写缓冲区保证原AOF文件完整以及新AOF文件生成期间的新的数据修改动作不会丢失。
- 子进程写完新的AOF文件后,向主进程发信号,父进程更新统计信息,主进程把aof_rewrite_buf中的数据写入到新的AOF文件。
- 使用新的AOF文件覆盖旧的AOF文件,完成AOF重写
- no-appendfsync-on-rewrite
6.3 混合持久化
- 发生在AOF重写时,重写后的文件前面部分是rdb数据,后半部分是AOF新增命令
- 配置文件中的相关解释,默认为开启状态
# When rewriting the AOF file, Redis is able to use an RDB preamble in the
# AOF file for faster rewrites and recoveries. When this option is turned
# on the rewritten AOF file is composed of two different stanzas:
# [RDB file][AOF tail]
# When loading, Redis recognizes that the AOF file starts with the "REDIS"
# string and loads the prefixed RDB file, then continues loading the AOF
# tail.
aof-use-rdb-preamble yes
7 Redis键过期与内存淘汰策略
7.1 Redis的过期键删除策略
- 定时删除:在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除操作。对内存最友好,对 CPU 时间最不友好。
- 惰性删除:放任键过期不管,但是每次获取键时,都检査键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。对 CPU 时间最优化,对内存最不友好。
- 定期删除:每隔一段时间,默认100ms,程序就对数据库进行一次检査,删除里面的过期键。至 于要删除多少过期键,以及要检査多少个数据库,则由算法决定。前两种策略的折中,对 CPU 时间和内存的友好程度较平衡。
7.2 Redis的内存淘汰策略
- 当redis的内存空间(maxmemory参数配置)已满时,redis将根据配置的淘汰策略(maxmemory-policy参数配置)进行内存淘汰,主要有以下8种
- noeviction:默认策略,不淘汰任何 key,直接返回错误
- allkeys-lru:在所有的 key 中,使用 LRU 算法淘汰部分 key
- allkeys-lfu:在所有的 key 中,使用 LFU 算法淘汰部分 key,该算法于 Redis 4.0 新增
- allkeys-random:在所有的 key 中,随机淘汰部分 key
- volatile-lru:在设置了过期时间的 key 中,使用 LRU 算法淘汰部分 key
- volatile-lfu:在设置了过期时间的 key 中,使用 LFU 算法淘汰部分 key,该算法于 Redis 4.0 新增
- volatile-random:在设置了过期时间的 key 中,随机淘汰部分 key
- volatile-ttl:在设置了过期时间的 key 中,挑选 TTL(time to live,剩余时间)短的 key 淘汰
8 Redis分布式缓存
8.1 主从赋值
8.1.1 主从搭建命令
- slaveof/replicaof host port
8.1.2 主从数据同步
8.1.2.1 全量同步
- 主从第一次建立连接时,会执行全量同步,将master节点的所有数据都拷贝给slave节点
- 两个概念
- Replication Id:简称replid,是数据集的标记,id一致则说明是同一数据集。每一个master都有唯一的replid,slave则会继承master节点的replid
- offset:偏移量,随着记录在repl_baklog中的数据增多而逐渐增大。slave完成同步时也会记录当前同步的offset。如果slave的offset小于master的offset,说明slave数据落后于master,需要更新。
- 主要步骤
- slave节点请求增量同步
- master节点判断replid,发现不一致,拒绝增量同步
- master将完整内存数据生成RDB,发送RDB到slave
- slave清空本地数据,加载master的RDB
- master将RDB期间的命令记录在repl_baklog,并持续将log中的命令发送给slave
- slave执行接收到的命令,保持与master之间的同步
8.1.2.2 增量同步
- 增量同步: 只更新slave与master存在差异的部分数据
- repl_backlog原理
- repl_baklog文件是一个固定大小的数组,只不过数组是环形,也就是说角标到达数组末尾后,会再次从0开始读写,这样数组头部的数据就会被覆盖。
- repl_baklog中会记录Redis处理过的命令日志及offset,包括master当前的offset,和slave已经拷贝到的offset:
slave与master的offset之间的差异,就是salve需要增量拷贝的数据了。随着不断有数据写入,master的offset逐渐变大,slave也不断的拷贝,追赶master的offset,知道数组被填满,此时,如果有新的数据写入,就会覆盖数组中的旧数据。不过,旧的数据只要是绿色的,说明是已经被同步到slave的数据,即便被覆盖了也没什么影响。因为未同步的仅仅是红色部分。
8.2 哨兵集群
8.2.1 哨兵的作用
- 监控:Sentinel 会不断检查您的master和slave是否按预期工作
- 自动故障恢复:如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主
- 通知:Sentinel充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端
8.2.2 哨兵监控原理:
- Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令
- 主观下线
- 如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线
- 客观下线
- 若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线。quorum值最好超过Sentinel实例数量的一半
8.2.3 故障恢复
- 选出领头sentinel,通常是最先发现master下线的sentinel
- 领头sentinel在下线的Master的slave中选取一个作为新的master,判断依据如下(亲信优先):
- 首先会判断slave节点与master节点断开时间长短,如果超过指定值(down-after-milliseconds * 10)则会排除该slave节点
- 然后判断slave节点的slave-priority值,越小优先级越高,如果是0则永不参与选举
- 如果slave-prority一样,则判断slave节点的offset值,越大说明数据越新,优先级越高
- 最后是判断slave节点的运行id大小,越小优先级越高。
- sentinel给选中的slave节点发送slaveof no one命令(独立宣言),让该节点成为master。
- sentinel通知其他剩余的slave节点,从新的master上开始复制 (俯首称臣)。
- sentinel将故障节点标记为slave,当故障节点恢复后自动成为新的master的slave节点。
8.3 分片集群
8.3.1 分片集群特征
- 集群中有多个master,每个master保存不同数据 (去中心化)
- 每个master都可以有多个slave节点(高可用)
- master之间通过ping监测彼此健康状态(哨兵特点)
- 客户端请求可以访问集群任意节点,最终都会被转发到正确节点(散列插槽)
8.3.2 散列插槽
- 集群共有2^14(16384)个插槽,集群将这些插槽分配到每个master节点上
- key的插槽计算规则:通过CRC16算法计算key的有效部分的hash,然后计算结果与2^14-1(16383)进行与运算(等价于对16384取余)
- key中包含"{}",且“{}”中至少包含1个字符,“{}”中的部分是有效部分
inel都认为该实例主观下线,则该实例客观下线。quorum值最好超过Sentinel实例数量的一半
- key中包含"{}",且“{}”中至少包含1个字符,“{}”中的部分是有效部分