redis是什么?看着一篇就够了

        目录

介绍一下 redis 数据库?

redis数据类型与应用场景

redis 为什么更快?

redis 怎么实现持久化的?

AOF 日志是如何实现的?

RDB 快照是如何实现的呢?

混合持久化

redis 单线程在多核机器里使用会不会浪费机器资源?

redis 执行命令还是单线程,那如何利用多核心来提升性能?

redis 缓存穿透、缓存击穿、缓存雪崩是什么?怎么解决?

缓存雪崩

解决方法

缓存击穿

解决方案:

缓存穿透

解决方案

怎么用 redis 分布式锁?

如何设计秒杀场景处理高并发以及超卖现象?

利用分布式锁

利用分布式锁 + 分段缓存

利用 redis 的 incr、decr 的原子性 + 异步队列

如果对热点数据设置过期时间,活动结束后删除可能会阻塞主线程,怎么解决?

Redis 内存淘汰策略


介绍一下 redis 数据库?

Redis 是一种基于内存的非关系型数据库,对数据的读写操作都是在内存中完成,因此读写速度非常快,常用于缓存,消息队列、分布式锁等场景。 Redis 提供了多种数据类型来支持不同的业务场景,比如 String(字符串)、Hash(哈希)、 List (列表)、Set(集合)、Zset(有序集合)、Bitmaps(位图)、HyperLogLog(基数统计)、GEO (地理信息)、Stream(流),并且对数据类型的操作都是原子性的,因为执行命令由单线程负责的,不存在并发竞争的问题。 除此之外,Redis 还支持事务 、持久化、Lua 脚本、多种集群方案(主从复制模式、哨兵模式、切片机群模式)、发布/订阅模式,内存淘汰机制、过期删除机制等等。

redis数据类型与应用场景

Redis 常见数据类型和应用场景 | 小林coding (xiaolincoding.com)

Redis 的应用场景包括:缓存系统(“热点”数据:高频读、低频写)、计数器、消息队列系统、排行榜、社交网络和实时系统

redis 为什么更快?

官方使用基准测试的结果是,单线程的 Redis 吞吐量可以达到 10W/ 每秒,如下图所示:

之所以 Redis 采用单线程(网络 I/O 和执行命令)那么快,有如下几个原因:

• Redis 的大部分操作 都在内存中完成 ,并且采用了高效的数据结构,因此 Redis 瓶颈可能是机器的内存或者网络带宽,而并非 CPU,既然 CPU 不是瓶颈,那么自然就采用单线程的解决方案。

• Redis 采用单线程模型可以避免了多线程之间的竞争 ,省去了多线程切换带来的时间和性能上的开销,而且也不会导致死锁问题。

• Redis 采用了 I/O 多路复用机制 处理大量的客户端 Socket 请求,IO 多路复用机制是指一 个线程处理多个 IO 流,就是我们经常听到的 select/epoll 机制。简单来说,在 Redis 只运 行单线程的情况下,该机制允许内核中,同时存在多个监听 Socket 和已连接 Socket。内 核会一直监听这些 Socket 上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。

redis 怎么实现持久化的?

Redis 的读写操作都是在内存中,所以 Redis 性能才会高,但是当 Redis 重启后,内存中的数 据就会丢失,那为了保证内存中的数据不会丢失,Redis 实现了数据持久化的机制,这个机制会把数据存储到磁盘,这样在 Redis 重启就能够从磁盘中恢复原有的数据。 Redis 共有两种数据持久化的方式:

AOF 日志 :每执行一条写操作命令,就把该命令以追加的方式写入到一个文件里;

RDB 快照 :将某一时刻的内存数据,以二进制的方式写入磁盘;

AOF 日志是如何实现的?

Redis 在执行完一条写操作命令后,就会把该命令以追加的方式写入到一个文件里,然后 Redis 重启时,会读取该文件记录的命令,然后逐一执行命令的方式来进行数据恢复。

我这里以「_set name xiaolin_」命令作为例子,Redis 执行了这条命令后,记录在 AOF 日 志里的内容如下图:

Redis 提供了 3 种写回硬盘的策略, 在 Redis.conf 配置文件中的 appendfsync 配置项可以 有以下 3 种参数可填:

