一 redis简介
- 基于内存的高性能的 key-value 数据库。
- NoSQL。
- 最终一致性,非ACID属性(CAP理论)。
- 支持基础事务。
Redis 优势:
- Redis支持数据的持久化
- 支持丰富数据类型,支持 string,list,set,zset,hash 等。
- Redis支持数据的备份,即 master-slave 模式的数据备份。
- 性能极高 – Redis 能读的速度是 110000 次/s,写的速度是 81000 次/s 。
- 支持事务 – Redis 的所有操作都是原子性的,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过 MULTI 和 EXEC指令包起来。MULTI、EXEC、DISCARD、WATCH。
- 丰富的特性:可用于缓存,消息(支持 publish/subscribe),按 key 设置过期时间,过期后将会自动删除。
Redis与Memcached 的优势与区别
- Memcached 所有的值均是简单的字符串,redis支持更为丰富的数据类
- Redis 的速度比 Memcached 快
- Redis 支持数据持久化。
redis为什么这么快
- 基于内存实现:(比磁盘I/O快,内存响应时长大约100纳秒)
- 高效的数据结构:(string、list、hash、dict、skiplist)
- 合理的底层数据编码
- 合适的线程模型:(I/O多路复用、避免上下文件切换、单线程模型)
- 单进程单线程架构:避免了线程切换和竞态产生的消耗(单线程的问题:某个命令执行过长,会造成其他命令的塞)
- 使用epoll作为I/O多路复用技术,再加上Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间.
二 redis基础知识
- redis.conf常用配置说明:
- demonize 以守护进程方式启动
- logfile 日志文件路径
- port 端口:默认端口6379
- dir 工作目录,AOF和RDB持久化文件生成的目录
- 基本操作
./redis-server /etc/redis.conf # 服务端启动
./redis-cli -h <host> -p <port> -a <password> # 客服端连接
./redis-cli shutdown # 客服端停止
ping # 测试客户端和服务器之间的连接是否有效,有效返回PONG
echo # 打印特定的信息, 如: echo 'HELLO WORLD'
quit/exit # 断开当前客户端与服务器之间的连接,可以重连
shutdown # 直接关闭服务器
select <index> # 切换数据库,默认有16个数据库(从零从始编号)
dbsize # 查看当前数据库的key的数量
flushdb # 清空当前库
flushall # 清空所有库
set key value # 存入字符串键值对
mset key value [key value ...] # 批量储字符串键值对
setnx key value # 存入一个不存在的字符串键值对
get key # 获取一个字符串的键值
mget key [key, key, key, ...] # 批量获取字符串的键值
del key # 删除一个键
expire key seconds # 设置一个键的过期时间(秒)
#Redis 如何设置密码及验证密码?在连接客户端时,添加-a password参数来进行解锁
config set requirepass 123456 #设置密码
auth 123456 #授权密码
三 Redis中的事务: WATCH,MULTI,EXEC,DISCARD
Redis事务以MULTI命令开始,EXEC/DISCARD命令结束
- 在开启事务时,Redis操作命令并不会立即执行,而是会先进入事务队列。Redis事务队列是一个FIFO队列
- 当执行EXEC命令时,会把事务队列中的命令逐个执行,不会被任何命令中断
MULTI # 开启事务(MULTI:开启事务)
INCT key
GET key
DISCARD # 取消事务(DISCARD: 取消事务)
###########################################
MULTI
INCR key
INCR key
EXEC # 执行事务(EXEC: 执行事务块中的命令)
Redis中的Watch机制,在开启事务前,使用WATCH命令监视指定的key,形成乐观锁
- 事务执行时,如果被WATCH的key发生了变化,则事务失效,不能执行。
- Redis事务时不支持回滚的。也就是说,如果事务中的命令执行出错,已经执行的命令不会撤回,后续的命令也会继续执行。
- Redis事务的命令分为入队和执行两个阶段。在入队(进入事务队列)时出错,Redis会忽略入队出错的命令,不会影响后续命令入队。
- 执行时出错,出错命令会被Redis忽略,其他没有出错的命令均会执行。
WATCH # 监视一个或者多个key,
UNWATCH # 取消WATCH对所有的key的监视
Redis事务不能嵌套。如果再收到MULTI命令开启事务,直接返回错误
- 遇到嵌套事务返回错误后,并不会影响事务队列继续接收命令,也不会修改事务队列中的任何数据。
- 多个客户端同时开启事务会怎样?
- 在并发情况,很容易出现多个客户端同时开启事务的情况。这时Redis会区分客户端维护事务队列吗?
- 另外,实际工程中为了提升性能,通常会用Redis连接池复用连接,这种情况下,会出现连接复用导致事务嵌套吗?
- Redis事务队列是与连接绑定的,不同的连接中允许同时开启事务,不会相互影响。
- 合理的事务管理器中,会在开启事务时绑定连接,防止连接被其他请求复用。
- 不过应用不当,会出现连接不能被归还的问题。(由此可见,一个优秀的事务管理器是多么重要)
事务中的错误处理
- 语法错误: 命令不存在,参数错误. 如果有语法错误,Redis在EXEC后直接返回错误,正确的命令也不会被执行
- 运行错误:指在运行命令的时候出现的问题,错误的不会被执行,正确的会被执行
四 Redis持久化机制(RDB/AOF)
RDB
以快照的方式将内存中的数据写入一个临时文件,恢复时,用这个临时文件来恢复数据。
数据的备份(save和bsave是已rdb方式持久化)
- save命令(同步指令):redis会创建一个RDB的二进制文件。如果有老的RDB文件,那么新的会替换掉老的文件。
- bgsave命令(异步指令):fork一个子进程来备份数据,生成一个RDB文件。
- 优点:
- 只有一个文件 dump.rdb,方便持久化。
- 容灾性好,一个文件可以保存到安全的磁盘。
- 性能最大化,fork 子进程来完成写操作,让主进程继续处理命令,所以是IO最大化。使用单独子进程来进行持久化,主进程不会进行任何IO操作,保证了redis的高性能)
- 相对于数据集大时,比AOF的启动效率更高。
- 缺点:
- 数据安全性低。
- RDB 是间隔一段时间进行持久化,如果持久化之间 redis 发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候。
AOF(Append-only file):记录命令行操作,将其保存为aof文件。
- 优点:
- 数据安全: AOF持久化可以配置redis.conf中的appendfsync属性实现。
- always:每一次写操作都会调用一次fsync,将数据记录到aof文件中.(性能有影响)
- everysec:每隔一秒进行一次fsync调用,将缓冲区中的数据写到磁盘。但是当这一 次的fsync调用时长超过1秒时。Redis会采取延迟fsync的策略,再等一秒钟。也就是在两秒后再进行fsync,这一次的fsync就不管会执行多长时间都会进行。这时候由于在fsync时文件描述符会被阻塞,所以当前的写操作就会阻塞。
- no:不会主动调用fsync去将AOF日志同步到磁盘。(操作系统自动调度刷磁盘,性能是最好的。)
- 通过append模式写文件,即使中途服务器宕机,可以通过 redis-check-aof工具解决数据一致性问题。
- AOF机制的 rewrite 模式。AOF 文件没被 rewrite 之前(文件过大时会对命令进行合并重写),可以删除其中的某些命令(比如误操作的 flushall))
- 缺点:
- AOF文件比RDB文件大,且恢复速度慢。
- 数据集大的时候,比rdb启动效率低。
redis过期键的删除策略(淘汰策略)
- 定时删除:在设置键的过期时间的同时,创建一个定时器 timer). 让定时器在键的过期时间来临时,立即执行对键的删除操作。
- 惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。
- 定期删除:每隔一段时间程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。
- Redis 的回收策略(6种淘汰机制)
- volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
- volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
- allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
- allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
- no-enviction(驱逐):禁止驱逐数据
注意:volatile 和 allkeys 规定了是对已设置过期时间的数据集淘汰数据还是从全部数据集淘汰数据,
后面的 lru、ttl 以及 random 是三种不同的淘汰策略,再加上一种 no-enviction 永不回收的策略。
- 使用策略规则:
- 如果数据呈现幂律分布,也就是一部分数据访问频率高,一部分数据访问频率低,则使用 allkeys-lru
- 如果数据呈现平等分布,也就是所有的数据访问频率都相同,则使用allkeys-random
五 redis缓存穿透、缓存雪崩、缓存击穿

