Redis工作原理
date
slug
redis-introduce
author
status
Public
tags
技术分享
summary
type
Post
thumbnail
category
💻 Backend
updatedAt
Oct 29, 2023 07:22 AM
Redis是单线程吗?
Redis 单线程指的是「接收客户端请求->解析请求 ->进行数据读写等操作->发送数据给客户端」这个过程是由一个线程(主线程)来完成的
但是,Redis 程序并不是单线程的,Redis 在启动的时候,是会启动后台线程(BIO)的:
所有的删除操作、关闭文件、释放内存都由后台线程来完成。通过轮训这三个队列的任务在后台线程里面执行。
单线程模式
I/O多路复用(epoll),读、写、连接事件分发器

Redis
单线程的 Redis 吞吐量可以达到 10W/每秒 (QPS)
为什么这么快?操作都在内存、I/O 多路复用机制、单线程模式避免多线程竞争
Redis6.0以后的多线程默认只针对write发送数据,如果读事件要生效要通过配置开启
//读请求也使用io多线程 io-threads-do-reads yes
Redis与MySQL之间的数据一致性
不管是什么方案先删后改,延时双删也好,在高并发的情况下都会出现数据不一致的。如果需要保持强一致性的话就不要做缓存,所以在我们的项目中还是采用先删再改+过期的方案来做的,虽然也会存在数据的不一致,但是勉强也能接受,因为毕竟使用缓存访问快的同时也能减轻DB压力,而且本身采用缓存就需要接受一定的数据延迟性和短暂的不一致性,我们只能采取合适的策略来降低缓存和数据库间数据不一致的概率,而无法保证两者间的强一致性。合适的策略包括合适的缓存更新策略,合适的缓存淘汰策略,更新数据库后及时更新缓存、缓存失败时增加重试机制等
Redis快的原因
基于内存
数据结构类似HashMap,查找O(1)
select/epoll多路复用模型
客户端通信协议采用RESP
缓存雪崩 -> 大量的key因为过期时间在同一时间失效,这个时候大量的查询进来了,直接走DB查询导致数据库里压力增大
一、根据业务场景来看吧,设置合理的过期时间
缓存穿透 -> 如果请求了一个不存在的key,导致查询来到数据库,数据库没查到所以也没写入redis。在高并发的情况下,DB就会出现瓶颈
一、做IP限流与黑名单,避免同一IP一瞬间发送大量请求
二、对于请求做非法校验,对于携带非法参数的请求直接过滤
三、对于DB中查询不存在的数据写入Redis中“Not Data”并设置短暂的过期时间,下次请求能够直接被拦截在Redis而不会落入DB
四、布隆过滤器(Key的映射在不同的位上,可以解决网页 URL 去重、垃圾邮件识别、大集合中重复元素的判断和缓存穿透等问题)
比如说应用初始化的时候将数据库表中所有数据的主键查询出来,构建布隆过滤器,后续接口在查询Redis之前先通过布隆过滤器查一下ID,如果通不过过滤器,就代表是攻击数据,直接返回空消息,不会与数据库交互
缓存击穿 -> 某一个热点key突然过期,而这个时候又突然又大量的请求来查询它,但是在Redis中却并没有查询到结果从而导致所有请求全部打向DB,导致在这个时刻DB直接被打穿
使用mutex锁机制:就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法
(定期删除 + 惰性删除) 不生效,内存还是高的时候淘汰策略的作用就出来了
淘汰策略
八种内存淘汰策略
主要有 lru、lfu、random、ttl, 分两种模式,一种是针对已经设置过期时间的数据,另一种是全体数据
lru: 最近最久未使用算法,如果一个数据在[最近一段时间没有被访问到],那么可以认为在将来它被访问的可能性也很小。因此,当空间满时,最久没有访问的数据最先被置换
- volatile: 已设置过期时间的数据
- allkeys: 全体
ttl: ttl值大的优先淘汰
- volatile: 已设置过期时间的数据
- allkeys: 全体
random: 任意淘汰
- volatile: 已设置过期时间的数据
- allkeys: 全体
lfu(Top K) 最近最少使用算法,如果一个数据在[最近一段时间很少被访问到],那么可以认为在将来它被访问的可能性也很小。因此,当空间满时,最小频率访问的数据最先被淘汰
- volatile: 已设置过期时间的数据
- allkeys: 全体
no-enviction: 禁止驱逐数据,这也是默认策略。意思是当内存不足以容纳新入数据时,新写入操作就会报错,请求可以继续进行,线上任务也不能持续进行,采用no-enviction策略可以保证数据不被丢失
Key删除策略
Redis默认采用定期+惰性删除策略
Redis三种持久化
4.x版本之前AOF、RDB,4.x版本之后混合型持久化(最终生成的是文件是.AOF)
RDB - 持久化把内存中当前进程的数据生成快照(.rdb)文件保存到硬盘的过程
自动持久化
手动持久化
优点:有单独的子线程进行持久化,主线程不会有影响
缺点:自动持久化模式有时间间隔,如果在这之间断电,那么就会数据丢失
AOF持久化 - AOF持久化到硬盘中的并不是内存中的数据快照,而是和MySQL的binlog日志一样记录写入命令
三种策略:
appendfsync always -> 发生数据变更 -> 追加到.aof文件,产生大量的磁盘I/0操作,性能不好但安全
appendfsync everysec -> 每秒追加 -> 如果在刚持久化之后的一秒内宕机,会造成1S的数据丢失
appendfsync no -> 交给操作系统决定 -> 性能较好,在物理服务器故障时,数据丢失量会因OS配置有关
优点:不同的策略保证数据丢失风险降低,fsync是后台线程在处理,所以对于处理客户端请求的线程并不影响
缺点:因为是指令记录,所以.afo文件体积大,数据恢复慢(可以通过AOF机制重写优化)

