Redis使用注意点
2022-12-14 09:36:55 13 举报
AI智能生成
redis学习脑图总结
作者其他创作
大纲/内容
使用规范
key的规范要点
可读性,简洁性
以业务名为key的前缀,用冒号隔开以防止key冲突覆盖。如anti:blacklist:001
确保key的语义清晰的情况下,key的长度尽量小于30个字符
禁止包含特殊字符
例如空格,换行,单双引号以及其他转义符
value的规范要点
拒接bigkey
如果大量存储bigKey会导致慢查询,内存增长过快等问题
如果是String类型,单个value大小控制10k以内
如果是hash,list,set,zset类型,元素个数一般不超过500
选择合适的数据类型
给key设置过期时间,同时避免过期时间过于集中
尽量设置ttl,以保证不适用的key能被及时清理或淘汰
因为Redis的数据是存在内存中的,而内存资源是很宝贵的,一般我们把Redis当做缓存来用,而不是数据库,所以key的生命周期不宜太长久。
批量操作提高效率
mget,mset
我们日常写SQL的时候,批量操作效率会更高,一次更新50条比循环50次效率更高,Redis操作命令也是这个道理。
Redis客户端执行一次命令可以分为4个过程:1.发送命令->2.命令排队->3.命令执行->4.返回结果。1和4称为RRT(命令执行往返时间)
Redis提供了批量操作命令可有效节约RRT
pipeline
Redis大部分的命令是不支持批量操作的,比如hgetall,并没有mhgetall的存在。Pipeline则可以解决这个问题。
Pipeline能将一组Redis命令进行组装,通过一次RTT传输给Redis,再将这组Redis命令的执行结果按顺序返回给客户端
有坑的命令
慎用O(n)复杂度命令,如hgetall,smember,lrange等
因为Redis是单线程执行命令的,复杂度为O(n)的命令,当n持续增加时,会导致Redis CPU持续飙高,阻塞其他命令的执行。
hgetall、smember,lrange等这些命令不是一定不能使用,需要综合评估数据量,明确n的值,再去决定。 比如hgetall,如果哈希元素n比较多的话,可以优先考虑使用hscan。
慎用monitor命令
Monitor命令在于实时打印出Redis服务器接收到的命令,如果我们想知道客户端对redis服务端做了哪些操作,就可以用Monitor命令查看,但是它一般调试用而已,尽量不要在生产上用,因为monitor命令可能导致redis的内存持续飙升。
monitor的模型是酱紫的,它会将所有在Redis服务器执行的命令进行输出,一般来讲Redis服务器的QPS是很高的,也就是如果执行了monitor命令,Redis服务器在Monitor这个客户端的输出缓冲区又会有大量“存货”,也就占用了大量Redis内存。
禁止使用keys指令
Keys命令用于查找所有符合给定模式pattern的key。比如 keys key前缀*
redis的keys是遍历匹配的,复杂度是O(n),数据库数据越多就越慢,Redis是单线程的,如果数据比较多的话,keys指令就会导致redis线程阻塞,线上服务也会停顿直到指令执行完,服务才会回复。
因此,一般在生产环境,不要使用keys指令。
官方文档也有声明:Warning: consider KEYS as a command that should only be used in production environments with extreme care. It may ruin performance when it is executed against large databases. This command is intended for debugging and special operations, such as changing your keyspace layout. Don't use KEYS in your regular application code. If you're looking for a way to find keys in a subset of your keyspace, consider using sets.
可以使用scan指令,通keys命令一样提供匹配模式功能
复杂度也是O(n),但是它通过游标分步进行,不会阻塞redis线程,但是会有一定的重复概率,需要在客户端做一次去重。
scan支持增量式迭代命令,增量式迭代命令也是有缺点的:举个例子, 使用 SMEMBERS 命令可以返回集合键当前包含的所有元素, 但是对于 SCAN 这类增量式迭代命令来说, 因为在对键进行增量式迭代的过程中, 键可能会被修改, 所以增量式迭代命令只能对被返回的元素提供有限的保证 。
禁止使用flushall,flushdb
Flushall命令用于清空整个Redis服务器的数据(删除所有数据库的所有key)
Flushdb命令用于清空当前数据库中的所有key
这两个命令是原子性的,不会终止执行,一旦执行开始,不会执行失败的。
注意使用del命令
如果删除一个String类型的key,时间复杂度就是O(1),可以直接del
如果删除一个List/Hash/Set/ZSet类型时,他的复杂度是O(n),n表示元素个数
所以删除时元素越多就越慢,当n很大时,要尤其注意,会阻塞主线程的。
如果是List类型,可以执行lpop或者rpop知道所有元素删除完成
如果是Hash/Set/ZSet类型,可以先执行hscan/sscan/scan查询,再执行hdel/srem/zrem依次删除每个元素
避免复杂度过高的命令,如SORT,SINTER等
执行复杂度较高的命令,会消耗更多的 CPU 资源,会阻塞主线程。所以你要避免执行如SORT、SINTER、SINTERSTORE、ZUNIONSTORE、ZINTERSTORE等聚合命令,一般建议把它放到客户端来执行。
项目实战闭坑操作
分布式锁注意点
两个命令SETNX+EXPIRE分开写(典型错误实现范例)
如果执行完setnx加锁,正要执行expire设置过期时间,进程crash或者重要维护重启了,那么这个锁就长生不老了,别的线程永远获取不到。
if(jedis.setnx(key_resource_id,lock_value) == 1){ //加锁
expire(key_resource_id,100); //设置过期时间
try {
do something //业务请求
}catch(){}
finally {
jedis.del(key_resource_id); //释放锁
}
}
expire(key_resource_id,100); //设置过期时间
try {
do something //业务请求
}catch(){}
finally {
jedis.del(key_resource_id); //释放锁
}
}
SETNX + value值是过期时间 (有些小伙伴是这么实现,有坑)
方案缺点:过期时间是客户端自己生成的,分布式环境下,每个客户端的时间必须同步。没有保存持有者的唯一标识,可能被别的客户端释放/解锁。锁过期的时候,并发多个客户端同时请求过来,都执行了jedis.getSet(),最终只能有一个客户端加锁成功,但是该客户端锁的过期时间,可能被别的客户端覆盖。
long expires = System.currentTimeMillis() + expireTime; //系统时间+设置的过期时间
String expiresStr = String.valueOf(expires);
// 如果当前锁不存在,返回加锁成功
if (jedis.setnx(key_resource_id, expiresStr) == 1) {
return true;
}
// 如果锁已经存在,获取锁的过期时间
String currentValueStr = jedis.get(key_resource_id);
// 如果获取到的过期时间,小于系统当前时间,表示已经过期
if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
// 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间(不了解redis的getSet命令的小伙伴,可以去官网看下哈)
String oldValueStr = jedis.getSet(key_resource_id, expiresStr);
if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
// 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才可以加锁
return true;
}
}
//其他情况,均返回加锁失败
return false;
}
String expiresStr = String.valueOf(expires);
// 如果当前锁不存在,返回加锁成功
if (jedis.setnx(key_resource_id, expiresStr) == 1) {
return true;
}
// 如果锁已经存在,获取锁的过期时间
String currentValueStr = jedis.get(key_resource_id);
// 如果获取到的过期时间,小于系统当前时间,表示已经过期
if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
// 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间(不了解redis的getSet命令的小伙伴,可以去官网看下哈)
String oldValueStr = jedis.getSet(key_resource_id, expiresStr);
if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
// 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才可以加锁
return true;
}
}
//其他情况,均返回加锁失败
return false;
}
SET的扩展命令(SET EX PX NX)(注意可能存在的问题)
可能存在问题:锁过期释放了,业务还没执行完。锁被别的线程误删。
if(jedis.set(key_resource_id, lock_value, "NX", "EX", 100s) == 1){ //加锁
try {
do something //业务处理
}catch(){}
finally {
jedis.del(key_resource_id); //释放锁
}
}
try {
do something //业务处理
}catch(){}
finally {
jedis.del(key_resource_id); //释放锁
}
}
SET EX PX NX + 校验唯一随机值,再删除(解决了误删问题,还是存在锁过期,业务没执行完的问题)
finally里面判断是不是当前线程加的锁和释放锁不是一个原子操作。如果调用jedis.del()释放锁的时候,可能这把锁已经不属于当前客户端,会解除他人加的锁
if(jedis.set(key_resource_id, uni_request_id, "NX", "EX", 100s) == 1){ //加锁
try {
do something //业务处理
}catch(){}
finally {
//判断是不是当前线程加的锁,是才释放
if (uni_request_id.equals(jedis.get(key_resource_id))) {
jedis.del(lockKey); //释放锁
}
}
}
try {
do something //业务处理
}catch(){}
finally {
//判断是不是当前线程加的锁,是才释放
if (uni_request_id.equals(jedis.get(key_resource_id))) {
jedis.del(lockKey); //释放锁
}
}
}
一般也是用lua脚本代替
if redis.call('get',KEYS[1]) == ARGV[1] then
return redis.call('del',KEYS[1])
else
return 0
end;
return redis.call('del',KEYS[1])
else
return 0
end;
Redisson框架 + Redlock算法 解决锁过期释放,业务没执行完问题+单机问题
Redisson 使用了一个Watch dog解决了锁过期释放,业务没执行完问题,Redisson原理图如上:
以上的分布式锁,还存在单机问题:
如果线程一在Redis的master节点上拿到了锁,但是加锁的key还没同步到slave节点。恰好这时,master节点发生故障,一个slave节点就会升级为master节点。线程二就可以获取同个key的锁啦,但线程一也已经拿到锁了,锁的安全性就没了。
缓存一致性注意点
如果是读请求,先读缓存,后读数据库。如果写请求,先更新数据库,再写缓存。每次更新数据后,需要清除缓存,缓存一般都需要设置一定的过期失效。一致性要求高的话,可以使用biglog+MQ保证。
频繁set覆盖问题
合理评估Redis容量,避免由于频繁set覆盖,导致之前设置的过期时间无效
Redis的所有数据结构类型,都是可以设置过期时间的。假设一个字符串,已经设置了过期时间,你再去重新设置它的值,就会导致之前的过期时间无效。
实际业务开发中,同时我们要合理评估Redis的容量,避免频繁set覆盖,导致设置了过期时间的key失效
缓存穿透问题
指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,进而给数据库带来压力。
产生情况
业务不合理的设计,比如大多数用户都没开守护,但是你的每个请求都去缓存,查询某个userid查询有没有守护。
业务/运维/开发失误的操作,比如缓存和数据库的数据都被误删除了。
黑客非法请求攻击,比如黑客故意捏造大量非法请求,以读取不存在的业务数据。
避免办法
如果是非法请求,我们在API入口,对参数进行校验,过滤非法值。
如果查询数据库为空,我们可以给缓存设置个空值,或者默认值。但是如有有写请求进来的话,需要更新缓存哈,以保证缓存一致性,同时,最后给缓存设置适当的过期时间。(业务上比较常用,简单有效)
使用布隆过滤器快速判断数据是否存在。即一个查询请求过来时,先通过布隆过滤器判断值是否存在,存在才继续往下查。
布隆过滤器原理:它由初始值为0的位图数组和N个哈希函数组成。一个对一个key进行N个hash算法获取N个值,在比特数组中将这N个值散列后设定为1,然后查的时候如果特定的这几个位置都为1,那么布隆过滤器判断该key存在。
缓存雪崩问题
指缓存中数据大批量到过期时间,而查询数据量巨大,请求都直接访问数据库,引起数据库压力过大甚至down机。
产生情况
缓存雪奔一般是由于大量数据同时过期造成的,对于这个原因,可通过均匀设置过期时间解决,即让过期时间相对离散一点。如采用一个较大固定值+一个较小的随机值,5小时+0到1800秒酱紫。
Redis 故障宕机也可能引起缓存雪奔。这就需要构造Redis高可用集群
避免办法
使用互斥锁方案。缓存失效时,不是立即去加载db数据,而是先使用某些带成功返回的原子操作命令,如(Redis的setnx)去操作,成功的时候,再去加载db数据库数据和设置缓存。否则就去重试获取缓存。
“永不过期”,是指没有设置过期时间,但是热点数据快要过期时,异步线程去更新和设置过期时间
缓存击穿问题
指缓存中某一个key过期时间到了,而查询量巨大,请求都直接访问数据库,引起数据库压力过大甚至down机
产生情况
redis某个key过期了,大量访问使用这个key(热点key)
避免办法
预先设置热门数据,在redis高峰访问之前,把一些热门数据提前存入到redis里面,加大这些热门数据key的时长。
实时调整,现场监控哪些数据热门,实时调整key的过期时长
使用锁
就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,先试用缓存工具的某些自带成功操作返回值的操作(如Redis的SETNX)去set一个mutex key。当操作返回成功时,再进行load db的操作,并回设缓存,最后删除mutex key。当操作返回失败,证明有线程在load db,当前线程睡眠一段时间再重试整个get缓存的方法。
缓存热key问题
在Redis中,我们把访问频率高的key,称为热点key。如果某一热点key的请求到服务器主机时,由于请求量特别大,可能会导致主机资源不足,甚至宕机,从而影响正常的服务。
热点key如何产生
用户消费的数据远大于生产的数据,如秒杀、热点新闻等读多写少的场景。
请求分片集中,超过单Redis服务器的性能,比如固定名称key,Hash落入同一台服务器,瞬间访问量极大,超过机器瓶颈,产生热点Key问题。
如何识别热点key
凭经验判断哪些是热Key
客户端统计上报
服务代理层上报
如何解决热点key问题
Redis集群扩容:增加分片副本,均衡读流量
对热key进行hash散列,比如将一个key备份为key1,key2……keyN,同样的数据N个备份,N个备份分布到不同分片,访问时可随机访问N个备份中的一个,进一步分担读流量;
使用二级缓存,即JVM本地缓存,减少Redis的读请求
配置运维
使用长链接操作Redis,合理配置连接池
如果使用短连接,每次都需要过 TCP 三次握手、四次挥手,会增加耗时。然而长连接的话,它建立一次连接,redis的命令就能一直使用,酱紫可以减少建立redis连接时间
连接池可以实现在客户端建立多个连接并且不释放,需要使用连接的时候,不用每次都创建连接,节省了耗时。但是需要合理设置参数,长时间不操作 Redis时,也需及时释放连接资源
只是用db0
Redis-standalone架构禁止使用非db0.原因有两个
一个连接,Redis执行命令select 0和select 1切换,会损耗新能
Redis Cluster 只支持 db0,要迁移的话,成本高
设置maxmemory+恰当的淘汰策略,避免内存膨胀
为了防止内存积压膨胀。比如有些时候,业务量大起来了,redis的key被大量使用,内存直接不够了,运维小哥哥也忘记加大内存了。难道redis直接这样挂掉?所以需要根据实际业务,选好maxmemory-policy(最大内存淘汰策略),设置好过期时间
内存淘汰策略
volatile-lru:当内存不足以容纳新写入数据时,从设置了过期时间的key中使用LRU(最近最少使用)算法进行淘汰;
allkeys-lru:当内存不足以容纳新写入数据时,从所有key中使用LRU(最近最少使用)算法进行淘汰。
volatile-lfu:4.0版本新增,当内存不足以容纳新写入数据时,在过期的key中,使用LFU算法进行删除key。
allkeys-lfu:4.0版本新增,当内存不足以容纳新写入数据时,从所有key中使用LFU算法进行淘汰
volatile-random:当内存不足以容纳新写入数据时,从设置了过期时间的key中,随机淘汰数据
allkeys-random:当内存不足以容纳新写入数据时,从所有key中随机淘汰数据
volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的key中,根据过期时间进行淘汰,越早过期的优先被淘汰
noeviction:默认策略,当内存不足以容纳新写入数据时,新写入操作会报错
开始lazy-free机制
Redis4.0+版本支持lazy-free机制,如果你的Redis还是有bigKey这种玩意存在,建议把lazy-free开启。当开启它后,Redis 如果删除一个 bigkey 时,释放内存的耗时操作,会放到后台线程去执行,减少对主线程的阻塞影响。
0 条评论
下一页