Redis作为常用的缓存/全局锁等分布式场景的选型,是有很多的考虑和设计,因此今天花比较大的篇幅来梳理下redis中涉及到的一些概念和知识点。
redis基本概念
redis是基于内存的数据库/缓存/消息代理方案,它是非关系型的、基于key-value的。它的高性能、简单性、原子操作能解决分布式系统中的全局锁等问题。
数据类型
支持5种数据类型:
- string(字符串):二进制安全的,可以包含任何数据,包括了jpg图片或者序列化的对象。最大能存储512Mb。
- hash(哈希):键值对集合,每个hash可以存储40多亿键值对。
- list(列表):字符串列表,按照插入顺序排序,可以添加一个元素到列表的头部或尾部。每个列表可以存储40多亿元素。
- set(集合):string类型的无序集合,基于hash实现。每个集合可以存储40多亿元素。
- zset(sorted set,有序集合):不允许重复的集合,每个元素都会关联一个double类型的分数,redis通过分数来为集合中的成员进行从小到大的排序。zset的成员是唯一的,但是分数(score)是可以重复的。
基本命令
- SETNX:SETNX KEY VALUE。意义是set if not exists,当key不存在时,设置value,并返回1;否则,不设置value,返回0。
- GETSET:GETSET KEY VALUE。给key设置value,并返回旧的value。若key不存在,则返回nil,若存在但是不是字符串类型,则返回一个错误。
- GET:GET KEY。返回key对应的value,key若不存在,则返回nil。
- DEL:DEL KEY。删除给定的一个或多个key,不存在的key会被忽略。
- HMSET:HMSET KEY FIELD1 VALUE。意义是hash map set。将field1-value键值对set到key对应的hash map中。
- HMGET:HMGET KEY FIELD1。将field1的value值从key对应的hash map中取出。
- LPUSH:LPUSH KEY VALUE。将value放到key对应的队列的左边。(RPUSH则是右边)
- LRANGE:LRANGE KEY START STOP。按照start、stop位置列出list中的元素。
- SADD:SADD KEY MEMBER。将member添加到key对应的set里。
- SMEMBERS:SMEMBERS KEY。列出set中所有的元素。
- ZADD:ZADD KEY SCORE MEMBER。将member按照score加到key对应的有序集合中。
- ZRANGEBYSCORE:ZRANGEBYSCORE KEY START STOP。按照start、stop位置列出zset中的元素。
- MULTI/EXEC:multi开启事务,exec执行事务。两个命令之间的命令会入队列,并按顺序执行。它们要么都成功、要么都失败。
- INFO MEMORY:查看内存使用情况。会返回包括:used_memory_human,used_memory_peak_human,mem_fragmentation_ratio(内存碎片率)等内存信息。
优势
- 性能极高:redis读的速度是11w次/s,写的速度是8.1w次/s。
- 丰富的数据类型:支持5中数据类型。
- 原子性:支持事务,通过multi/exec指令支持,同时单个操作也具有原子性。
- 丰富的特性:支持publish/subscribe,通知,key过期等特性。
对比memcached
从几下几点进行比较,总体来说redis有更丰富的数据类型,不需要二次封装,高级的功能如持久化,集群管理更友好等优势。
- 网络IO模型:
- memcached是多线程模型
- redis是io多路复用模型
- 数据支持类型:
- memcached使用key-value形式存储和访问数据
- redis支持5种数据类型
- 内存管理机制:
- memcached使用slab allocation,其主要思想是按照预先规定的大小,将分配的内存分割成特定长度的块以存储相应长度的key-value数据记录,以完全解决内存碎片问题。
- redis采用的是包装的mallc/free,相较于Memcached的内存管理方法来说,要简单很多。在Redis中,并不是所有的数据都一直存储在内存中的。这是和Memcached相比一个最大的区别。当物理内存用完时,Redis可以将一些很久没用到的value交换到磁盘。
- 数据存储及持久化:
- memcached不支持持久化,所有的数据都以in-memory形式存储
- redis支持持久化操作,包括了快照方式和AOF方式。前者将某一时刻的所有数据都写入硬盘;后者会在执行写命令的时候将写命令复制到硬盘。
- 数据一致性问题:
- memcached提供了cas命令,可以保重多个并发访问操作同一份数据的一致性
- redis则提供了事务的功能,保重一批命令的原子性
- 集群管理不同:
- memcached本身不支持分布式,因此只能在客户端通过像一致性哈希这样的分布式算法来实现Memcached的分布式存储。
- redis更偏向于在服务器端构建分布式存储。最新版本的Redis已经支持了分布式存储功能。Redis Cluster是一个实现了分布式且允许单点故障的Redis高级版本,它没有中心节点,具有线性可伸缩的功能。
持久化方案
redis共支持两种持久化方式,分别是:
- RDB持久化:使用fork子进程来将快照数据写入临时文件,写入成功之后,再替换原有文件,用二进制压缩存储。
- AOF持久化:以日志的方式记录写/删除操作,追加到日志文件中。
优缺点比较
- RDB:
- 优点:
- 基于文件的备份策略在恢复时可以容易地恢复,对文件可以压缩后复制到合适的存储介质
- 性能最大化,只需要fork一个子进程,让子进程执行RDB,对redis对外提供的读写服务影响非常小
- 相比于AOF,如果数据较大,RDB的启动效率会更高
- 缺点:
- 当数据较大时,可能会导致整个服务器停止服务
- 如果写文件途中出现宕机,则未写入文件丢失,那么数据也会丢失
- 优点:
- AOF:
- 优点:
- 更高的数据持久性。利用2种同步策略:每秒同步、每修改同步来将数据同步到磁盘中
- append模式保证了不会破坏日志文件中已存在的内容,也没有任何磁盘寻址的开销,写入性能非常高,而且文件不容易破损,即使文件尾部破损,也很容易修复。
- 缺点:
- AOF通常大于RDB文件,在恢复大数据集时的速度会比RDB慢
- AOF运行效率往往会慢于RDB
- AOF开启后支持的写QPS会比RDB的写QPS低
- 优点:
- 如何选择:
选择牺牲性能换取更高的缓存一致性-AOF,还是希望有更高的性能-RDB。Redis支持两种方式同时开启,用AOF保证数据不丢失,用RDB做冷备,用来快速恢复。
过期策略
过期策略主要由”定时删除”和”惰性删除”组成。
- 定时删除指的是每100ms就会检查键空间中已过期的键,并将其删除。检查是随机的,而不是遍历所有键,从而将对性能的影响降到最低。
- 惰性删除指的是当一个键被请求的时候,判断其是否已过期,已过期则将其删除。
内存淘汰机制
在redis的配置文件中可以设置maxmemory,一旦内存打到该临界值,则会触发内存淘汰机制。没有配置时,默认为no-eviction,即不删除任何键,但会在写入时报错。其他主要的内存淘汰机制有:
- allkeys-lru:内存不足时,在所有键中移除最近最少使用的key(这个是最常用的)。
- allkeys-random:内存不足时,在所有键中随机移除某个key。
- volatile-lru:内存不足时,在设置了过期时间的键空间中,移除最近最少使用的key。
- volatile-random:内存不足时,在设置了过期时间的键空间中,随机移除某个key。
- volatile-ttl:内存不足时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。
锁的实现
主要是利用setNx命令来完成加锁操作,setNx命令是用来设置某个key的值,它只有在该key不存在时才能设置成功,若存在则不会设置。利用这一个原理,可以做到排他性,获取锁的思路就是:
- 计算重试终止时间
- 生成uuid
- 尝试用set命令带上nx参数对lockName设置uuid,并设置超时时间。set命令具有原子性,因此不会有并发问题
- 同时只会有一个请求能将lockName设置成功,失败则再次重试,直到到达终止时间
相应地,释放锁的思路是:
- 利用del命令删除锁
但是,这种方案中没有考虑死锁的场景。具体场景如下:
- A进程持有锁,但A崩溃,导致锁未释放
- B进程、C进程同时尝试获取锁,但失败。于是获取锁时间戳,同时判定锁超时。
- B、C先后删除、获得锁,导致B、C同时持有锁
在Redis 2.6.12之后,set命令的参数变得更加丰富,因此可以优化上述锁的实现方案。可以直接在set里使用nx参数,同时传入超时时间,那么锁就会在超时后自动释放。而释放时,可以用口令匹配的思路来保证释放的是自己持有的锁。
线程安全
Redis为单进程单线程模式,采用队列的思路将并发访问变为串行访问,利用这些特性,可以避免并发问题。同时,它支持原子性操作,以及通过multi/exce指令支持事务。
IO多路复用
io多路复用(I/O multiplexing)其实是计算机问题中的一个经典问题,它的概念可以参考铁路运输中的”分轨器”–道岔。即利用一个进程–“道岔”,来同时处理不同来向的io请求–“火车”,将请求引导到正确的处理程序–“轨道”上。 它具体指的是在单个进程里,通过记录跟踪每一个Sock(I/O流)的状态来同时管理多个I/O流。 目前主流的多路复用实现有select, poll, epoll, kqueue这几种实现方式。而redis默认采用的是epoll方式。 之所以使用IO多路复用是因为它有如下优势:
- 让单个线程高效的处理多个连接请求,减少多线程切换开销的同时降低了IO时间消耗
- 主要操作都在内存,操作非常快速,也符合redis内存数据库的设计
调优思路
利用诊断工具查看ceres系统指标,从如下角度分析并调优:
- 内存:info memory
- 命令处理数:info stats
- 延迟时间:redis-cli工具加–latency参数来查看,若对于1G带宽延迟高于200微秒,则可能出现问题。可进一步排查:
- 查出慢指令:redis-cli下使用slowlog get查看
- 查看客户端连接数:info clients
- 限制客户端连接数:可以在配置文件中设置,或在redis-cli中设置最大连接数。连接数超过maxclients后,会返回错误消息。此举可以保证redis的性能
- 内存碎片率:info memory中指标mem_fragmentation_ratio
参考:
- Redis锁_分析redis锁的实现原理
-
[程序员如何 Get 分布式锁的正确姿势? 技术头条](https://blog.csdn.net/csdnnews/article/details/84207657) - Redis分布式锁最完美实现Redisson剖析
- runoob redis教程
- 全面对比 Redis 和 Memcached 的 6 点区别
- redis持久化方式对比
- Redis面试题
- 关于redis性能问题分析和优化