
Redis基础
Redis是一款内存高速缓存数据库。Redis全称为:Remote Dictionary Server(远程数据服务),使用C语言编写,Redis是一个key-value存储系统(键值存储系统),支持丰富的数据类型,如:String、list、set、zset、hash。Redis是一种支持key-value等多种数据结构的存储系统。可用于缓存,事件发布或订阅,高速队列等场景。支持网络,提供字符串,哈希,列表,队列,集合结构直接存取,基于内存,可持久化。
为什么要使用redis
使用redis主要基于其性能与并发的特点。
比如一个执行耗时久并且结果变动不频繁的SQL,可以尝试将结果放到缓存中,使得请求可以快速响应。
再比如一个并发很大的功能,如果每次都从数据库获取数据,由于数据库响应问题,可能会出现连接异常,照成响应失败,这时可以将数据放在redis种,提高响应速度,降低数据库压力。
当然,redis除了性能与并发,也存在其他特点,现将这些特点总结如下:
- 读写性能优异:Redis能读的速度是110000次/s,写的速度是81000次/s
- 数据类型丰富:Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作
- 原子性:Redis的所有操作都是原子性的,同时Redis还支持对几个操作全并后的原子性执行
- 发布订阅:Redis支持发布/订阅模式
- 持久化:Redis支持RDB, AOF等持久化方式
- 支持分布式部署
redis为什么响应快
redis响应快的主要因素有:
- redis是基于内存的,读写都在内存中进行
- redis是单线程的,不存在上下文切换线程
- redis采用多路复用IO,可以处理并发连接
- 非阻塞IO内部实现采用epoll,采用了epoll+自己实现的简单的事件框架。epoll中的读、写、关闭、连接都转化成了事件,然后利用epoll的多路复用特性,绝不在io上浪费一点时间。
- 数据结构优化,redis为key-value的存储,使用hash结构,读写速度快;对于特殊的数据类型也引入了特殊的数据结构进行优化,比如使用跳表加快有序的数据结构(比如zset)的读取速度
redis为什么是单线程
因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了。
- 不需要各种锁的性能消耗
redis的数据结构并不全是简单的Key-Value,还有list,hash等复杂的结构,这些结构有可能会进行很细粒度的操作,比如在很长的列表后面添加一个元素,在hash当中添加或者删除
一个对象。这些操作可能就需要加非常多的锁,导致的结果是同步开销大大增加。
总之,在单线程的情况下,就不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗。
- 单线程多进程集群方案
单线程的威力实际上非常强大,每核心效率也非常高,多线程自然是可以比单线程有更高的性能上限,但是在今天的计算环境中,即使是单机多线程的上限也往往不能满足需要了,需要进一步摸索的是多服务器集群化的方案,这些方案中多线程的技术照样是用不上的。
所以单线程、多进程的集群不失为一个时髦的解决方案。
- CPU消耗
采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU。但是如果CPU成为Redis瓶颈,或者不想让服务器其他CUP核闲置,那怎么办?可以考虑多起几个Redis进程,Redis是key-value数据库,不是关系数据库,数据之间没有约束。只要客户端分清哪些key放在哪个Redis进程上就可以了。
什么是多路复用 IO
Redis单线程的优劣势单进程单线程优势代码更清晰,处理逻辑更简单不用去考虑各种锁的问题,不存在加锁释放锁操作,不会因为可能出现死锁而导致的性能消耗,不存在多进程或者多线程导致的切换而消耗CPU
不过在单线程下无法发挥多核CPU性能,于是redis引入多路复用 IO 方案来保证多连接时的系统吞吐量。
其中:
- 多路:指的是多个socket连接,即多个网络连接
- 复用:指的是复用一个线程
多路复用主要有三种技术:select,poll,epoll。redi采用的是epoll。
采用多路 IO 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),且Redis在内存中操作数据的速度非常快(内存内的操作不会成为这里的性能瓶颈),主要以上两点造就了Redis具有很高的吞吐量。
在redis中,所有的接收到的请求,都会由单线程来处理(多路复用 IO),而这个单线程并不会立即处理,而是所有的命令都会进入一个 Socket 队列中,当 socket 可读则交给单线程事件分发器逐个被执行。
redis 6.0 中的多线程
随着硬件性能提升,Redis 的性能瓶颈可能出现网络 IO 的读写,也就是:单个线程处理网络读写的速度跟不上底层网络硬件的速度。
读写网络的read/write
系统调用占用了Redis 执行期间大部分 CPU 时间,瓶颈主要在于网络的 IO 消耗, 优化主要有两个方向:
- 提高网络 IO 性能,典型的实现比如使用 DPDK 来替代内核网络栈的方式。
- 使用多线程充分利用多核,提高网络请求读写的并行度,典型的实现比如 Memcached
在redis中采用的办法是使用多个 IO 线程处理网络请求,提高网络请求处理的并行度。需要注意的是,Redis 多 IO 线程模型只用来处理网络读写请求,对于 Redis 的读写命令,依然是单线程处理。
主要流程:
- 主线程负责接收建立连接请求,获取 socket 放入全局等待读处理队列;
- 主线程通过轮询将可读 socket 分配给 IO 线程;
- 主线程阻塞等待 IO 线程读取 socket 完成;
- 主线程执行 IO 线程读取和解析出来的 Redis 请求命令;
- 主线程阻塞等待 IO 线程将指令执行结果回写回 socket完毕;
- 主线程清空全局队列,等待客户端后续的请求。
在redis 6.0中,IO 多线程默认是关闭的,如需开启需要修改 redis.conf 配置文件:io-threads-do-reads yes
,同时也需要执行线程数,否儿将不生效:io-threads 4
对于线程数的设置,官方有一个建议:4 核的机器建议设置为 2 或 3 个线程,8核的建议设置为 6 个线程,线程数一定要小于机器核数。
线程数并不是越大越好,官方认为超过了 8 个基本就没什么意义了。
redis使用场景
热点数据缓存
缓存是Redis最常见的应用场景,之所有这么使用,主要是因为Redis读写性能优异。而且逐渐有取代memcached,成为首选服务端缓存的组件。而且,Redis内部是支持事务的,在使用时候能有效保证数据的一致性。
作为缓存使用时,一般有两种方式保存数据:
- 读取前,先去读Redis,如果没有数据,读取数据库,将数据拉入Redis。
- 插入数据时,同时写入Redis。
方案一:实施起来简单,但是有两个需要注意的地方:
- 避免缓存击穿。(数据库没有就需要命中的数据,导致Redis一直没有数据,而一直命中数据库。)
- 数据的实时性相对会差一点。
方案二:数据实时性强,但是开发时不便于统一处理。
两种方式根据实际情况来适用。如:方案一适用于对于数据实时性要求不是特别高的场景。方案二适用于字典表、数据量不大的数据存储。
当然除了这两个方案,对于数据变化不大,但又强调响应速度的场景,可以预先将数据写入缓存,然后应用程序仅读取redis,然后仅在需要变更的时候,再对redis进行修改。
限时业务的运用
redis中可以使用expire命令设置一个键的生存时间,到时间后redis会删除它。利用这一特性可以运用在限时的优惠活动信息、手机验证码等业务场景。
延时处理
比如在订单生产后我们占用了库存,10分钟后去检验用户是否真正购买,如果没有购买将该单据设置无效,同时还原库存。 由于redis自2.8.0之后版本提供Keyspace Notifications功能,允许客户订阅Pub/Sub频道,以便以某种方式接收影响Redis数据集的事件。 所以我们对于上面的需求就可以用以下解决方案,我们在订单生产时,设置一个key,同时设置10分钟后过期, 我们在后台实现一个监听器,监听key的实效,监听到key失效时将后续逻辑加上。当然我们也可以利用rabbitmq、activemq等消息中间件的延迟队列服务实现该需求。#
计数器
redis由于incrby命令可以实现原子性的递增,所以可以运用于高并发的秒杀活动、分布式序列号的生成、具体业务还体现在比如限制一个手机号发多少条短信、一个接口一分钟限制多少请求、一个接口一天限制调用多少次等等。
排行榜
关系型数据库在排行榜方面查询速度普遍偏慢,所以可以借助redis的SortedSet进行热点数据的排序。
比如点赞排行榜,做一个SortedSet, 然后以用户的openid作为上面的username, 以用户的点赞数作为上面的score, 然后针对每个用户做一个hash, 通过zrangebyscore就可以按照点赞数获取排行榜,然后再根据username获取用户的hash信息,这个当时在实际运用中性能体验也蛮不错的。
点赞、好友等关系存储
Redis 利用集合的一些命令,比如求交集、并集、差集等。
在微博应用中,每个用户关注的人存在一个集合中,就很容易实现求两个人的共同好友功能。
分布式锁
这个主要利用redis的setnx命令进行,setnx:”set if not exists”就是如果不存在则成功设置缓存同时返回1,否则返回0 ,这个特性在很多后台中都有所运用,因为我们服务器是集群的,定时任务可能在两台机器上都会运行,所以在定时任务中首先 通过setnx设置一个lock, 如果成功设置则执行,如果没有成功设置,则表明该定时任务已执行。
当然结合具体业务,我们可以给这个lock加一个过期时间,比如说30分钟执行一次的定时任务,那么这个过期时间设置为小于30分钟的一个时间就可以,这个与定时任务的周期以及定时任务执行消耗时间相关。
在分布式锁的场景中,主要用在比如秒杀系统等。
redis版本特性
TODO
redis4.0
redis5.0
redis6.0
redis7.0
持久化
redis 提供了两种持久化的方式,分别是RDB(Redis DataBase)和AOF(Append Only File)。
RDB,简而言之,就是在不同的时间点,将 redis 存储的数据生成快照并存储到磁盘等介质上;
AOF,则是换了一个角度来实现持久化,那就是将 redis 执行过的所有写指令记录下来,在下次 redis 重新启动时,只要把这些写指令从前到后再重复执行一遍,就可以实现数据恢复了。
其实 RDB 和 AOF 两种方式也可以同时使用,在这种情况下,如果 redis 重启的话,则会优先采用 AOF 方式来进行数据恢复,这是因为 AOF 方式的数据恢复完整度更高。
如果你没有数据持久化的需求,也完全可以关闭 RDB 和 AOF 方式,这样的话,redis 将变成一个纯内存数据库,就像 memcache 一样。
RDB
RDB 是将redis的某一时刻的数据持久化到磁盘中,是一种快照式的持久化方法。
redis在进行数据持久化的过程中,会先将数据写入到一个临时文件中,待持久化过程完成,才会将此文件替换为最终的持久化文件。
对于 RDB 模式,redis会单独创建一个子进程进行持久化操作,不会有主进程介入,从而保证redis的性能。
如果需要进行大规模的数据存储与恢复,那么 RDB 方式是当仁不让的,不过 RDB 的缺点也较为明显:
- 在redis中,RDB 是默认开启的,默认为每 900 秒内1 次修改、300 秒内 10 此修改、60 秒内 10000次修改进行一次 RDB 备份操作(可以通过
save 900 1
的方式修改频率),当redis故障时,根据备份的时间点,总会可能存在数据的丢失。 - save 频率较高时,频繁写入磁盘,会造成磁盘压力过大,同时多个子进程之间相互竞争服务器 CPU、磁盘资源。
- 虽然 RDB 备份是通过子进程实现,但如果频繁主进程创建子进程进行操作,也会对主进程造成阻塞。
AOF
上面提到 RDB 模式不可避免会存在数据丢失的情况,对于解决这一问题,可以使用 AOF 备份方式,AOF 是将redis执行过程的指令都记录下来,在数据恢复时,按照从前到后的顺序再将指令执行一次。
在redis中,通过修改appendonly yes
配置可打开 AOF 备份。
在redis中,AOF 的持久化策略有三种:
- Everysec,每秒写回,redis默认的配置,每秒执行一次fsync(fsync是指吧缓存中的写指令记录到磁盘中),所以即使redis故障,也只会丢失 1 秒钟的数据。
- Always,同步写回,每次执行命令都需要执行fsync,对redis性能影响较大。
- No,操作系统控制写回,这一方式性能较高,但是宕机时丢失数据可能较多。
由于 AOF 是追加日志的方式,如果不做干预的话,AOF 文件将会越来越大,备份恢复时间也会越来越长,对于这个问题,redis提供了 AOF 文件重写机制,当 AOF 文件的大小超过设定的阈值时,redis 会对AOF文件的内容进行压缩,只保留恢复数据的最小指令集。
AOF 文件压缩示例:用户调用了 100 次 INCR 指令,此时进行压缩后,只会存在一个 SET 指令。
在重写时,如果有新的操作,redis会讲新的操作记录到新的日志文件中,待重写完成再对文件进行合并操作。
混合模式
从redis4.0开始支持混合型持久化模式。在 4.0 时提出创建一个同时包含rdb数据和aof数据的文件(其中rdb数据在前,aof数据在后)用于存储服务器开始执行重写操作时的数据库状态。
混合模式下,内存快照以一定频率进行,然后在两次快照之间,使用aof日志记录两次快照期间所有的命令操作。
在混合模式下,既能享受 RDB 文件快速恢复的好处,也能享受到 AOF 只记录操作命令的简单优势。
在redis4.0时,需要通过aof-use-rdb-preamble yes
配置开启混合模式,但是在redis5.0时,redis已经将混合模式设置为默认的持久化方案。
持久化恢复
对于 RDB 和 AOF 而言,持久化的恢复较为简单。
在加入混合模式后,持久化的恢复,需要同时兼顾 RDB 与 AOF,混合模式的恢复流程如下:
发布订阅
TODO
基于频道
基于模式
事件机制
redis事件包含两部分:
- 文件事件:用于处理 Redis 服务器和客户端之间的网络IO。
- 时间事件:Redis 服务器中的一些操作(比如serverCron函数)需要在给定的时间点执行,而时间事件就是处理这类定时操作的。
事务
redis 事务的本质是一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。
redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。
redis事务相关的命令:
- MULTI :开启事务,redis会将后续的命令逐个放入队列中,然后使用EXEC命令来原子化执行这个命令系列。
- EXEC:执行事务中的所有操作命令。
- DISCARD:取消事务,放弃执行事务块中的所有命令。
- WATCH:监视一个或多个key,如果事务在执行前,这个key(或多个key)被其他命令修改,则事务被中断,不会执行事务中的任何命令。
- UNWATCH:取消WATCH对所有key的监视。
CAS 操作实现乐观锁
在redis中可以使用watch命令实现cas乐观锁,利用watch的特性,首先对某个key进行watch,开启事务修改key,如果此时该key被其他客户端修改,那么当前事务将会无效,在 java中代码如下:
1 | public void delete(String key, String value) { |