• Always ,这个单词的意思是「总是」,所以它的意思是每次写操作命令执行完后,同步将 AOF 日志数据写回硬盘;

• Everysec ,这个单词的意思是「每秒」,所以它的意思是每次写操作命令执行完后,先将 命令写入到 AOF 文件的内核缓冲区,然后每隔一秒将缓冲区里的内容写回到硬盘;

• No ,意味着不由 Redis 控制写回硬盘的时机,转交给操作系统控制写回的时机,也就是每 次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,再由操作系统决定何时 将缓冲区内容写回硬盘。

我也把这 3 个写回策略的优缺点总结成了一张表格:

RDB 快照是如何实现的呢?

因为 AOF 日志记录的是操作命令,不是实际的数据,所以用 AOF 方法做故障恢复时,需要全 量把日志都执行一遍,一旦 AOF 日志非常多,势必会造成 Redis 的恢复操作缓慢。为了解决 这个问题,Redis 增加了 RDB 快照。所谓的快照,就是记录某一个瞬间东西,比如当我们给 风景拍照时,那一个瞬间的画面和信息就记录到了一张照片。所以,RDB 快照就是记录某一个 瞬间的内存数据,记录的是实际数据,而 AOF 文件记录的是命令操作的日志,而不是实际的 数据。因此在 Redis 恢复数据时, RDB 恢复数据的效率会比 AOF 高些,因为直接将 RDB 文 件读入内存就可以,不需要像 AOF 那样还需要额外执行操作命令的步骤才能恢复数据。 Redis 提供了两个命令来生成 RDB 文件,分别是 save 和 bgsave,他们的区别就在于是否在 「主线程」里执行:

• 执行了 save 命令,就会在主线程生成 RDB 文件,由于和执行操作命令在同一个线程,所 以如果写入 RDB 文件的时间太长, 会阻塞主线程;

• 执行了 bgsave 命令,会创建一个子进程来生成 RDB 文件,这样可以 避免主线程的阻塞;

混合持久化

在 Redis 4.0 提出了混合持久化。如果想要开启混合持久化功能,可以在 Redis 配置文件将下 面这个配置项设置成 yes:

aof-use-rdb-preamble yes

混合持久化是工作在 AOF 日志重写过程。当开启了混合持久化时,在 AOF 重写日志时, fork 出来的重写子进程会先将与主线程共享的内存数据以 RDB 方式写入到 AOF 文件,然后 主线程处理的操作命令会被记录在重写缓冲区里,重写缓冲区里的增量命令会以 AOF 方式写 入到 AOF 文件,写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换 旧的的 AOF 文件。也就是说,使用了混合持久化,AOF 文件的前半部分是 RDB 格式的全量 数据,后半部分是 AOF 格式的增量数据。

这样的好处在于,重启 Redis 加载数据的时候,由于前半部分是 RDB 内容,这样加载的时候速度会很快。

redis 单线程在多核机器里使用会不会浪费机器资源?

虽然 Redis 的主要工作(网络 I/O 和执行命令)一直是单线程模型,但是在 Redis 6.0 版本之 后,也采用了多个 I/O 线程来处理网络请求,这是因为随着网络硬件的性能提升,Redis 的性 能瓶颈有时会出现在网络 I/O 的处理上。

所以为了提高网络 I/O 的并行度,Redis 6.0 对于网络 I/O 采用多线程来处理。但是对于命令 的执行,Redis 仍然使用单线程来处理。

Redis 官方表示,Redis 6.0 版本引入的多线程 I/O 特性对性能提升至少是一倍以上。

redis 执行命令还是单线程,那如何利用多核心来提升性能?

可以在系统部署多个 redis docker 容器来处理,达到充分利用 cpu 多核心的效果

redis 缓存穿透、缓存击穿、缓存雪崩是什么?怎么解决?

缓存雪崩

当大量缓存数据在同一时间过期或者 Redis 故障宕机时,如果此时有大量的用户请求,都无法 在 Redis 中处理,于是全部请求都直接访问数据库,从而导致数据库的压力增加,严重的会造 成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃。

解决方法

大量数据同时过期

◦ 均匀设置过期时间 :避免将大量的数据设置成同一个过期时间。

