夜雪天狼
学习笔记
技术博文
转载备份
心灵鸡汤
redis学习笔记
发布者:caijw
阅读量:42688
发布时间:2015-01-19 14:29:10
# 学习资料 [官方手册](https://redis.io/documentation "官方手册") # 学习笔记 ## 简介 简单来说 redis 就是一个数据库,不过与传统数据库不同的是 redis 的数据是存在内存中的,所以读写速度非常快,因此 redis 被广泛应用于缓存方向。另外,redis 也经常用来做分布式锁。redis 提供了多种数据类型来支持不同的业务场景。除此之外,redis 支持事务 、持久化、LUA脚本、LRU驱动事件、多种集群方案 ## redis 常见数据结构以及使用场景分析 | 数据结构 | 描述 | 常用命令 | | ------------ | ------------ | ------------ | | String | String数据结构是简单的key-value类型,value其实不仅可以是String,也可以是数字。 常规key-value缓存应用; 常规计数:微博数,粉丝数等 | ` set,get,decr,incr,mget` | | Hash | hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值, 比如我们可以 hash 数据结构来存储用户信息,商品信息等等 | ` hget,hset,hgetall` | | List | list 就是链表,Redis list 的应用场景非常多,也是Redis最重要的数据结构之一,比如微博的关注列表,粉丝列表,消息列表等功能都可以用Redis的 list 结构来实现。Redis list 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。另外可以通过 lrange 命令,就是从某个元素开始读取多少个元素,可以基于 list 实现分页查询,这个很棒的一个功能,基于 redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西(一页一页的往下走),性能高 | `lpush,rpush,lpop,rpop,lrange` | | Set | set 对外提供的功能与list类似是一个列表的功能,特殊之处在于 set 是可以自动排重的。当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作 | `sadd,spop,smembers,sunion` | | Sorted Set | 和set相比,sorted set增加了一个权重参数score,使得集合中的元素能够按score进行有序排列 | `zadd,zrange,zrem,zcard` | 详见:[Redis常用数据类型介绍、使用场景及其操作命令](https://www.cnblogs.com/lizhenghn/p/5322887.html "Redis常用数据类型介绍、使用场景及其操作命令") ## redis 设置过期时间 Redis中有个设置时间过期的功能,即对存储在 redis 数据库中的值可以设置一个过期时间。作为一个缓存数据库,这是非常实用的。如项目中的 token 或者一些登录信息,尤其是短信验证码都是有时间限制的,按照传统的数据库处理方式,都是自行判断过期,这样无疑会严重影响项目性能 set key 的时候,都可以给一个 expire time,就是过期时间,通过过期时间可以指定这个 key 可以存活的时间 ## redis删除过期数据的方式 * 定期删除:redis默认是每隔 100ms 就**随机抽取**一些设置了过期时间的key,检查其是否过期,如果过期就删除。注意这里是随机抽取的 * 惰性删除 :定期删除可能会导致很多过期 key 到了时间并没有被删除掉。所以就有了惰性删除。假如你的过期 key,靠定期删除没有被删除掉,还停留在内存里,除非你的系统去查一下那个 key,才会被redis给删除掉。这就是所谓的惰性删除 ## redis 内存淘汰机制 仅通过设置过期时间还是有问题的,如果定期删除漏掉了很多过期 key,而程序也没及时去查,也就无法进行惰性删除,此时可能会大量过期key堆积在内存里,导致redis内存块耗尽了,为了处理这个问题,redis引入了内存淘汰机制 redis 提供 8 种数据淘汰策略: 1. volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰 2. volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰 3. volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰 4. allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的) 5. allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰 6. no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧 7. volatile-lfu:从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰(4.0后新增) 8. allkeys-lfu:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的key(4.0后新增) ## redis 持久化机制 Redis不同于Memcached的很重一点就是,Redis支持持久化,而且支持两种不同的持久化操作 * RDB快照(snapshotting)持久化机制是对 Redis 中的数据执行周期性的持久化 * RDB 的创建与载入 * SAVE:阻塞 Redis 的服务器进程,直到 RDB 文件被创建完毕。SAVE 命令很少被使用,因为其会阻塞主线程来保证快照的写入,由于 Redis 是使用一个主线程来接收所有客户端请求,这样会阻塞所有客户端请求 * BGSAVE:该指令会 Fork 出一个子进程来创建 RDB 文件,不阻塞服务器进程,子进程接收请求并创建 RDB 快照,父进程继续接收客户端的请求 * 优缺点: * 生成多份数据文件,每个数据文件分别都代表了某一时刻Redis里面的数据,适合做冷备份 * 对Redis的性能影响非常小,是因为在同步数据的时候他只是fork了一个子进程去做持久化的,但是如果数据量特别多,会导致客户端暂停服务 * RDB都是快照文件,都是默认五分钟甚至更久的时间才会生成一次,所以可能丢失较长时间段内的数据 * AOF 机制对每条写入命令作为日志,以 append-only 的模式写入一个日志文件中,因为这个模式是只追加的方式,所以没有任何磁盘寻址的开销,所以很快,有点像Mysql中的binlog * 优缺点: * AOF是一秒一次去通过一个后台的线程fsync操作,那最多丢这一秒的数据 * AOF在对日志文件进行操作的时候是以append-only的方式去写的,他只是追加的方式写数据,自然就少了很多磁盘寻址的开销了,写入性能惊人,文件也不容易破损 * 但是一样的数据,AOF文件比RDB还要大 两种方式都可以把Redis内存中的数据持久化到磁盘上,然后再将这些数据备份到别的地方去,RDB更适合做冷备,AOF更适合做热备 > 两种机制全部开启的时候,Redis在重启的时候会默认使用AOF去重新构建数据,因为AOF的数据是比RDB更完整的 Redis 4.0 之后推出了此种持久化方式,RDB 作为全量备份,AOF 作为增量备份,并且将此种方式作为默认方式使用 在上述两种方式中,RDB 方式是将全量数据写入 RDB 文件,这样写入的特点是文件小,恢复快,但无法保存最近一次快照之后的数据,AOF 则将 Redis 指令存入文件中,这样又会造成文件体积大,恢复时间长等弱点 在 RDB-AOF 方式下,持久化策略首先将缓存中数据以 RDB 方式全量写入文件,再将写入后新增的数据以 AOF 的方式追加在 RDB 数据的后面,在下一次做 RDB 持久化的时候将 AOF 的数据重新以 RDB 的形式写入文件 这种方式既可以提高读写和恢复效率,也可以减少文件大小,同时可以保证数据的完整性 在此种策略的持久化过程中,子进程会通过管道从父进程读取增量数据,在以 RDB 格式保存全量数据时,也会通过管道读取数据,同时不会造成管道阻塞 可以说,在此种方式下的持久化文件,前半段是 RDB 格式的全量数据,后半段是 AOF 格式的增量数据。此种方式是目前较为推荐的一种持久化方式 ## redis 事务 Redis 通过 MULTI、EXEC、WATCH 等命令来实现事务(transaction)功能。事务提供了一种将多个命令请求打包,然后一次性、按顺序地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而改去执行其他客户端的命令请求,它会将事务中的所有命令都执行完毕,然后才去处理其他客户端的命令请求 在传统的关系式数据库中,常常用 ACID 性质来检验事务功能的可靠性和安全性 在 Redis 中,事务不具备原子性:如果有一条命令执行失败,其后的命令仍然会被执行,并且当 Redis 运行在某种特定的持久化模式下时,事务才具有持久性 ## 缓存雪崩、缓存击穿和缓存穿透问题解决方案 * 缓存雪崩(缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉) 解决办法: 1. 事前:尽量保证整个 redis 集群的高可用性,发现机器宕机尽快补上。选择合适的内存淘汰策略。 2. 事中:本地ehcache缓存 + hystrix限流&降级,避免MySQL崩掉 3. 事后:利用 redis 持久化机制保存的数据尽快恢复缓存 * 缓存击穿(缓存失效的瞬间,大量请求同时落到数据库上,造成数据库短时间内承受大量请求而崩掉) 解决办法:设置数据永不过期,或者使用互斥锁 * 缓存穿透(一直请求一份缓存不存在,数据库也不存在的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉) 解决办法: 有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。另外也有一个更为简单粗暴的方法,如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟 # 常见问题 ## 为什么要用 redis 而不用本地缓存? 缓存分为本地缓存和分布式缓存。以 Java 为例,使用自带的 map 或者 guava 实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着 jvm 的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性 使用 redis 或 memcached 之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持 redis 或 memcached服务的高可用,整个程序架构上较为复杂 ## redis 的线程模型 redis 内部使用文件事件处理器 `file event handler`,这个文件事件处理器是单线程的,所以 redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket,根据 socket 上的事件来选择对应的事件处理器进行处理 文件事件处理器的结构包含 4 个部分: * 多个 socket * IO 多路复用程序 * 文件事件分派器 * 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器) 多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,会将 socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理 为啥 redis 单线程模型也能效率这么高? * 纯内存操作 * 核心是基于非阻塞的 IO 多路复用机制 * 单线程反而避免了多线程的频繁上下文切换问题 ## redis 和 memcached 的区别 * redis 支持复杂的数据结构 redis 相比 memcached 来说,拥有更多的数据结构,能支持更丰富的数据操作。如果需要缓存能够支持更复杂的结构和操作, redis 会是不错的选择 * 持久化 Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而Memecache把数据全部存在内存之中 * redis 原生支持集群模式 在 redis3.x 版本中,便能支持 cluster 模式,而 memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据 * 性能对比 由于 redis 只使用单核,而 memcached 可以使用多核,所以平均每一个核上 redis 在存储小数据时比 memcached 性能更高。而在 100k 以上的数据中,memcached 性能要高于 redis,虽然 redis 最近也在存储大数据的性能上进行优化,但是比起 memcached,还是稍有逊色 ## 如何解决 Redis 的并发竞争 Key 问题 所谓 Redis 的并发竞争 Key 的问题也就是多个系统同时对一个 key 进行操作,但是最后执行的顺序和我们期望的顺序不同,这样也就导致了结果的不同 推荐一种方案:分布式锁(zookeeper 和 redis 都可以实现分布式锁),(如果不存在 Redis 的并发竞争 Key 问题,不要使用分布式锁,这样会影响性能) 基于zookeeper临时有序节点可以实现的分布式锁。大致思想为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。完成业务流程后,删除对应的子节点释放锁 在实际开发中,是以可靠性为主,首推Zookeeper ## 如何保证缓存与数据库双写时的数据一致性? > 一般情况下我们都是这样使用缓存的:先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。这种方式很明显会存在缓存和数据库的数据不一致的情况 只要用缓存,就可能会涉及到缓存与数据库双存储双写,只要是双写,就一定会有数据一致性的问题,那么如何解决一致性问题? 一般来说,如果系统不是严格要求缓存+数据库必须一致性的话,缓存可以稍微的跟数据库偶尔有不一致的情况,最好不要做这个方案,读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况 串行化之后,就会导致系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求 ## 从海量 Key 里查询出某一个固定前缀的 Key * 使用 keys \[pattern\] 查找符合给定模式的key,但是由于redis是单线程的,使用这个命令会堵塞redis,导致redis服务崩溃 * 使用 scan 这个命令不会阻塞redis ## 如何通过 Redis 实现分布式锁 使用 SETNX 实现,SETNX key value:如果 Key 不存在,则创建并赋值,在使用EXPIRE设置过期时间 为了避免中间出现问题导致锁不会过期,Redis 2.6.12 版本开始可以使用set将 SETNX 和 EXPIRE 融合在一起执行 -separator-