Redis学习总结
参考:
一、概述
二、Redis 常见数据结构以及使用场景分析
你可以自己本机安装 redis 或者通过 redis 官网提供的在线 redis 环境。
字符串string:
- 介绍 :string 数据结构是简单的 key-value 类型。虽然 Redis 是用 C 语言写的,但是 Redis 并没有使用 C 的字符串表示,而是自己构建了一种 简单动态字符串(simple dynamic string,SDS)。相比于 C 的原生字符串,Redis 的 SDS 不光可以保存文本数据还可以保存二进制数据,并且获取字符串长度复杂度为 O(1)(C 字符串为 O(N)),除此之外,Redis 的 SDS API 是安全的,不会造成缓冲区溢出。
- 常用命令: set,get,strlen,exists,dect,incr,setex 等等。
- 应用场景 :一般常用在需要计数的场景(字符串的内容为整数的时候可以使用),比如用户的访问次数、热点文章的点赞转发数量等等。
set test:count 1 incr test:count decr test:count
列表 list
- 介绍 :list 即是 链表。链表是一种非常常见的数据结构,特点是易于数据元素的插入和删除并且且可以灵活调整链表长度,但是链表的随机访问困难。许多高级编程语言都内置了链表的实现比如 Java 中的 LinkedList,但是 C 语言并没有实现链表,所以 Redis 实现了自己的链表数据结构。Redis 的 list 的实现为一个 双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。
- 常用命令: rpush,lpop,lpush,rpop,lrange、llen 等。
- 应用场景: 发布与订阅或者说消息队列、慢查询。
lpush test:ids 101 102 103 // 每次把数据从列表的左边压入 lindex test:ids 0; // 103 lrange test:ids 0 -1 // 列出所有元素 103 102 101 rpop test:ids // 101 lpop test:ids // 103 rpush test:ids 101 102 103 // 每次把数据从列表的右边压入 lindex test:ids 0; // 101 lrange test:ids 0 -1 // 列出所有元素 101 102 103 rpop test:ids // 103 lpop test:ids // 101
Hash: 每个键对应的值是一个HashMap
- 介绍 :hash 类似于 JDK1.8 前的 HashMap,内部实现也差不多(数组 + 链表)。不过,Redis 的 hash 做了更多优化。另外,hash 是一个 string 类型的 field 和 value 的映射表,特别适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。 比如我们可以 hash 数据结构来存储用户信息,商品信息等等。
- 常用命令: hset,hmset,hexists,hget,hgetall,hkeys,hvals 等。
- 应用场景: 系统中对象数据的存储
hset test:user username zhangsan hset test:user id 1 hget test:user id
无序集合set
- 介绍 : set 类似于 Java 中的 HashSet 。Redis 中的 set 类型是一种无序集合,集合中的元素没有先后顺序。当你需要存储一个列表数据,又不希望出现重复数据时,set 是一个很好的选择,并且 set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是 list 所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作。比如:你可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis 可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程。
- 常用命令: sadd,spop,smembers,sismember,scard,sinterstore,sunion 等。
- 应用场景: 需要存放的数据不能重复以及需要获取多个数据源交集和并集等场景
sadd test:teacher aaa bbb bbb ccc
scard test:teacher // 统计大小 3
smembers test:teacher //列出所有成员 "aaa" "bbb" "ccc"
有序集合zset (sorted set)
- 介绍: 和 set 相比,sorted set 增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列,还可以通过 score 的范围来获取元素的列表。有点像是 Java 中 HashMap 和 TreeSet 的结合体。
- 常用命令: zadd,zcard,zscore,zrange,zrevrange,zrem 等。
- 应用场景: 需要对数据根据某个权重进行排序的场景。比如在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息。
zadd test:student 10 aaa 20 bbb 30 ccc 40 ccc zcard test:student //元素个数 3 zrange test:student 0 -1 //列出所有元素(按分值的从小到大顺序列出) "aaa" "bbb" "ccc" zscore test:student ccc //查找某个key的分值 40 zrank test:student ccc // 按分值的从小到大排名 2
Redis 其他常用命令
keys * // 列出所有key keys test:* // 列出所有以test:开头的key type test:user // 打印test:user这个key的数据类型 hash/string/list/set/zset exists test:user // 判断当前数据库是否存在test:user这个key del test:user // 删除这个key expire test:teacher 10 // 把test:teacher标记为10s后过期
跳跃表
查找:
插入:
与红黑树等平衡树相比,跳跃表具有以下优点:
三、分布式缓存常见的技术选型方案有哪些?
分布式缓存的话,使用的比较多的主要是 Memcached 和 Redis。
过去分布式缓存最开始兴起的那会Memcached 比较常用,不过已经慢慢地被强大的Redis取代了。
分布式缓存主要解决的是单机缓存的容量受服务器限制并且无法保存通用的信息。部署了同一服务的多台机器的本地缓存之间是无法数据共享的。
四、说一下 Redis 和 Memcached 的区别和共同点
随着 Redis 越来越强大,现在公司一般都是用 Redis 来实现缓存!不过,了解 Redis 和 Memcached 的区别和共同点,有助于我们在做相应的技术选型的时候,能够做到有理有据!
共同点 :
- 都是基于内存的数据库,一般都用来当做缓存使用。
- 都有过期策略。
- 两者的性能都非常高。
区别 :
- Redis 支持更丰富的数据类型(支持更复杂的应用场景)。Redis 不仅仅支持简单的 k/v 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储。Memcached 只支持最简单的 k/v 数据类型。
- Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而 Memecache 把数据全部存在内存之中。
- Redis 有灾难恢复机制。 因为可以把缓存中的数据持久化到磁盘上。
- Redis 在服务器内存使用完之后,可以将不用的数据放到磁盘上。但是,Memcached 在服务器内存使用完之后,就会直接报异常。
- Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 Redis 目前是原生支持 cluster 模式的.
- Memcached 是多线程,非阻塞 IO 复用的网络模型;Redis 使用单线程的多路 IO 复用模型。 (Redis 6.0 引入了多线程 IO )
- Redis 支持发布订阅模型、Lua 脚本、事务等功能,而 Memcached 不支持。并且,Redis 支持更多的编程语言。
- Memcached过期数据的删除策略只用了惰性删除,而 Redis 同时使用了惰性删除与定期删除。
五、为什么要用 Redis/为什么要用缓存?
高性能 :
如果命中缓存,可以直接在内存中取出数据返回响应,不需要和数据库交互,效率高。降低了服务的请求响应延迟,提升用户体验。
不过,要保持数据库和缓存中的数据的一致性。 如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!
高并发:
一般像 MySQL 这类的数据库的 每秒大概能执行1w条简单SQL ,太多的请求就会把数据库压死,导致数据库宕机。但是加上缓存后,大部分请求都会命中缓存然后直接返回,绝大部分流量都不会打到数据库中,这样极大的降低了数据库的压力。这样也就提高的系统整体的并发能力。
六、Redis 单线程模型详解
Redis 基于 Reactor 模式开发了自己的网络事件处理器:这个处理器被称为文件事件处理器(file event handler)。文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字,并根据 套接字目前执行的任务来为套接字关联不同的事件处理器。
当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关 闭(close)等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。
虽然文件事件处理器以单线程方式运行,但通过使用 I/O 多路复用程序来监听多个套接字,文件事件处理器既实现了高性能的网络通信模型,又可以很好地与 Redis 服务器中其他同样以单线程方式运行的模块进行对接,这保持了 Redis 内部单线程设计的简单性。
可以看出,文件事件处理器(file event handler)主要是包含 4 个部分:
- 多个 socket(客户端连接)
- IO 多路复用程序(支持多个客户端连接的关键)
- 文件事件分派器(将 socket 关联到相应的事件处理器)
- 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
七、Redis 没有使用多线程?为什么不使用多线程?
虽然说 Redis 是单线程模型,但是, 实际上,Redis 在 4.0 之后的版本中就已经加入了对多线程的支持。
不过,Redis 4.0 增加的多线程主要是针对一些大键值对的删除操作的命令,使用这些命令就会使用主线程之外的其他线程来“异步处理”。
大体上来说,Redis 6.0 之前主要还是单线程处理。
那Redis6.0 之前 为什么不使用多线程?
- 单线程编程容易并且更容易维护;
- Redis 的性能瓶颈不在 CPU ,主要在内存和网络;
- 多线程就会存在死锁、线程上下文切换等问题,甚至会影响性能。
八、Redis6.0 之后为何引入了多线程?
Redis6.0 引入多线程主要是为了提高网络 IO 读写性能,因为这个算是 Redis 中的一个性能瓶颈(Redis 的瓶颈主要受限于内存和网络)。
虽然,Redis6.0 引入了多线程,但是 Redis 的多线程只是在网络数据的读写这类耗时操作上使用了, 执行命令仍然是单线程顺序执行。因此,你也不需要担心线程安全问题。
Redis6.0 的多线程默认是禁用的,只使用主线程。如需开启需要修改 redis 配置文件 redis.conf :
io-threads-do-reads yes
开启多线程后,还需要设置线程数,否则是不生效的。同样需要修改 redis 配置文件 redis.conf :
io-threads 4 #官网建议4核的机器建议设置为2或3个线程,8核的建议设置为6个线程
推荐阅读:
九、Redis 给缓存数据设置过期时间有啥用?
一般情况下,我们设置保存的缓存数据的时候都会设置一个过期时间。为什么呢?
因为内存是有限的,如果缓存中的所有数据都是一直保存的话,分分钟直接Out of memory。
Redis 自带了给缓存数据设置过期时间的功能,比如:
127.0.0.1:6379> exp key 60 # 数据在 60s 后过期 (integer) 1 127.0.0.1:6379> setex key 60 value # 数据在 60s 后过期 (setex:[set] + [ex]pire) OK 127.0.0.1:6379> ttl key # 查看数据还有多久过期 (integer) 56
注意:Redis中除了字符串类型有自己独有设置过期时间的命令 setex 外,其他方法都需要依靠 expire 命令来设置过期时间 。另外, persist 命令可以移除一个键的过期时间:
过期时间除了有助于缓解内存的消耗,还有什么其他用么?
很多时候,我们的业务场景就是需要某个数据只在某一时间段内存在,比如我们的短信验证码可能只在1分钟内有效,用户登录的 token 可能只在 1 天内有效。
如果使用传统的数据库来处理的话,一般都是自己判断过期,这样更麻烦并且性能要差很多。
十、Redis是如何判断数据是否过期的呢?
Redis 通过一个叫做过期字典(可以看作是hash表)来保存数据过期的时间。过期字典的键指向Redis数据库中的某个key(键),过期字典的值是一个long long类型的整数,这个整数保存了key所指向的数据库键的过期时间(毫秒精度的UNIX时间戳)。
过期字典是存储在redisDb这个结构里的:
typedef struct redisDb { ... dict *dict; //数据库键空间,保存着数据库中所有键值对 dict *expires // 过期字典,保存着键的过期时间 ... } redisDb;
十一、过期的数据的删除策略了解么?
Redis会把设置了过期时间的key放入一个独立的字典里,在key过期时并不会立刻删除它。Redis会通过如下两种策略,来删除过期的key(重要!自己造缓存轮子的时候需要格外考虑的东西):
- 惰性删除 :客户端访问某个key时, Redis会检查该key是否过期,若过期则删除。只会在取出key的时候才对数据进行过期检查。这样对CPU很好,只消耗很少的 CPU 资源,但是可能会造成太多过期 key 没有被删除。
- 定期删除 : 每隔一段时间抽取一批 key 执行删除过期key操作。Redis默认每秒执行10次过期扫描(配置hz选项),扫描策略如下:
- 从过期字典中随机选择20个key;
- 删除这20个key中已过期的key;
- 如果过期的key的比例超过25%,则重复步骤1;
定期删除对内存更加友好,可以及时清理过期的key, 给内存腾空间,但是定时任务会消耗cpu资源,惰性删除对CPU更加友好,只消耗很少的 CPU 资源。两者各有千秋,所以Redis 采用的是 定期删除+惰性/懒汉式删除 。
但是,仅仅通过给 key 设置过期时间还是有问题的。因为还是可能存在定期删除和惰性删除漏掉了很多过期 key 的情况。这样就导致大量过期 key 堆积在内存里,然后就Out of memory了。
怎么解决这个问题呢?答案就是: Redis 内存淘汰机制。
十二、Redis 内存淘汰机制了解么?
相关问题:MySQL 里有 2000w 数据,Redis 中只存 20w 的数据,如何保证 Redis 中的数据都是热点数据?
当Redis占用内存超出最大限制(maxmemory)时,可采用如下策略(maxmemory-policy) , 让Redis淘汰一些数据,以腾出空间继续提供读写服务:
- noeviction:对可能导致增大内存的命令返回错误(大多数写命令,DEL除外);
- volatile-ttl: 在设置了过期时间的key中,选择剩余寿命(TTI)最短的key,将其淘汰;
- volatile-lru:在设置了过期时间的key中,选择最少使用的key(LRU),将其淘汰;
- volatile-random:在设置了过期时间的key中,随机选择一些key,将其淘汰;
- allkeys-lru:在所有的key中,选择最少使用的key (LRU),将其淘汰;
- allkeys-random:在所有的key中,随机选择一些key,将其淘汰;
4.0 版本后增加以下两种:
- volatile-lfu(least frequently used):在设置了过期时间的key中,选择使用频率最低的一些key, 进行淘汰。
- allkeys-lfu(least frequently used):在所有的key中,选择使用频率最低的一些key, 进行淘汰。
十三、持久化
RDB(Redis DataBase) 持久化
AOF 持久化
- always 选项会严重减低服务器的性能;
- everysec 选项比较合适,可以保证系统崩溃时只会丢失一秒左右的数据,并且 Redis 每秒执行一次同步对服务器性能几乎没有任何影响;
- no 选项并不能给服务器性能带来多大的提升,而且也会增加系统崩溃时数据丢失的数量。
拓展:Redis 4.0 对于持久化机制的优化
Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 aof-use-rdb-preamble 开启)。
如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的 RDB 部分是压缩格式不再是 AOF 格式,可读性较差。
AOF 重写
- AOF 持久化是通过保存被执行的写命令来记录数据库状态的,所以AOF文件的大小随着时间的流逝一定会越来越大;影响包括但不限于:对于Redis服务器,计算机的存储压力;AOF还原出数据库状态的时间增加;
- 为了解决AOF文件体积膨胀的问题,Redis提供了AOF重写功能:Redis服务器可以创建一个新的AOF文件来替代现有的AOF文件,新旧两个文件所保存的数据库状态是相同的,但是新的AOF文件不会包含任何浪费空间的冗余命令,通常体积会较旧AOF文件小很多。
十四、缓存穿透
解决方案
1.缓存空对象存储层未命中后,仍然将空值存入缓存层。再次访问该数据时,缓存层会直接返回空值。
2.布隆过滤器
将所有存在的key提前存入布隆过滤器,在访问缓存层之前,先通过过滤器拦截,若请求的是不存在的key,则直接返回空值。
十五、缓存雪崩
解决方案:
十六、缓存击穿
场景
解决方案
十七、如何保证缓存和数据库数据的一致性?
Cache Aside Pattern(旁路缓存模式)
- 读:从 cache 中读取数据,读取到就直接返回,读取不到的话,就从 DB 中取数据返回,然后再把数据放到 cache 中。
- 写:更新 DB,然后直接删除 cache 。
十八、Redis 事务
// 编程式事务 @Test public void testTransaction(){ Object result = redisTemplate.execute(new SessionCallback() { @Override public Object execute(RedisOperations redisOperations) throws DataAccessException { String redisKey = "test:tx"; // 启用事务 redisOperations.multi(); redisOperations.opsForSet().add(redisKey, "zhangsan"); redisOperations.opsForSet().add(redisKey, "lisi"); redisOperations.opsForSet().add(redisKey, "wangwu"); System.out.println(redisOperations.opsForSet().members(redisKey)); // [] // 提交事务 return redisOperations.exec(); } }); System.out.println(result); // [1, 1, 1, [wangwu, lisi, zhangsan]] }
十九、Redis有哪些优缺点
优点
- 读写性能优异, Redis读的速度是110000次/s,写的速度是81000次/s。
- 支持数据持久化,支持AOF和RDB两种持久化方式。
- 支持事务,Redis的所有操作都是原子性的,同时Redis还支持对几个操作合并后的原子性执行。
- 数据结构丰富,除了支持string类型的value外还支持hash、set、zset、list等数据结构。
- 支持主从复制,主机会自动将数据同步到从机,可以进行读写分离。
缺点
- 数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。
- Redis 不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的IP才能恢复。
- 主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性。
- Redis 较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。为避免这一问题,运维人员在系统上线时必须确保有足够的空间,这对资源造成了很大的浪费。