缓存穿透
- 定义:通过key去查缓存,但是不存在对应的value,就穿透到DB查询。(利用不存在的key攻击DB)
- 优化:
- 查询返回的数据为空,仍把这个空结果进行缓存,但过期时间会比较短。
- 布隆过滤器:将所有可能存在的数据哈希到一个足够大的bitmap中,
- 一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对DB的查询。
缓存击穿

- 定义:对于设置了过期时间的 key,缓存在某个时间点过期的时候,
恰好这时间点对这个 Key 有大量的并发请求过来,
这些请求发现缓存过期一般都会从后端 DB 加载数据并回设到缓存,
这个时候大并发的请求可能会瞬间把 DB 压垮。 - 优化:
- 使用互斥锁:当缓存失效时,不立即去 load db,先使用如 Redis 的 setnx 去设置一个互斥锁,当操作成功返回时再进行 load db 的操作并回设缓存,否则重试 get 缓存的方法。
- key永远不过期(物理上不过期,逻辑过期,如key对应的值为“空”)
缓存雪崩: 与缓存击穿的区别:雪崩是很多 key,击穿是某一个key 缓存。
- 定义:缓存服务器大量缓存集中在某一时间段失效。导致请求都直接打到DB,导致系统崩溃。
- 优化
- 不同的key,设置不同的过期时间,让缓存失效的时间尽量均匀。
- 做二级缓存, 如:A为原始缓存,B为拷贝缓存。A缓存失效时间设置为短期,B设置为长期。
- 在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个 key 只允许一个线程查询数据和写缓存,其他线程等待。
六 redis集群
1. redis主从复制
- 作用:主从备份,防止节点宕机。使得读写分离,达到负载均衡的目的。
- 工作原理:Redis的主从结构可以采用一主多从或者级联结构,Redis主从复制可以根据是否是全量分为全量同步和增量同步。
- 全量同步:Redis全量复制一般发生在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份。具体步骤如下:
- 从服务器连接主服务器,发送SYNC命令
- 主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令;
- 主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令;
- 从服务器收到快照文件后丢弃所有旧数据,载入收到的快照;
- 主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;
- 从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;
- 增量同步:Redis增量复制是指Slave初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程。 增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。
- Redis主从同步策略:主从刚刚连接的时候,进行全量同步;全量同步结束后,进行增量同步。当然,如果有需要,slave 在任何时候都可以发起全量同步。redis 策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步。
- 注意点:如果多个Slave断线了,需要重启的时候,因为只要Slave启动,就会发送sync请求和主机全量同步,当多个同时出现的时候,可能会导致Master IO剧增宕机。
2. redis Sentinel
- 作用:主要是redis高可用的实现方案,具有故障发现,故障自动转移,客户端通知,
- Redis Sentinel中的Sentinel节点个数应该是大于等于3且最好为奇数。
3. codis
- redis集群最大节点个数16384个
七 redis应用场景
1. Redis实现分布式锁
所谓分布式锁就是在多个节点都可以访问的位置设置一个标志,
任务节点想访问被锁的资源时,首先尝试获取这个标志,
如果获取成功表示获取锁成功,如果获取失败表示获取锁失败,
使用redis实现分布锁,就是在准备一个redis节点,在redis设置一个key作为锁标志。
当key存在的时候就表示资源被锁,如果key不存在就说明资源处于空闲状态。
加锁的过程就是判断key是否存在,如果不存在就添加一个key.(注意:判断key是否存在和添加key的过程必须是原子操作)
可以使用redis的setnx方法,资源使用完成再将key删除释放锁。这里同样是原子操作。
问题一:如果持有锁的节点,在释放锁前宕机了,就会导致锁永远无法释放
解决:在添加key的时候设置一个过期时间,这样的话即使用持有锁的节点宕机了,Redis会自动释放锁。(还有问题二)
问题二:如果设置key的过期时间为20s, 如果一个请求一次操作时间超过20s, 那么就会导致持有锁的节点,还没有完成操作之前,锁就被redis给释放了。
解决:当某个节点获取到锁后,立刻启动一个子线程给自己续命。子线程的主要任务就是等key的过期时间到的时候,一般都是还剩1/3的时间的时候,就重新设置key的过期时间为20s, 当持有锁的节点完成操作之后,显式主动关闭子线程,然后释放锁。当持有锁的节点宕机之后,续命的子线程也会停止,锁也就自动释放了。
2. redis和mysql的双写一致性
方式一: 先更新缓存,再更新数据库
问题:如果更新缓存成功,更新数据库失败(缓存脏数据)
方式二: 先更新数据库, 再更新缓存
问题:高并发场景下
线程A更新了数据库中的数据。
这时由于网络或其它原因, 在线程A还没有来得及更新缓存的时候,线程B更新了数据库,同时也更新了缓存。
接着线程A才更新缓存,就导致线程B对缓存的更新丢失了,是不是有点像事件丢失的情况。
方式三:先删除缓存,再更新数据库
问题:这种方式,可以避免方式二中的缓存更新丢失的情况。
但是在高并发下,依然会有不一致的情况。
如:线程A先执行删除缓存,然后准备更新数据库。
这时线程B执行了读操作。发现缓存没有命中,所以从数据库读取,这时读取的是旧值。并肯线程B会将旧值写入缓存。
接着线程A完成了数据库的更新。【很明显数据库和缓存又出现了不一致的现象】
解决:在线程A完成数据库更新之后,稍作延迟,再删除一次缓存。【延迟时间>一次读作的时间】
方式四:先更新数据库,再删除缓存
问题:这种方式,存在数据不一致的场景
如: 线程A查询数据,正准备将数据写入缓存的时候。
线程B更新了数据库,然后执行了删除缓存的操作。
这时,线程A才把之前读取的旧值写入缓存。(这种现像出像的概率很底,因为写操作的时间大概率是比读操作的时间长,所以一般不会出现在一次读操作期间进行一次写操作)
解决:【延迟双删策略】(延迟双删的问题:如果删除缓存失败了怎么办)
尝试不断的循环删除,删除失败后,我们可以将要删除的key放入队列,然后重复尝试删除直到删除成功。
- 可以使用setnx命令实现CAS
3. 热点数据存储
- 最新评论,最新文章列表,使用list存储,ltrim取出热点数据,删除老数据。
4. 排行榜
- zset可以实现有序性操作,从而实现排行榜等功能。
5. 分布式ID生成
- 利用自增特性
6. 会话缓存(缓存一致性问题)
7.分布式队列/阻塞队列
- list是一个双向链表,可以通过lpush/rpush和rpop/lpop写入和读取消息。
- 可以通过使用brpop/blpop来实现阻塞队列。
8. redis限流的3种实现
- 基于Redis的setnx的操作,给指定的key设置了过期时间;
- 基于Redis的数据结构zset,将请求打造成一个zset数组;
- 基于Redis的令牌桶算法,输出速率大于输入速率,就要限流。
Redis 常见性能问题和解决方案
- Master 最好不要写内存快照,如果 Master 写内存快照,save 命令调度 rdbSave函数,会阻塞主线程的工作,当快照比较大时对性能影响是非常大的,会间断性暂停服务
- 如果数据比较重要,某个 Slave 开启 AOF 备份数据,策略设置为每秒同步一
- 为了主从复制的速度和连接的稳定性,Master 和 Slave 最好在同一个局域网
- 尽量避免在压力很大的主库上增加从
- 主从复制不要用图状结构,用单向链表结构更为稳定,即:Master <Slave1<Slave2 <Slave3…这样的结构方便解决单点故障问题,实现 Slave 对 Master的替换。如果 Master 挂了,可以立刻启用 Slave1 做 Master,其他不变。
在选择缓存时,什么时候选择 redis,什么时候选择 memcached?
选择 redis 的情况
- 复杂数据结构,value 的数据是哈希,列表,集合,有序集合等这种情况下,会选择redis,
因为 memcache 无法满足这些数据结构,最典型的的使用场景是,用户订单列表,用户消息,帖子评论等。 - 需要进行数据的持久化功能,但是注意,不要把 redis 当成数据库使用,如果 redis挂了,
内存能够快速恢复热数据,不会将压力瞬间压在数据库上,没有 cache 预热的过程。
对于只读和数据一致性要求不高的场景可以采用持久化存储 - 高可用,redis 支持集群,可以实现主动复制,读写分离,
而对于 memcache 如果想要实现高可用,需要进行二次开发。 - 存储的内容比较大,memcache 存储的 value 最大为 1M。
选择 memcache 的场景
- 纯 KV,数据量非常大的业务,使用 memcache 更合适,原因是:
- memcache 的内存分配采用的是预分配内存池的管理方式,能够省去内存分配的时间,
redis 是临时申请空间,可能导致碎片化。 - 虚拟内存使用,memcache 将所有的数据存储在物理内存里,redis 有自己的 vm 机制,
理论上能够存储比物理内存更多的数据,当数据超量时,引发 swap,把冷数据刷新到磁盘上,
从这点上,数据量大时,memcache 更快 - 网络模型,memcache 使用非阻塞的 IO 复用模型,
redis 也是使用非阻塞的 IO 复用模型,
但是 redis 还提供了一些非 KV 存储之外的排序,
聚合功能,复杂的 CPU 计算,会阻塞整个 IO 调度,
从这点上由于 redis 提供的功能较多,memcache 更快些 - 线程模型,memcache 使用多线程,主线程监听,
worker 子线程接受请求,执行读写,这个过程可能存在锁冲突。
redis 使用的单线程,虽然无锁冲突,但是难以利用多核的特性提升吞吐量。
网友评论