◦ 互斥锁 :当业务线程在处理用户请求时,如果发现访问的数据不在 Redis 里,就加个 互斥锁,保证同一时间内只有一个请求来构建缓存。未能获取互斥锁的请求等待锁释放 后重新读取缓存,或者返回空值或者默认值。

◦ 双 key 策略 :使用两个 key,一个是主 key,设置过期时间,一个是备 key,不会设置 过期,key 不一样,但是 value 值是一样。当业务线程访问不到主 key 的缓存数据时, 就直接返回备 key 的缓存数据,然后在更新缓存的时候,同时更新主 key 和备 key 的数 据。

◦ 后台更新缓存 :业务线程不再负责更新缓存,缓存也不设置有效期,而是让缓存“永久 有效”,并将更新缓存的工作交由后台线程定时更新。

Redis 故障宕机

◦ 服务熔断或请求限流机制:启动 服务熔断 机制, 暂停业务应用对缓存服务的访问,直 接返回错误 ,所以不用再继续访问数据库,保证数据库系统的正常运行,等到 Redis 恢复正常后,再允许业务应用访问缓存服务。服务熔断机制是保护数据库的正常允许, 但是暂停了业务应用访问缓存服系统,全部业务都无法正常工作。也可以启用 请求限流 机制, 只将少部分请求发送到数据库进行处理,再多的请求就在入口直接拒绝服务。

◦ 构建高可靠集群:通过 主从节点的方式构建 Redis 缓存高可靠集群 。如果 Redis 缓存 的主节点故障宕机,从节点可以切换成为主节点,继续提供缓存服务,避免了由于 Redis 故障宕机而导致的缓存雪崩问题。

缓存击穿

如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读 取,直接访问数据库,数据库很容易就被高并发的请求冲垮。

解决方案:

• 互斥锁方案 :保证同一时间只有一个业务线程更新缓存,未能获取互斥锁的请求,要么等 待锁释放后重新读取缓存,要么就返回空值或者默认值。

• 不给热点数据设置过期时间 :由后台异步更新缓存,或者在热点数据准备要过期前,提前 通知后台线程更新缓存以及重新设置过期时间。

缓存穿透

当用户访问的数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺 失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后 续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是缓存穿透的问题。

解决方案

• 非法请求的限制 :当有大量恶意请求访问不存在的数据的时候会发生缓存穿透,可以在 API 入口处判断求请求参数是否合理,请求参数是否含有非法值、请求字段是否存在,如果 判断出是恶意请求就直接返回错误,避免进一步访问缓存和数据库。

• 缓存空值或者默认值 :当线上业务发现缓存穿透的现象时,可以针对查询的数据,在缓存 中设置一个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给 应用,而不会继续查询数据库。

• 使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在 :可以 在写入数据库数据时,使用布隆过滤器做个标记,然后在用户请求到来时,业务线程确认缓 存失效后,可以通过查询布隆过滤器快速判断数据是否存在,如果不存在,就不用通过查询 数据库来判断数据是否存在。

怎么用 redis 分布式锁?

基于 Redis 节点实现分布式锁时,对于加锁操作,我们需要满足三个条件。

• 加锁包括了读取锁变量、检查锁变量值和设置锁变量值三个操作,但需要以原子操作的方式 完成,所以,我们使用 SET 命令带上 NX 选项来实现加锁;

• 锁变量需要设置过期时间,以免客户端拿到锁后发生异常,导致锁一直无法释放,所以,我 们在 SET 命令执行时加上 EX/PX 选项,设置其过期时间;

• 锁变量的值需要能区分来自不同客户端的加锁操作,以免在释放锁时,出现误释放操作,所 以,我们使用 SET 命令设置锁变量值时,每个客户端设置的值是一个唯一值,用于标识客 户端;

满足这三个条件的分布式命令如下:

SET lock_key unique_value NX PX 10000 

• lock_key 就是 key 键;

• unique_value 是客户端生成的唯一的标识,区分来自不同客户端的锁操作;

• NX 代表只在 lock_key 不存在时,才对 lock_key 进行设置操作;

• PX 10000 表示设置 lock_key 的过期时间为 10s,这是为了避免客户端发生异常而无法释 放锁。

