一、Redis简介和总结思路
1.Redis简介:
由于普通的关系型数据库不能很好的解决数据量增长和读写数据的压力不断增加带来的存储问题,数据不断从单表演化出了多库多表的模式,数据库从单机演进出了集群,同时为了对冷热数据存储查询进行区分,加快热数据访问,需要利用起内存,减少磁盘读写,解决思路就是把热数据存储在内存中,这样就衍生出了Redis;
Redis是最常用的非关系型数据库之一,以键值对的方式存储数据,是一种高性能的键值存储系统,基于内存存储,支持AOF和RDB持久化存储,Redis支持丰富的数据类型和对象,包括字符串、哈希、列表、集合、有序集合等,常用于签到,对话存储,订阅,排行榜,消息通知,计数,限流和分布式锁等场景;由于Redis在内存中存储数据,并采用单线程模型,单线程处理所有操作命令,避免了多线程的竞争问题,因此具有非常高的读写性能。
2.Windows下Redis及其可视化工具安装:
Redis安装部署:
Redis的Github地址:github.com/MicrosoftAr…
下载zip文件并解压后,终端进入解压后的目录,依次输入指令:
redis-server redis.windows.conf,redis-server --service-install redis.windows.conf
完成后打开Windows的服务工具启动redis即可:
Another Redis DeskTop Manager工具下载安装:
Another Redis DeskTop Manager是Redis的开源免费可视化工具,自带控制台以及数据可视化,支持简体中文,支持Redis的大部分命令;
下载地址:AnotherRedisDesktopManager 发行版 - Gitee.com
下载exe文件并安装后填写IP端口连接Redis即可;
3.总结思路:
首先对Redis支持的几种重要数据结构依次进行解析学习总结,同时总结Redis针对不同数据结构的操作指令,接下来总结使用Golang连接Redis的方法和操作,介绍"github.com/go-redis/redis/v9"库,总结其提供的各种数据操作接口,最后给出Redis的注意事项及其出现问题的解决办法。
二、Redis的数据结构及其指令总结
Redis中的每个数据对象都由redisObject表示,redisObject有三个成员变量:
- type:标识对象的数据类型;
- encoding:表示该对象的编码方式,使用了底层的哪种数据结构;
- ptr:指向底层数据结构的指针;
因此Redis的数据结构不是指String(字符串)、List(列表)、Hash(哈希)、Set(集合)和 Zset(有序集合)对象,这些只是Redis的键值对中,值所支持的数据类型,其底层用于管理这些数据的才是Redis真正意义上的数据结构;
1.string对象:
Redis的String类型不同于普通的字符串String,它不仅可以存储文本字符串,还可以存储数字、二进制数据等,同时提供了增减操作、位操作等功能,常用于缓存、计数器、排行榜、会话管理等场景;
SDS数据结构:
SDS(Simple Dynamic String简单动态字符串)是Redis内部用于管理字符串值的数据结构;
SDS结构体包含以下字段:
- len:表示字符串的长度,即字符串值的实际字节数,方便获取长度而无需遍历;
- alloc:表示字符串中未使用的字节数,这个字段可以用来追加新数据而不需要重新分配内存,修改字符串时通过alloc-len计算剩余空间大小,如果大小不足,SDS会自动扩容再进行数据修改,避免溢出;
- flags:表示不同的sds类型;
- buf:指向实际存储字符串值或二进制数据的内存区域。
SDS数据结构还包含SDS指针,指针向buf方向移动获取value,向其他字段移动获取元信息。
常用指令:
//给键赋值(键存在)
set key value
//创建键值对(键不存在)
setnx key value
//获取键的值
get key value
//同时赋值多个键值对(键已存在);一个失败就全都失败
mset key1 value1 key2 value2 ...
//同时获取多个键值对的值
mget key1 value1 key2 value2 ...
//同时创建多个键值对(键不存在)
msetnx key1 value1 key2 value2
//给键追加值(追加到末尾)
append key value
//获取键值的长度
strlen key
//截取一定长度字符串的值
//begin起始位置,end截止位置,如果end=-1就表示直接截取到最后一个字符
//redis分正反顺序,左边起是0,1,2,右边起是-1,-2,-3,即首0尾-1
getrange key begin end
//给key的一定范围进行赋值
setrange key begin value end value
//设置键值对的过期时间,单位秒
setex key time value
//获取原来的值的同时并赋予新的值,且返回旧值
getset key new-value
//当值为int类型时
//只加1:
incr key
//只减1
decr key
//加n
incrby key n
//减n
decrby key n
部分演示:
2.list对象:
Redis中,List是一系列按插入顺序排列的元素集合,并且可以根据元素插入顺序或索引位置进行访问、添加、删除和更新操作,常用于实现消息队列、任务队列、最近访问列表等,注:list是有序可重复的,所以不同key对应的同一个value的键值对可以有多个;
Quicklist数据结构:
其底层数据结构是Quicklist,Quicklist又是由一个双向链表和listpack结合实现的,双向链表具有头尾指针,每一个结点都有指向前一结点和后一结点的两个指针prev和next,其中的每个结点的entry对应listpack,listpack实现参考Ziplist(压缩链表),但是舍弃了压缩链表中记录前一节点长度的字段,只记录当前节点长度,从而避免压缩链表连锁更新的问题,其对应有字段:
- tot-bytes:记录整个列表占用的内存字节数;
- num-elements,记录列表包含的节点数量;
- elements:列表节点;
- element-tot-len:记录了元素的总长度,type+data;
- element-type:记录了当前节点编码类型以及数据长度;
- element-data:记录了当前节点的实际数据;
- listpack-end-byte,标记列表的结束点。
常用指令:
//从左边/右边插入一个或多个值
lpush/rpush key value1 value2 value3 ...
//弹出(删除)左边/右边的第一个元素
lpop/rpop key
//从key1的右边弹出一个元素并将改元素插入到key2的左边
rpoplpush key1 key2
//按照索引下标获取元素(从左到右的顺序获取)
lrange key beginIndex endIndex
//获取该list中全部元素的值
lrange key 0 -1
//按照索引下标获取元素值(从左往右正向顺序)
lindex key index
//表示从右往左反向顺序获取第一个元素值,即尾元素值
lindex key -1
//获取list的长度
llen key
//在某一个元素值之前或者之后加入一个新的值
linsert key before/after value new-value
//将list中的某一个元素删除;
//count为正数,表示从左往右删除count个value;
//count为负数,表示从右往左删除count个value;
//count为0时,表示删除该集合中所有的这个value;
lrem key count value
部分演示:
3.Hash对象:
Hash数据类型可以用于存储多个键值对的集合,允许在一个键下存储多个字段及其对应的值,类似于关联数组或字典,其中字段必须唯一,Hash数据类型适用于存储对象(例如用户信息、文章等)的属性,其中每个属性都用字段表示,而对应的值则是属性的具体内容,常用于实现计数器、缓存、对象属性存储等;
HashTable哈希表数据结构和链式哈希:
哈希表使用散列函数将键转换为桶(bucket)的索引,每个桶中存储着一组键值对,因为哈希表实际上是数组,所以可以通过索引值快速查询到数据,借此实现存储查询多个键值对集合,但是会出现多个键通过散列函数映射到同一个桶即发生哈希冲突的情况;
Redis内部使用链式哈希来处理哈希冲突,在不扩容哈希表的前提下,将具有相同哈希值的数据串起来,哈希表节点结构中的dictEntry 结构里不仅包含指向键和值的指针,还包含了指向下一个哈希表节点的指针,这个指针可以将多个哈希值相同的键值对连接起来,以此来解决哈希冲突的问题;
dict数据结构和rehash:
dict:
Redis底层使用的不是简单的哈希表结构,为了保持良好的性能,防止单个哈希表的负载因子过高(哈希表负载因子=哈希表已保存节点数量/哈希表大小),采用了dict结构,dict包含两个哈希表,在数据量小,正常访问的情况下,数据插入只会占用哈希表1(ht[0]),哈希表2(ht[1])并没有分配空间;
rehash:
但是当数据量不断增多,就需要进行rehash操作,即先为ht[1]开辟比ht[0]大两倍左右的空间,把ht[0]中的数据迁移到ht[1]中,并且将ht[0]中的数据清空,把迁移后的ht[0]和ht[1]角色交换,等待下一次的rehash操作;
渐进式rehash:
当然由于rehash需要不断扩容,数据量会越来越大,此时迁移时会涉及大量的数据拷贝,会造成明显的用户操作阻塞,为了解决这个问题,使用了渐进式rehash的操作,其原理就是提前为两个哈希表都申请好空间,用户每次对Hash对象进行操作时,都同时进行少量的数据迁移,将整个迁移过程平摊到每次操作中,避免大量的数据拷贝。
常用指令:
//添加一个hash的集合
//其中key表示hash集合的键值
//field表示所存对象的字段属性,value表示属性值
hset key field value
hset user:1 name "John"
//给hash表中的某key的属性进行赋值,
//若不存在则创建属性并赋值,若存在则不进行赋值:
hsetnx key field value
//同时给hash对象中的多个属性赋值:
hmset key field1 value1 field2 value2 ...
//获取hash集合中的属性值:
hget key field
//获取hash表中某个key的所有属性及其属性值
hgetall key
//查看hash中的某个key的属性是否存在,存在返回1,不存在返回0
hexists key field
//获取hash表中某键的所有属性:
hkeys key
//获取hash表中某键的所有属性的属性值:
hvals key
//删除hash中的字段
hdel key field
//给hash表中某key的某个属性的值进行增量操作,count可为负数
hincrby key field count
部分演示:
4.set对象:
set是一种无序的、不重复的数据集合(区别于list,list是可重复的),set数据类型可以用来存储一组不重复的元素,它常用于存储关联性不强的数据,如用户标签、点赞列表、粉丝列表等,除了支持基本的增删查改操作外,还可以支持集合运算,如交并差补等,其底层数据结构使用的是哈希表(和Hash对象的实现相似,是value为null的哈希表)和整数集合;
inset整数集合数据结构:
整数集合是set对象的底层实现之一,当一个set对象只包含整数值元素(int16_t、int32_t 或者int64_t类型),就会使用整数集合这个数据结构作为底层实现,整数集合的三个字段分别表示编码方式(encoding),整数集合长度(length)和元素内容(contents[]);
当在整数集合中插入元素时,如果新元素的类型比集合中原有元素类型要长时,例如集合中是int16_t插入元素是int32_t时,整数集合就会触发升级的机制,也就是按照新元素的大小扩展contents[]数组的大小,升级后再将元素插入,这样实现可以兼容不同大小的整数类型而又不必一直开辟int64_t大小的空间造成浪费。
常用指令:
//添加一个set集合:
sadd key value1 value2 value3 ...
//获取某一个set集合中的所有元素
smembers key
//判断某set集合中是否存在某个value,存在则返回1,否则返回0
sismember key value
//获取set集合中的元素个数
scard key
//删除set集合中的某一个或多个元素
srem key value1 value2 value3 ...
//随机从某集合中弹出一个值,且会将该值删除掉,可用于抽奖过程中
spop key
//随机从某set集合中弹出多个值,但不会将这些值删除掉,count表示弹出的个数
srandmemberv key count
//集合操作
//获取两个set集合的交集元素
sinter key1 key2
//获取两个set集合的并集元素
sunion key1 key2
//获取两个set集合的差集元素
sdiff key1 key2
部分演示:
5.zset对象:
zset指有序集合(Sorted Set),其中的每个元素都关联一个分数(score),与普通set不同,zset中的元素是有序的,根据分数进行排序,这使得zset非常适合用于根据分数或权重排序和排名实现排行榜,其底层使用了listpack(list对象中已介绍)和skiplist(跳表);
skiplist跳表数据结构:
为了增加链表的查询效率,防止逐一查询,在链表的基础上改进出了跳表这种数据结构,跳表实现了一个多层的有序链表,每一层链表都是所有元素的子集,最底层包含了所有元素,并且每一层的节点都具有指向下一层节点的指针用于快速查找元素;
跳表的结构中包括以下数据:
- 跳表的头尾节点指针;
- 跳表的长度,即跳表节点的数量;
- 跳表的最大层数,最高层层数;
跳表的查询过程:
跳表会从头节点的最高层开始,逐一遍历每一层,在遍历某一层的跳表节点时,会用跳表节点中的SDS类型(即String对象底层结构)的元素和元素的权重来进行判断:
- 如果当前节点的权重小于要查找的权重时,跳表就会访问该层上的下一个节点;
- 如果当前节点的权重等于要查找的权重时,并且当前节点的SDS类型数据小于要查找的数据时,跳表就会访问该层上的下一个节点;
- 如果不满足上述两种情况,或者下一个节点为空时,跳表就会跳跃到当前位置的下一层指针,然后沿着下一层指针继续查找;
同时跳表结构中还有保存节点之间跨度的变量,跨度用于计算节点的排名,由于设定跳表的节点按序排列,计算某节点数据的排名时,只需要将查询到该节点沿途访问的所有跨度相加即可,得到的即为zset对象所需的排名。
常用指令:
//添加一个zset的集合,score即为用于排序的分数或权重;
//添加时的保留机制:
//1.相同的value,不同的score,则新value会覆盖旧value;
//2.相同的score,不同的value,两个都保留;
zadd key score1 value1 score2 value2 ...
//通过权重范围来获取zset中的元素,从小到大排列;
//limit offset count用于限制返回的结果数量;
zrangebyscore key min max [withscores] [limit offset count]
//通过权重范围来获取zset中的元素,从大到小排列;
zrevrangebyscore key max min [withscores] [limit offset count]
//获取zset集合,zrange 表示按权重从小到大排列,zrevrange 表示从大到小排列;
zrange/zrevrange key beginIndex endIndex [withscores]
//给zset中的元素的权重做增量操作,count可为负数;
zincrby key count value
//删除zset中的指定值的元素;
zrem key value
//统计zset中某键在一定权重范围内的元素个数;
zcount key scoreMin scoreMax
//获取zset集合中某键的某个元素的下标(非权重);
zrank key value
部分演示:
三、Golang中的Redis使用:
注:以下代码均只为记录总结接口,并未做错误处理,输出调试等,也不能直接运行,旨在整理!
1."github.com/go-redis/redis/v9"库介绍:
官方文档:redis package - github.com/go-redis/redis/v9 - Go Packages
该库提供了Golang连接操作Redis数据库的各种接口函数,并针对Redis的五种数据对象分别提供了对应的操作函数实现Redis命令的相同功能,同时附加了实现管道、事务、连接池等操作的函数;
2.连接Redis:
通过NewClient函数连接,指定IP端口、用户密码以及操作的数据库序号即可连接Redis数据库;
rdb := redis.NewClient(&redis.Options{
Addr: "127.0.0.1:6379",
Password: "",
DB: 0,
})
3.String对象接口:
// 创建上下文
ctx := context.Background()
// 设置一个key的值
err := client.Set(ctx, "Key", "Value", 0).Err()
// 查询key的值
value, err := client.Get(ctx, "Key").Result()
// 设置一个key的值,并返回这个key的旧值
oldValue, err := client.GetSet(ctx, "Key", "newValue").Result()
// 如果key不存在,则设置这个key的值
setNXResult, err := client.SetNX(ctx, "nonExistingKey", "nonExistingValue", 0).Result()
// 批量设置key的值
msetResult, err := client.MSet(ctx, "key1", "value1", "key2", "value2").Result()
// 批量查询key的值
mgetValues, err := client.MGet(ctx, "key1", "key2").Result()
// 针对一个key的数值进行加1操作
incrValue, err := client.Incr(ctx, "counter").Result()
// 针对一个key的数值进行加5操作
incrByValue, err := client.IncrBy(ctx, "counter", 5).Result()
// 针对一个key的数值进行加浮点数操作
incrByFloatValue, err := client.IncrByFloat(ctx, "floatCounter", 2.5).Result()
// 针对一个key的数值进行减1操作
decrValue, err := client.Decr(ctx, "counter").Result()
// 针对一个key的数值进行减3操作
decrByValue, err := client.DecrBy(ctx, "counter", 3).Result()
// 删除key操作,可以批量删除
delCount, err := client.Del(ctx, "key1", "key2").Result()
// 设置key的过期时间的使用示例
expireResult, err := client.Expire(ctx, "myKey", 10*time.Second).Result()
4.Hash对象接口:
//设置hash字段值
err := client.HSet(ctx, "hash", "field", "2").Err()
//查询hash字段值
getValue, err := client.HGet(ctx, "hash", "field").Result()
//查询所有hash字段和值
allValues, err := client.HGetAll(ctx, "hash").Result()
//累加数值
incrByValue, err := client.HIncrBy(ctx, "hash", "field", 5).Result()
//查询所有字段名
keys, err := client.HKeys(ctx, "hash").Result()
//查询hash字段数量
hashLen, err := client.HLen(ctx, "hash").Result()
//批量查询多个hash字段值
fields := []string{"field1", "field2"}
multiValues, err := client.HMGet(ctx, "hash", fields...).Result()
//批量设置hash字段值
multiSet := map[string]interface{}{"field2": "value2", "field3": "value3"}
//设置不存在的hash字段值
setNXSuccess, err := client.HSetNX(ctx, "hash", "newField", "newValue").Result()
//删除hash字段
fieldsToDelete := []string{"field1", "field2"}
deletedCount, err := client.HDel(ctx, "hash", fieldsToDelete...).Result()
//检测字段是否存在
exists, err := client.HExists(ctx, "hash", "field3").Result()
5.list对象接口:
//从列表左边插入数据
err := client.LPush(ctx, "listKey", "value1").Err()
//仅当列表存在的时候才插入数据
err = client.LPushX(ctx, "listKey", "value2").Err()
//从列表的右边删除第一个数据,并返回删除的数据
poppedValue, err := client.RPop(ctx, "listKey").Result()
//从列表右边插入数据
err = client.RPush(ctx, "listKey", "value3").Err()
//仅当列表存在的时候才插入数据
err = client.RPushX(ctx, "listKey", "value4").Err()
//从列表左边删除第一个数据,并返回删除的数据
poppedLeftValue, err := client.LPop(ctx, "listKey").Result()
//返回列表的大小
listSize, err := client.LLen(ctx, "listKey").Result()
//返回列表的一个范围内的数据,也可以返回全部数据
rangeValues, err := client.LRange(ctx, "listKey", 0, -1).Result()
//删除列表中的数据
removedCount, err := client.LRem(ctx, "listKey", 0, "value3").Result()
//根据索引坐标,查询列表中的数据
indexValue, err := client.LIndex(ctx, "listKey", 0).Result()
//在指定位置插入数据
err = client.LInsertBefore(ctx, "listKey", "value4", "insertedValue").Err()
// 打印插入后的列表数据
rangeValuesAfterInsert, err := client.LRange(ctx, "listKey", 0, -1).Result()
6.Set对象接口:
//添加集合元素
addedCount, err := client.SAdd(ctx, "setKey", "value1", "value2", "value3").Result()
//获取集合元素个数
cardinality, err := client.SCard(ctx, "setKey").Result()
//判断元素是否在集合中
isMember, err := client.SIsMember(ctx,"setKey", "value1").Result()
//获取集合中所有的元素
members, err := client.SMembers(ctx, "setKey").Result()
//删除集合元素
removedCount, err := client.SRem(ctx, "setKey", "value2").Result()
//随机返回集合中的元素,并删除返回的元素
poppedValue, err := client.SPop(ctx, "setKey").Result()
//随机返回集合中的多个元素,并删除返回的元素
poppedValues, err := client.SPopN(ctx, "setKey", 2).Result()
7.Zset对象接口:
//添加一个或多个元素到集合,如果元素已经存在则更新分数
addedCount, err := client.ZAdd(ctx, "zsetKey", redis.Z{Score: 1, Member: "value1"}, redis.Z{Score: 2, Member: "value2"}).Result()
//返回集合元素个数
cardinality, err := client.ZCard(ctx, "zsetKey").Result()
//统计某个分数范围内的元素个数
count, err := client.ZCount(ctx, "zsetKey", "1", "2").Result()
//增加元素的分数
newScore, err := client.ZIncrBy(ctx, "zsetKey", 3, "value1").Result()
//返回集合中某个索引范围的元素,根据分数从小到大排序
rangeValues, err := client.ZRange(ctx, "zsetKey", 0, -1).Result()
//根据分数从小到大排序,返回集合中某个索引范围的元素
revRangeValues, err := client.ZRevRange(ctx, "zsetKey", 0, -1).Result()
//根据分数范围返回集合元素,元素根据分数从小到大排序,支持分页
//-inf和+inf表示负无穷和正无穷
rangeByScoreValues, err := client.ZRangeByScore(ctx,"zsetKey", &redis.ZRangeBy{
Min: "-inf",
Max: "+inf",
}).Result()
//根据分数范围返回集合元素,元素根据分数从小到大排序,支持分页
revRangeByScoreValues, err := client.ZRevRangeByScore(ctx, "zsetKey", &redis.ZRangeBy{
Min: "10",
Max: "100",
}).Result()
//删除集合元素
removedCount, err := client.ZRem(ctx, "zsetKey", "value1").Result()
//根据索引范围删除元素
remRangeByRankCount, err := client.ZRemRangeByRank(ctx, "zsetKey", 0, 1).Result()
//根据分数范围删除元素
remRangeByScoreCount, err := client.ZRemRangeByScore(ctx, "zsetKey", "-inf", "+inf").Result()
//查询元素对应的分数
score, err := client.ZScore(ctx, "zsetKey", "value2").Result()
//查询元素的排名
rank, err := client.ZRank(ctx, "zsetKey", "value2").Result()
//查询元素的倒序排名
revRank, err := client.ZRevRank(ctx, "zsetKey", "value2").Result()
8.Pipeline打包操作:
Pipeline可以将多个Redis操作打包发给Redis服务器,一次性发送多个命令,可以减少网路往返,提高交互性能,利用这种方法还可以实现Redis的事务,即TxPipeline使用方法于Pipeline类似;
// 创建 Pipeline
pipeline := client.Pipeline()
// 添加多个操作到 Pipeline
setOp := pipeline.Set(ctx, "key1", "value1", 0)
getOp := pipeline.Get(ctx, "key1")
// 执行 Pipeline 中的操作
_, err := pipeline.Exec(ctx)
if err != nil {
log.Fatal(err)
}
// 获取操作结果
setResult := setOp.Err()
getResult := getOp.Val()
fmt.Printf("SET Result: %v\n", setResult)
fmt.Printf("GET Result: %s\n", getResult)
除上述操作之外,Go的Redis库还提供了发布订阅、乐观锁、位图操作、脚本操作、服务器操作等常用接口。
四、Redis使用的注意事项:
1.大key和热key的处理:
大key:
对于String类型,value的字节数大于10KB即为大key,对于其他复杂对象,元素个数大于5000或总value字节数大于10MB即为大key,由于Redis顺序处理命令,value太大会导致读写耗时变长,读取成本高,慢查询,过期删除,主从复制异常,服务阻塞,无法正常响应请求等问题;业务侧显示请求Redis超时报错,则表示出现了大key;
消除方法:
- 1.拆分:将大key拆分为小key,例如一个string拆分成多个string;
- 2.压缩:将value压缩后再写入redis,读取时解压后再使用,算法可以使用gzip,snappy,lz4等,通常情况压缩算法压缩率高,解压耗时就长,需要根据需求选择合适算法;如果是存储json字符串可以考虑用messagepack进行序列化;
- 3.集合类结构hash、list、set解决方法:
- 拆分:可以使用hash取余,用位掩码决定放在哪个key中;
- 区分冷热,如榜单列表场景使用zset,只缓存前十页数据作为热数据,后续数据作为冷数据走db;
热key
用户访问key的QPS(Queries Per Second,每秒查询次数)特别高,导致服务出现cpu负载突增或者负载不均的情况;热key没有明确的标准,QPS>500就可能识别为热key;如每个请求都会读取的一个全局配置,全局配置放在Redis中时,由于每秒查询次数很高,就变成了热key;
解决方法:
- 1.设置localcache
访问Redis前在业务服务侧设置localcache,localcache包含缓存过期管理,缓存分片,本机内存,可以利用它降低访问Redis的QPS,当前服务器内存,缓存过期或未命中时,则从Redis中将数据更新到localcache,Java的guava和golang的bigcache就是localcache的实现; - 2.拆分
将热key复制写成多份,访问时访问多个key,但value是同一个,这样就将QPS分散到不同实例,降低负载,代价是需要更新多个key,存在数据不一致的风险; - 3.使用redis代理的热key承载能力 本质是结合了“热key发现”,“localcache”两个功能,用proxy来做localcache,统计发现热key,proxy就将热key存在自己的内存中供客户查询。
2.慢查询场景:
容易导致redis慢查询操作:
- 一次性传入太多kv,如mset、hmset、sadd、zadd等复杂度为O(n)的操作;建议单批次不要超过100,超过100性能会明显下降;
- zset大部分命令复杂度O(logn),大小超过5k的简单zadd/zrem也可能导致慢查询;
- 操作的单个value过大,超过10kb时会出现慢查询,即需要避免使用大key;
- 对大key的delete和expire操作也可能导致慢查询,Redis4.0之前不支持异步删除unlink,大key删除会阻塞Redis。
3.缓存穿透和缓存雪崩:
缓存穿透:热点数据查询绕过缓存,直接查询数据库;
缓存雪崩:大量数据缓存同时过期;
- 缓存穿透:查询一个一定不存在的数据,通常不会缓存不存在的数据,查询会直接打到db,如果是bug或人为攻击,容易导致db响应慢或者db宕机;
- 缓存雪崩:热key缓存过期,大量请求直击db,造成db不稳定,或者同时有大量key集中过期,大量请求直击db,会导致查询缓慢,甚至db无法响应查询;
减少缓存穿透的策略:
- 1.缓存空值:如不存在的key,该key在缓存和数据库中都不存在时,可以对这个key缓存一个空值,下次在查该key时缓存直接返回空值;
- 2.布隆过滤器:通过bloom filter算法来存储合法key,得益于算法超高的压缩率,只需占用极小空间就能存储大量key值,可以用于判断一个元素是否存在于一个集合中,即可以将所有key存储,然后查询时先查询key是否存在,不存在就不需要进入db查询,从而避免缓存穿透。
避免缓存雪崩
- 1.缓存空值:将缓存失效时间分散,在原有失效时间基础上增加随机值,将数据的过期时间分散,热点数据,过期时间设置的更长,冷数据设置为少;
- 2.使用缓存集群:避免单机宕机造成缓存雪崩。
五、总结
以前没有接触过Redis,一直都在用关系型数据库,对Redis的学习让我对数据库有了不一样的理解,切身感受到这种数据库的需求,现在的时代是数据的时代,需要这样针对对象类型进行数据存储;同时对这些数据对象的底层数据结构的学习也让我再次认识到数据结构对数据存储的决定性作用,感受了大佬开发数据库时优化数据结构的思路是怎样的,如何针对不同的数据对象进行数据结构的选型,如何结合不同的数据结构来优化读写性能,这都是需要深入思考的知识;
整理总结了很久,但也总算是入门了Redis数据库,从为什么需要Redis到如何通过代码操作Redis,能够进行基本的数据操作,了解熟悉常用的指令,但在此基础上还需要进一步动手实现Redis常用的场景,还得继续深入学习,这篇笔记也就用来随时复习和找资料了。