AOF重写机制
将当前内存最新值重新记录到新的AOF文件,替换成旧的。
Redis 的重写 AOF 过程是由后台子进程 bgrewriteaof 来完成的
redis配置参数查看:config get *
https://juejin.cn/post/7097521572885299214#heading-14
Reids 可以做的事情
数据缓存、分布式锁、幂等性、分布式事务、消息系统、分布式session
分布式锁核心关键点
抢锁(实现跨 JVM 的互斥)
- 抢锁的设计,实现跨 JVM 的互斥
- 要求:不同的节点(应用)去抢
锁对象的时候,同一时刻有且只有一个人能抢成功 - 锁对象(名称)的选择,一般都是根据具体业务来选择,比如说:文件上传如果使用分布式锁,那么文件 MD5 做锁对象。
- 支持互斥功能的中间件,比如:数据库的悲观锁、Redis 的
setnx(key, value)、Zookeeper 的目录唯一性等,这些特性都能实现互斥效果。
setnx (key,value) 的特性是,如果 key 存在则无法保存,并且 Redis 是单线程的,因此能保证互斥性
可以给 key 设置过期时间,避免死锁的发生
阻塞(抢不到锁时应该怎么办呢)
- 方案一:循环,抢到不到锁的线程,不断的轮询,直到锁被释放并且自己抢成功为止
- 方案二:堵塞 + 通知,如果抢到不到锁的线程,则该线程处于堵塞状态(比如:J.U.C 下面的几个常见工具类,CountDownLatch、Semaphore、CyclicBarrier 有可以实现堵塞和唤醒的功能),锁被释放时,通知客户端,唤醒等待的线程去抢锁。
释放锁(执行完成之后进行锁的释放)
- 正常情况下,执行完成业务之后,释放锁,并且通知其他线程去抢锁
- 要求 1:不能释放别人的锁,如果释放别人的锁,则会出现干扰的情况
- 要求 2:不能出现死锁,如果抢到锁的进程挂掉了,无法释放锁了,那么其他线程只能一直处于堵塞状态。
大Key处理
del 是在主线程处理的,这样会导致 Redis 主线程卡顿,因此我们应该使用 unlink 命令来异步删除大key
实战
使用Docker拉取Redis
docker search redis docker pull redis:latest docker images
本地新建一个redis目录, 用于挂载redis.config 和 data
mkdir my-workspace/redis cd redis
master节点配置文件 - redis.conf
bind * -::* #任意外部和内部端口访问 protected-mode no #默认yes,开启保护模式,限制为本地访问 daemonize no#默认no,改为yes意为以守护进程方式启动,可后台运行,除非kill进程,改为yes会使配置文件方式启动redis失败 databases 16 #数据库个数(可选),我修改了这个只是查看是否生效。。 dir ./ #输入本地redis数据库存放文件夹(可选) appendonly yes #redis持久化(可选) logfile "access.log"
查看redis master的内部IP
docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 2674f91946bf redis "docker-entrypoint.s…" About a minute ago Up About a minute 6379/tcp, 0.0.0.0:6380->6380/tcp redis-slave 8dcc95b5c230 redis "docker-entrypoint.s…" 8 minutes ago Up 8 minutes 0.0.0.0:6379->6379/tcp redis-master docker inspect 8dcc95b5c230 #找到以下返回信息 #"IPAddress": "172.17.0.2",
salve节点配置文件 - redis-slave.conf
bind * -::* #任意外部和内部端口访问 protected-mode no #默认yes,开启保护模式,限制为本地访问 daemonize no#默认no,改为yes意为以守护进程方式启动,可后台运行,除非kill进程,改为yes会使配置文件方式启动redis失败 databases 16 #数据库个数(可选),我修改了这个只是查看是否生效。。 dir ./ #输入本地redis数据库存放文件夹(可选) appendonly yes #redis持久化(可选) logfile "access.log" # 主地址 replicaof #容器内master节点Ip 6379
重启redis-salve节点
docker restart redis-slave
docker run \
-p 6379:6379 --name redis-master \
-v /Users/kenny/Documents/Code/redis/redis.conf:/etc/redis/redis.conf \
-v /Users/kenny/Documents/Code/redis/redis/data:/data:rw \
--privileged=true -d redis redis-server /etc/redis/redis.conf \
--appendonly yes
docker run \
-p 6380:6380 --name redis-slave \
-v /Users/kenny/Documents/Code/redis/redis-slave.conf:/etc/redis/redis.conf \
-v /Users/kenny/Documents/Code/redis/redis-slave/data:/data:rw \
--privileged=true -d redis redis-server /etc/redis/redis.conf \
--appendonly yes
登陆主节点redis容器查看主从配置是否生效
→ redis-cli
→ info
# Replication role:master connected_slaves:1 slave0:ip=172.17.0.3,port=6379,state=online,offset=14,lag=0 #从节点准备就绪 master_failover_state:no-failover master_replid:1089d1e52c38bc70a88908503b511733388806d8 master_replid2:0000000000000000000000000000000000000000 master_repl_offset:14 second_repl_offset:-1 repl_backlog_active:1 repl_backlog_size:1048576 repl_backlog_first_byte_offset:1 repl_backlog_histlen:14
最经典的缓存 + 数据库读写的模式,就是 Cache Aside Pattern
- 读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
- 更新的时候,先更新数据库,然后再删除缓存。
@Service @Slf4j public class CacheService { private AtomicInteger db_value = new AtomicInteger(100); //模拟数据库值 @Autowired private StringRedisTemplate redisTemplate; public Integer readIfNotExistThenPutCache(String key){ String result = redisTemplate.opsForValue().get(key); //并发线程会同时获取到该值 if (StringUtils.isEmpty(result)){ log.info("获取key值为空,重新写入缓存"); result = String.valueOf(db_value.get()); redisTemplate.opsForValue().set(key, result, 5, TimeUnit.SECONDS); } return Integer.parseInt(result); } public void update(String key, Integer val){ db_value.set(val); log.info("设置值" + val); redisTemplate.delete(key); log.info("删除key" + key); } }
这个例子的问题在于,当高并发情况下,redisTemplate.opsForValue().get(key)不是互斥的,可能存在多个线程同时获取的result为空,导致同时更新value。
/** * 使用一个string key 模拟 synchronized 加锁,while模拟自旋锁 *@paramkey *@return */ public Integer readIfNotExistThenPutCache2(String key){ while (true){ Object lock = redisTemplate.opsForValue().get("lock_key" + ":syn"); if(lock == null ){ // 获得锁 -> 加锁 -> 跳出循环 log.info(Thread.currentThread().getName() + ":获得锁"); redisTemplate.opsForValue().set("lock_key" + ":syn","lock"); break; } } try { String result = redisTemplate.opsForValue().get(key); if (StringUtils.isEmpty(result)){ log.info("获取key值为空,重新写入缓存"); result = String.valueOf(db_value.get()); redisTemplate.opsForValue().set(key, result, 5, TimeUnit.SECONDS); } log.info(Thread.currentThread().getName() + "获取到值" + result); return Integer.parseInt(result); }finally { redisTemplate.delete("lock_key" + ":syn"); log.info(Thread.currentThread().getName() + ":释放锁"); } }
上面的代码虽然看似在请求进来的时候会尝试加锁,但如果多个请求同时进来,因为会出现String result = redisTemplate.opsForValue().get(key) 为空的情况,因为他们的操作不具备原子性。
为了解决上面两个例子出现的问题,所以我们要用到Redis的原子性锁,它具有互斥性。
//该方法其实是使用了 redis 的指令 SET key value`NX // 1.key 不存在,设置成功返回 value setIfAbsent返回true // 2.key 存在,则设置失败返回 nul,setIfAbsent返回false // 3.原子性操作 Boolean setIfAbsent(K var1, V var2);