Redis数据库
说明
本文仅供本人学习,如果有不对的地方欢迎大家一起讨论
Redis基础
什么是Redis?
Redis 是一种基于内存的非关系型数据库,对数据的读写操作都是在内存中完成,因此读写速度非常快,常用于缓存,消息队列、分布式锁等场景。对数据类型的操作都是**原子性(Redis的事务不支持原子性)**的,因为执行命令由单线程负责的,不存在并发竞争的问题。
Redis的几种数据类型
String(字符串)、Hash(哈希)、 List (列表)、Set(集合)、Zset(有序集合)、Bitmaps(位图)、HyperLogLog(基数统计)、GEO(地理信息)、Stream(流)
- String 类型的应用场景:缓存对象、常规计数、分布式锁、共享 session 信息等。
- List 类型的应用场景:消息队列(但是有两个问题:1. 生产者需要自行实现全局唯一 ID;2. 不能以消费组形式消费数据)等。
- Hash 类型:缓存对象、购物车等。
- Set 类型:聚合计算(并集、交集、差集)场景,比如点赞、共同关注、抽奖活动等。
- Zset 类型:排序场景,比如排行榜、电话和姓名排序等。
Redis 后续版本又支持四种数据类型,它们的应用场景如下:
- BitMap(2.2 版新增):二值状态统计的场景,比如签到、判断用户登陆状态、连续签到用户总数等;
- HyperLogLog(2.8 版新增):海量数据基数统计的场景,比如百万级网页 UV 计数等;
- GEO(3.2 版新增):存储地理位置信息的场景,比如滴滴叫车;
- Stream(5.0 版新增):消息队列,相比于基于 List 类型实现的消息队列,有这两个特有的特性:自动生成全局唯一消息ID,支持以消费组形式消费数据。一般不使用,消息队列一般用MQ来实现
Redis 和 Memcached 有什么区别?
共同点
- 都是基于内存的数据库,一般都用来当做缓存使用。
- 都有过期策略。
- 两者的性能都非常高。
区别
- Redis 支持的数据类型更丰富(String、Hash、List、Set、ZSet),而 Memcached 只支持最简单的 key-value 数据类型;
- Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而 Memcached 没有持久化功能,数据全部存在内存之中,Memcached 重启或者挂掉后,数据就没了;
- Redis 原生支持集群模式,Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;
- Redis 支持发布订阅模型、Lua 脚本、事务等功能,而 Memcached 不支持;
为什么用 Redis 作为 MySQL 的缓存?
主要是因为 Redis 具备「高性能」和「高并发」两种特性。
-
Redis 具备高性能
基于内存,所以操作数据的速度快。
-
Redis 具备高并发
单台设备Redis的QPS是MySQL的10倍,所以,直接访问 Redis 能够承受的请求是远远大于直接访问 MySQL 的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。
Redis优缺点
优点
- 基于内存操作,内存读写速度快。
- 支持多种数据类型,包括String、Hash、List、Set、ZSet等。
- 支持持久化,Redis支持RDB和AOF两种持久化机制,持久化功能可以有效的避免数据丢失问题。
- 支持事务,Redis的事务不支持原子性。
- 支持主从复制,主节点会自动将数据同步到从节点,从节点可以用于读取,主节点负责写入,实现读写分离。
- Redis命令的处理是单线程的。
缺点
- 对结构化查询支持较差
- 数据库容量受物理内存的限制,不适合用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的操作。
- Redis较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。
Redis为什么快?
1、基于内存,内存离cpu最近
2、IO多路复用
一个服务器系统,正常每个客户端和服务端连接,每个连接都需要一个线程
优化:维护线程消耗资源,肯定要想办法尽量减少new线程不是每个连接时时刻刻都有数据的,一个线程监控多个客户端socket连接
io多路复用模型:select poll epoll
3、单线程 减少上下文切换
持久化
Redis 如何实现数据不丢失?
RDB快照
默认的就是RDB,一般情况下不需要修改,比AOF方式更高效,但是如果最后一次持久化宕机数据可能会丢失。
AOF日志
每执行一条写操作命令,就把该命令以追加的方式写入到一个文件里;Redis 在执行完一条写操作命令后,就会把该命令以追加的方式写入到一个文件里,然后 Redis 重启时,会读取该文件记录的命令,然后逐一执行命令的方式来进行数据恢复。
混合持久化方式
RDB 优点是数据恢复速度快,但是快照的频率不好把握。频率太低,丢失的数据就会比较多,频率太高,就会影响性能。 AOF 优点是丢失数据少,但是数据恢复不快。
Redis集群
Redis如何实现服务高可用?
设计一个高可用的 Redis 服务,一定要从 Redis 的多服务节点来考虑,比如 Redis 的主从复制、哨兵模式、切片集群。
主从复制
主从复制是 Redis 高可用服务的最基础的保证,实现方案就是将从前的一台 Redis 服务器,同步数据到多台从 Redis 服务器上,即一主多从的模式,且主从服务器之间采用的是「读写分离」的方式。主服务器可以进行读写操作,当发生写操作时自动将写操作同步给从服务器,而从服务器一般是只读,并接受主服务器同步过来写操作命令,然后执行这条命令。所有的数据修改只在主服务器上进行,然后将最新的数据同步给从服务器,这样就使得主从服务器的数据是一致的。
主从服务器之间的命令复制是异步进行的。
哨兵模式
为了解决在使用 Redis 主从服务的时候当 Redis 的主从服务器出现故障宕机时,需要手动进行恢复这个问题,Redis 增加了哨兵模式(Redis Sentinel),因为哨兵模式做到了可以监控主从服务器,并且提供主从节点故障转移的功能。
Redis 过期删除与内存淘汰
Redis 使用的过期删除策略是什么?
Redis 是可以对 key 设置过期时间的,因此需要有相应的机制将已过期的键值对删除,而做这个工作的就是过期键值删除策略。
每当我们对一个 key 设置了过期时间时,Redis 会把该 key 带上过期时间存储到一个过期字典(expires dict)中,也就是说「过期字典」保存了数据库中所有 key 的过期时间。
当我们查询一个 key 时,Redis 首先检查该 key 是否存在于过期字典中:
- 如果不在,则正常读取键值;
- 如果存在,则会获取该 key 的过期时间,然后与当前系统时间进行比对,如果比系统时间大,那就没有过期,否则判定该 key 已过期。
Redis 使用的过期删除策略是「惰性删除+定期删除」这两种策略配和使用。
什么是惰性删除策略?
惰性删除策略的做法是,不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。
惰性删除策略的优点:
- 因为每次访问时,才会检查 key 是否过期,所以此策略只会使用很少的系统资源,因此,惰性删除策略对 CPU 时间最友好。
惰性删除策略的缺点:
- 如果一个 key 已经过期,而这个 key 又仍然保留在数据库中,那么只要这个过期 key 一直没有被访问,它所占用的内存就不会释放,造成了一定的内存空间浪费。所以,惰性删除策略对内存不友好。
什么是定期删除策略?
定期删除策略的做法是,每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key。
Redis 的定期删除的流程:
- 从过期字典中随机抽取 20 个 key;
- 检查这 20 个 key 是否过期,并删除已过期的 key;
- 如果本轮检查的已过期 key 的数量,超过 5 个(20/4),也就是「已过期 key 的数量」占比「随机抽取 key 的数量」大于 25%,则继续重复步骤 1;如果已过期的 key 比例小于 25%,则停止继续删除过期 key,然后等待下一轮再检查。
定期删除策略的优点:
- 通过限制删除操作执行的时长和频率,来减少删除操作对 CPU 的影响,同时也能删除一部分过期的数据减少了过期键对空间的无效占用。
定期删除策略的缺点:
- 难以确定删除操作执行的时长和频率。如果执行的太频繁,就会对 CPU 不友好;如果执行的太少,那又和惰性删除一样了,过期 key 占用的内存不会及时得到释放。
Redis 持久化时,对过期键会如何处理的?
Redis 持久化文件有两种格式:RDB(Redis Database)和 AOF(Append Only File)
RDB 文件分为两个阶段,RDB 文件生成阶段和加载阶段。
- RDB 文件生成阶段:从内存状态持久化成 RDB(文件)的时候,会对 key 进行过期检查,过期的键「不会」被保存到新的 RDB 文件中,因此 Redis 中的过期键不会对生成新 RDB 文件产生任何影响。
- RDB 加载阶段:RDB 加载阶段时,要看服务器是主服务器还是从服务器,分别对应以下两种情况:
- 如果 Redis 是「主服务器」运行模式的话,在载入 RDB 文件时,程序会对文件中保存的键进行检查,过期键「不会」被载入到数据库中。所以过期键不会对载入 RDB 文件的主服务器造成影响;
- 如果 Redis 是「从服务器」运行模式的话,在载入 RDB 文件时,不论键是否过期都会被载入到数据库中。但由于主从服务器在进行数据同步时,从服务器的数据会被清空。所以一般来说,过期键对载入 RDB 文件的从服务器也不会造成影响。
AOF 文件分为两个阶段,AOF 文件写入阶段和 AOF 重写阶段。
- AOF 文件写入阶段:当 Redis 以 AOF 模式持久化时,如果数据库某个过期键还没被删除,那么 AOF 文件会保留此过期键,当此过期键被删除后,Redis 会向 AOF 文件追加一条 DEL 命令来显式地删除该键值。
- AOF 重写阶段:执行 AOF 重写时,会对 Redis 中的键值对进行检查,已过期的键不会被保存到重写后的 AOF 文件中,因此不会对 AOF 重写造成任何影响。
Redis 主从模式中,对过期键会如何处理?
当 Redis 运行在主从模式下时,从库不会进行过期扫描,从库对过期的处理是被动的。也就是即使从库中的 key 过期了,如果有客户端访问从库时,依然可以得到 key 对应的值,像未过期的键值对一样返回。
从库的过期键处理依靠主服务器控制,主库在 key 到期时,会在 AOF 文件里增加一条 del 指令,同步到所有的从库,从库通过执行这条 del 指令来删除过期的 key。
Redis 内存满了,会发生什么?
在 Redis 的运行内存达到了某个阀值,就会触发内存淘汰机制(一般是LRU算法,最近最少使用),这个阀值就是我们设置的最大运行内存,此值在 Redis 的配置文件中可以找到,配置项为 maxmemory。
Redis 缓存设计
如何避免缓存雪崩、缓存击穿、缓存穿透?
如何避免缓存雪崩?
缓存雪崩:大量缓存数据在同一时间过期(失效)时,如果此时有大量的用户请求,都无法在 Redis 中处理,于是全部请求都直接访问数据库,从而导致数据库的压力骤增,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃。
对于缓存雪崩问题,我们可以采用两种方案解决。
- 将缓存失效时间随机打散: 我们可以在原有的失效时间基础上增加一个随机值(比如 1 到 10 分钟)这样每个缓存的过期时间都不重复了,也就降低了缓存集体失效的概率。
- 设置缓存不过期: 我们可以通过后台服务来更新缓存数据,从而避免因为缓存失效造成的缓存雪崩,也可以在一定程度上避免缓存并发问题。
如何避免缓存击穿?
缓存击穿:如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮。
应对缓存击穿可以采取前面说到两种方案:
- 互斥锁方案(Redis 中使用 setNX 方法设置一个状态位,表示这是一种锁定状态),保证同一时间只有一个业务线程请求缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。
- 不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间;
如何避免缓存穿透?
缓存穿透:当用户访问的数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增
缓存穿透的发生一般有这两种情况:
- 业务误操作,缓存中的数据和数据库中的数据都被误删除了,所以导致缓存和数据库中都没有数据;
- 黑客恶意攻击,故意大量访问某些读取不存在数据的业务;
应对缓存穿透的方案,常见的方案有三种。
- 非法请求的限制:当有大量恶意请求访问不存在的数据的时候,也会发生缓存穿透,因此在 API 入口处我们要判断求请求参数是否合理,请求参数是否含有非法值、请求字段是否存在,如果判断出是恶意请求就直接返回错误,避免进一步访问缓存和数据库。
- 设置空值或者默认值:当我们线上业务发现缓存穿透的现象时,可以针对查询的数据,在缓存中设置一个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应用,而不会继续查询数据库。
- 使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在:我们可以在写入数据库数据时,使用布隆过滤器做个标记,然后在用户请求到来时,业务线程确认缓存失效后,可以通过查询布隆过滤器快速判断数据是否存在,如果不存在,就不用通过查询数据库来判断数据是否存在,即使发生了缓存穿透,大量请求只会查询 Redis 和布隆过滤器,而不会查询数据库,保证了数据库能正常运行,Redis 自身也是支持布隆过滤器的。
说说常见的缓存更新策略?
常见的缓存更新策略共有3种:
- Cache Aside(旁路缓存)策略;
- Read/Write Through(读穿 / 写穿)策略;
- Write Back(写回)策略;
实际开发中,Redis 和 MySQL 的更新策略用的是 Cache Aside,另外两种策略应用不了。
其他一些问题
redis基于内存,如何节约内存资源?
过期删除:
redis的key一般都有一个过期时间,过期数据如何删除?
随机部分抽查+惰性删除,先查数据返回,发现过期了再删除,
内存淘汰:
内存空间有限,满了怎么办?使用默认的LRU算法 最近最少使用算法
内存数据一旦重启,会不会丢失?----> 持久化操作
全部备份 不合适 太慢了
只记录最新数据的变化也不行,因为不全
RDB:全量备份
AOF:增量备份
增量备份 aof 记录redis命令,文件太多了太大了?aof重写
什么时候重写?
aof文件太大?按照命令条数?文件大小?根据配置文件来
redis主从怎么同步?先同步rdb文件,然后再同步aof
分布式锁
分布式环境下保证同一时间只有一个线程独享共享资源
MySQL:给一个字段设置一个唯一索引,并发插入只能有一个能成功
zookeeper:临时顺序节点
为什么用redis分布式锁?
1、快
2、redission框架出来的早,功能比较完善
数据结构 原理
SET NX(set if not exist):如果不存在就赋值
redis是单线程的,确保了只有一个能拿到锁
一般使用线程thread.id避免重复
hash结构 两层hash map
第一层 key 业务key 比如skuCode 比如订单id
第二层 key threadId 防止别的线程解锁
看门狗 watchDog
默认30s过期,每隔10s会去续约一次
四种缓存模式
- 旁路缓存模式(Cache-Aside Pattern)
- 读穿透模式(Read Through Cache Pattern)
- 写穿透模式(Write Through Cache Pattern)
- 异步缓存写入模式(Write Behind Pattern,又称Write Back)
一般使用旁路缓存模式就可以满足日常开发和使用了,先查Redis缓存,没有命中再查MySQL数据库再将数据写入到Redis。
缓存一致性
一般使用过期时间来兜底,并且再更新DB后删除缓存来提升一致性的方式。另外还可以通过订阅binlog的方案来实现,但是这种方案额外引入了消息队列和消费服务,成本太高而收益不足,所以一般都使用第一种方案。
LUA脚本
- 支持if else的选择结构
- 事务中间如果失败,会中断后续后续执行
- 使用方便,开始LUA就可以执行事务