redis使用文件事件处理器,是单线程的,它采用IO多路复用机制同时监听多个socket,根据socket上的事件来选择对应的事件处理器进行处理
命令到达服务端以后不会立即执行,而是进入一个队列中,然后I/O多路复用程序通过队列向文件事件分派器传送socket
redis6.0以后增加了多线程,不过执行命令还是单线程,使用多线程来处理数据的读写和协议解析,因为redis的瓶颈在于网络io,使用多线程能提高IO读写的效率
redis时间过期策略:
定期删除+惰性删除
定期删除:默认每隔100ms随机抽取过期的key进行删除,防止过期key太多占用内存
惰性删除:在查询的时候会判断是否已经过期,如果已经过期则删除
内存淘汰机制:当内存使用达到maxmemory时,触发主动清理策略
有8种内存淘汰策略
- noeviction:当内存使用超过配置的时候会返回错误,不会驱逐任何键
- allkeys-lru:加入键的时候,如果过限,首先通过LRU算法驱逐最久没有使用的键
- volatile-lru:加入键的时候如果过限,首先从设置了过期时间的键集合中驱逐最久没有使用的键
- allkeys-random:加入键的时候如果过限,从所有key随机删除
- volatile-random:加入键的时候如果过限,从过期键的集合中随机驱逐
- volatile-ttl:从配置了过期时间的键中驱逐马上就要过期的键
- volatile-lfu:从所有配置了过期时间的键中驱逐使用频率最少的键
- allkeys-lfu:从所有键中驱逐使用频率最少的键
redis中lru的实现:如果按照HashMap和双向链表实现,需要额外的存储存放 next 和 prev 指针,牺牲比较大的存储空间,显然是不划算的。所以Redis采用了一个近似的做法,就是随机取出若干个key,然后按照访问时间排序后,淘汰掉最不经常使用的
redis维护了一个24位时钟,可以简单理解为时间戳,不过按秒为单位存储,只能存储194天,每个key对象同样维护了一个24位时钟,新增对象的时候会把系统时钟赋值到这个对象时钟,lru的时候首先拿到全局时钟,然后找到内部对象时钟与全局时钟距离时间最久(差最大)的进行淘汰,由于最多存储194天,所以可能产生key时钟大于全局时钟的情况,这说明全局时钟已经走完一轮了,因此需要把这两个时间相加来求最久的key
4.0引入了LFU,即最少频率使用的优先淘汰,lfu把原来key对象的内部时钟分成两部分,前16位还表示时钟,后8位代表一个计数器,8位只能代表255,所以redis没有采用线性上升的方式,而是通过配置两个参数来调整数据的递增速度,如果key过了几分钟还没被命中,那么后8位还会递减
相关文章:https://www.jianshu.com/p/c8aeb3eee6bc
redis的持久化:
1、快照快照(snapshotting)持久化(RDB)存储某个时间点的副本,并且可以对快照进行备份,redis默认采用
原理:fork一个子进程来进行持久化,因为进程是内存隔离的,保证了时点数据的准确性,同时linux是写时复制原理,即fork子进程时不会立即拷贝内存,而是拷贝映射关系,指向同一个内存空间,只有父进程进行写操作时,才会真正复制,这样保证了效率,减少了客户端阻塞时间
2、只追加文件AOF(append-only file)持久化 可以通过 appendonly yes 指令开启 三种不同的AOF持久化方式:
1、每执行一条命令,都会写入硬盘中的AOF文件
2、每1s同步一次
3、让操作系统决定何时同步
如果不设置,默认选项将会是everysec,因为always来说虽然最安全(只会丢失⼀次事件循环的写命令),但是性能较差,⽽everysec模式只不过会可能丢失1秒钟的数据,⽽no模式的效率和everysec相仿,但是会丢失上次同步AOF⽂件之后的所有写命令数据。
优缺点:rdb快照文件比aof文件小一些,但是可能存在丢数据的问题
redis事物:
可以通过 MULTI、EXEC、WATCH 等命令来实现事务(transaction)功能
将多个命令打包,一次性,按顺序执行,在执行期间,不会中断事物去执行其他客户端的命令请求
具有原子性,一致性,隔离性,和持久性
缓存穿透:
大量请求的key不在redis中, 直接打到数据库上
解决办法:
1、做好参数校验,比如用户鉴权校验,id做基础校验,id<=0的直接拦截;
2、从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
缓存雪崩:
缓存在同一时刻大面积失效,导致后续请求都落到了数据库上,造成数据库短时间内承受大量请求
原因:redis故障,或者一些访问量大的key在某一时刻大面积失效
解决办法:针对redis不可用: 1、采用redis集群,2、限流
针对热点缓存失效:1、设置不同的失效时间,比如随机设置缓存的失效时间 2、缓存永不失效
缓存击穿
和热key的概念有点类似,热key:单个key的并发访问量过高,导致流量集中访问同一台redis机器,导致redis机器宕机引发雪崩 解决方案:
1. 备份热点Key:即将热点Key+随机数,随机分配至Redis其他节点中。这样访问热点key的时候就不会全部命中到一台机器上了。
2. 加⼊⼆级缓存,提前加载热key数据到内存中,如果redis宕机,⾛内存查询
单个key并发过高,过期时导致请求直接打到db上,
1. 加锁更新,⽐如请求查询A,发现缓存中没有,对A这个key加锁,同时去数据库查询数据,写⼊缓
存,再返回给⽤户,这样后⾯的请求就可以从缓存中拿到数据了。
2. 将过期时间组合写在value中,通过异步的⽅式不断的刷新过期时间,防⽌此类现象。
分布式锁
两种,setnx+lua 或者set key value px mill nx,nx表示if not exist 核心实现如下
- 获取锁(unique_value可以是UUID等)
SET resource_name unique_value NX PX 30000
- 释放锁(lua脚本中,一定要比较value,防止误解锁)
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
3大要点:1、set命令要用 set key value milliseconds nx; 2、value要具有唯一性 3、释放锁时要验证value值,不能误解锁
但是这个只能作用在一个redis节点上,因此基于分布式环境提出了一种更高级的分布式锁算法:Redlock,大体就是获取每个集群实例的锁,只有一半以上获取锁成功时,才认为成功,并且锁的有效时间为,锁有效时间-获取锁的时间-漂移时间
1、获取当前unix时间,以毫秒为单位
2、依次尝试从5个实例,使用相同的key和具有唯一性的value(比如uuid)获取锁,当向redis获取锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间,这样可以避免redis已经挂掉的情况下还在等待响应结果,如果响应超时,则继续请求另外的redis
3、如果取到了锁,则key的有效时间为有效时间减去获取锁所使用的时间,如果获取锁失败(没有在至少n/2 +1 个redis上获取锁或者取锁时间超过了有效时间),则客户端应该在所有的实例上进行解锁
相关代码:
// 实现分布式锁非常重要的一点就是set的value要具有唯一性
protected final UUID id = UUID.randomUUID();
String getLockName(long threadId) {
return id + ":" + threadId;
}
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
// 获取锁时需要在redis实例上执行的lua命令
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
// 首先分布式锁的KEY不能存在,如果确实不存在,那么执行hset命令(hset REDLOCK_KEY uuid+threadId 1),并通过pexpire设置失效时间(也是锁的租约时间)
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('hset', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
// 如果分布式锁的KEY已经存在,并且value也匹配,表示是当前线程持有的锁,那么重入次数加1,并且设置失效时间
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
// 获取分布式锁的KEY的失效时间毫秒数
"return redis.call('pttl', KEYS[1]);",
// 这三个参数分别对应KEYS[1],ARGV[1]和ARGV[2]
Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
// 释放锁时需要在redis实例上执行的lua命令
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
// 如果分布式锁KEY不存在,那么向channel发布一条消息
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; " +
"end;" +
// 如果分布式锁存在,但是value不匹配,表示锁已经被占用,那么直接返回
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
"return nil;" +
"end; " +
// 如果就是当前线程占有分布式锁,那么将重入次数减1
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
// 重入次数减1后的值如果大于0,表示分布式锁有重入过,那么只设置失效时间,还不能删除
"if (counter > 0) then " +
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
"return 0; " +
"else " +
// 重入次数减1后的值如果为0,表示分布式锁只获取过1次,那么删除这个KEY,并发布解锁消息
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; "+
"end; " +
"return nil;",
// 这5个参数分别对应KEYS[1],KEYS[2],ARGV[1],ARGV[2]和ARGV[3]
Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));
}
https://www.cnblogs.com/rgcLOVEyaya/p/RGC_LOVE_YAYA_1003days.html
redis优化:
1、缩短键值对的存储长度;
内容越大需要的持久化时间就越长,需要挂起的时间越长,Redis 的性能就会越低;
内容越大在网络上传输的内容就越多,需要的时间就越长,整体的运行速度就越低;
内容越大占用的内存就越多,就会更频繁的触发内存淘汰机制,从而给 Redis 带来了更多的运行负担。
2、使用 lazy free(延迟删除)特性;
3、设置键值的过期时间;
4、禁用长耗时的查询命令;
避免使用O(n)时间复杂度的命令,比如说
禁止使用 keys 命令;
避免一次查询所有的成员,要使用 scan 命令进行分批的,游标式的遍历;
通过机制严格控制 Hash、Set、Sorted Set 等结构的数据大小;
将排序、并集、交集等操作放在客户端执行,以减少 Redis 服务器运行压力;
5、使用 slowlog 优化耗时命令;
6、使用 Pipeline 批量操作数据;
pipeline在集群下效果不是太好,他和mget的区别是:
-- 原生批量命令是原子的,Pipeline是非原子的。
-- 原生批量命令是一个命令对应多个key,Pipeline支持多个命令。
-- 原生批量命令是Redis服务端支持实现的,而Pipeline需要服务端和客户端的共同实现。
集群模式下,使用pipeline时,slot必须是对的,不然服务端会返回redirecred to slot xxx的错误;同时不建议使用Transaction,因为假设一个Transaction中的命令即在Master A上执行,也在Master B执行,A成功了,B因为某种原因失败了,这样数据就不一致了,这个有点类似于分布式事务,无法保证绝对一致性。
7、避免大量数据同时失效;
可能会产生缓存雪崩
8、客户端使用优化;
9、限制 Redis 内存大小;
10、使用物理机而非虚拟机安装 Redis 服务;
11、检查数据持久化策略;
12、禁用 THP 特性;
Linux kernel 在 2.6.38 内核增加了 Transparent Huge Pages (THP) 特性 ,支持大内存页 2MB 分配,默认开启。
当开启了 THP 时,fork 的速度会变慢,fork 之后每个内存页从原来 4KB 变为 2MB,会大幅增加重写期间父进程内存消耗。同时每次写命令引起的复制内存页单位放大了 512 倍,会拖慢写操作的执行时间,导致大量写操作慢查询。例如简单的 incr 命令也会出现在慢查询中,因此 Redis 建议将此特性进行禁用
13、使用分布式架构来增加读写速度。
数据库和redis缓存一致性解决方案
1、双写模式,即修改数据库中数据后,再更新redis缓存,两次写操作,称为双写模式
问题:高并发下可能有脏数据,比如线程A更新数据库后还没来得及更新缓存,此时线程B再次更新数据,并更新缓存,此时线程A更新缓存的话,会将缓存更新为老数据,而不是B更新的数据,导致产生脏数据
还有一点是更新的值并不一定会被频繁查询,如果每次更新,但是并没有查询,那是得不偿失的,因此应该在数据被查询的时候再放入缓存, 而不是每次都更新
2、失效模式,即更新数据库后,删除redis缓存,在查询请求时判断是否存在缓存,不存在会先去数据库查询,然后更新到redis,存在则直接查询
在高并发下同样存在脏数据问题:
1、先改数据库,再删redis:线程A更改数据库,然后删除缓存,线程B来查询,发现没有缓存,去数据库中查询得到A修改的数据,然后在更新之前,线程C修改了数据库,并且删除了为空的缓存,此时线程B再更新缓存,更新的是A修改的数据而不是C修改的,导致产生脏数据
2、先删redis,再改数据库:线程A需要更新数据,首先删除了redis,然后在更新数据库之前,线程B来查询数据库,查询到的还是原来的数据,并更新到缓存中,执行完后线程A继续更新数据库,这时候与缓存数据不一致
3、延迟双删,在失效模式基础上,在删除redis时,让程序睡十几毫秒,但是正常请求会被阻塞十几毫秒,影响性能
4、在更新时增加分布式锁
5、订阅mysqlbinlog,然后将数据同步到redis