而解锁的过程就是将 lock_key 键删除(del lock_key),但不能乱删,要保证执行操作的客 户端就是加锁的客户端。所以,解锁的时候,我们要先判断锁的 unique_value 是否为加锁客 户端,是的话,才将 lock_key 键删除。

可以看到,解锁是有两个操作,这时就需要 Lua 脚本来保证解锁的原子性,因为 Redis 在执 行 Lua 脚本时,可以以原子性的方式执行,保证了锁释放操作的原子性。

// 释放锁时,先比较 unique_value 是否相等,避免锁的误释放

if redis.call("get",KEYS[1]) == ARGV[1] then    

return redis.call("del",KEYS[1])

else    

return 0

end

这样一来,就通过使用 SET 命令和 Lua 脚本在 Redis 单节点上完成了分布式锁的加锁和解锁。

如何设计秒杀场景处理高并发以及超卖现象?

1. 在查询商品库存时加排他锁,执行如下语句:

select * from goods for where goods_id=? for update

在事务中线程 A 通过 语句给 goods_id 为具体的id的数据行上了锁。那么其他线程此时可以使用 select 语句读取数据,但是如 果也使用 select for update 语句加锁,或者使用 update,delete 都会阻塞,直到线程 A 将事 务提交(或者回滚),其他线程中的某个线程排在线程 A 后的线程才能获取到锁。

1. 更新数据库减库存的时候,进行库存限制条件

update goods set stock = stock - 1 where goods_id = ? and stock >0

这种通过数据库加锁来解决的方案,性能不是很好,在高并发的情况下,还可能存在因为获取 不到数据库连接或者因为超时等待而报错。

利用分布式锁

同一个锁 key,同一时间只能有一个客户端拿到锁,其他客户端会陷入无限的等待来尝试获取 那个锁,只有获取到锁的客户端才能执行下面的业务逻辑。这种方案的缺点是同一个商品在多用户同时下单的情况下,会基于分布式锁串行化处理,导致没法同时处理同一个商品的大量下单的请求。

利用分布式锁 + 分段缓存

把数据分成很多个段,每个段是一个单独的锁,所以多个线程过来并发修改数据的时候,可以 并发的修改不同段的数据 假设场景:假如你现在商品有 100 个库存,在 redis 存放 5 个库存 key,形如 :

key1=goods-01,value=20;
key2=goods-02,value=20;
key3=goods-03,value=20

用户下单时对用户 id 进行 %5 计算,看落在哪个 redis 的 key 上,就去取哪个,这样每次就能 够处理 5 个进程请求 这种方案可以解决同一个商品在多用户同时下单的情况,但有个坑需要解 决:当某段锁的库存不足,一定要实现自动释放锁然后换下一个分段库存再次尝试加锁处理, 此种方案复杂比较高。

利用 redis 的 incr、decr 的原子性 + 异步队列

实现思路

  1. 在系统初始化时,将商品的库存数量加载到 redis 缓存中
  2. 接收到秒杀请求时,在 redis 中进行预减库存(利用 redis decr 的原子性),当 redis 中 的库存不足时,直接返回秒杀失败,否则继续进行第 3 步;
  3. 将请求放入异步队列中,返回正在排队中;
  4. 服务端异步队列将请求出队(哪些请求可以出队,可以根据业务来判定,比如:判断对 应用户是否已经秒杀过对应商品,防止重复秒杀),出队成功的请求可以生成秒杀订单,减 少数据库库存(在扣减库存的 sql 如下,返回秒杀订单详情)
update goods set stock = stock - 1 where goods_id = ? and stock >0

        5.用户在客户端申请秒杀请求后,进行轮询,查看是否秒杀成功,秒杀成功则进入秒杀订单详情,否则秒杀失败

这种方案的缺点:由于是通过异步队列写入数据库中,可能存在数据不一致,其次引用多个组 件复杂度比较高

如果对热点数据设置过期时间,活动结束后删除可能会阻塞主线程,怎么解决?

可以使用 unlink 的方式来删除缓存,unlink 是异步删除数据,不会阻塞主线程。

Redis 内存淘汰策略

Redis 内存淘汰策略共有八种,这八种策略大体分为「不进行数据淘汰」和「进行数据淘汰」 两类策略。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值