Redis

170 阅读8分钟

非关系型数据库之一(NoSQL)


原理/模型

  1. 多个socket
  2. IO多路复用
  3. 文件事件分派器
  4. 事件处理器(Reactor设计模式) alt 模型图

特点

  1. 性能高,能达到10w次读/s,8w次写/s
  2. 具有比较简单的ACID,如:没有事务回滚
  3. 单线程操作,每个操作都是原子操作,没有并发相关问题

数据结构

Key的设计: 短,见名知意。

  1. String:最常用的kv。使用场景:基础缓存,计数器(原子性的自增操作,点赞,访问数之类的)
  2. List:双向链接。使用场景:朋友圈点赞:k用户v存点赞用户资料,回帖,异步队列:rpush生产消息,lpop消费消息
  3. Set:去重数组。使用场景:抽奖池,朋友圈差集并集等。
  4. ZSet:排序的去重数组。使用场景:rank排行榜,延时队列(时间戳作为score,时间戳作为score,消费者用zrangebyscore指令获取N秒之前的数据轮询进行处理)
  5. Hash:无序字典 ,数据结构同样的数组 + 链表二维结构(key,map)。使用场景:保存对象。
  6. 高级结构:HyperLogLog,Geo 位图,Pub/Sub
  7. 布隆过滤器(BloomFilter):

业务场景

  1. 缓存
  2. 排行榜:list,zset
  3. 好友关系(点赞/共同好友)
  4. 简单的消息队列(订阅发布/阻塞队列)
  5. session 服务器
  6. 计数器/限速器(统计播放数据/浏览量/在线人数)
  7. list异步队列
  8. 分布式锁:(自己找资料看)
    • setnx抢锁:如果不存在,则创建并赋值,expire给锁加一个过期时间
    • setnx和expire合成一条指令来用的(客户端有实现)
      • NX:表示只有 key 不存在的时候才会设置成功。(如果此时 redis 中存在这个 key,那么设置失败,返回 nil)不存在才成功很关键.
      • PX 30000:意思是 30s 后锁自动释放。别人创建的时候如果发现已经有了就不能加锁了。
    • RedLock
    • 删除锁的时候,找到 key 对应的 value,跟自己传过去的 value 做比较,如果是一样的才删除
    if redis.call("get",KEYS[1]) == ARGV[1] then
       return redis.call("del",KEYS[1])
    else
       return 0
    end
    

Redis持久化方案

RDB(默认)

【全量】RDB 持久化,是指在指定的时间间隔内将内存中的数据集快照写入磁盘。实际操作过程是,fork 一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。

AOF

【增量】AOF持久化,以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。

Redis 4.0 混合持久化

将 rdb 文件的内容和增量 的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是 自持久化开始到持久化结束 的 这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小。于是在 Redis 重启的时候,可以先加载 rdb 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。 (此时继续持久化的是AOF)

** 对比: ** RDB恢复加载速度大于AOF,非实时持久化,AOF实时持久化,加载慢,体积大。

数据一致性:事务

用Multi(Start Transaction)、Exec(Commit)、Discard(Rollback)实现。 在事务提交前,不会执行任何指令,只会把它们存到一个队列里,不影响其他客户端的操作。在事务提交时,批量执行所有指令。《Redis设计与实现》中的详述。

  • 它仅仅是保证事务里的操作会被连续独占的执行。因为是单线程架构,在执行完事务内所有指令前是不可能再去同时执行其他客户端的请求的。
  • 它没有隔离级别的概念,因为事务提交前任何指令都不会被实际执行,也就不存在”事务内的查询要看到事务里的更新,在事务外查询不能看到”这个让人万分头痛的问题。
  • 它不保证原子性——所有指令同时成功或同时失败,只有决定是否开始执行全部指令的能力,没有执行到一半进行回滚的能力。

过期策略

  • 定期删除:redis默认是每隔 100ms 就随机抽取一些设置了过期时间的key,检查其是否过期,如 果过期就删除。注意这里是随机抽取的。为什么要随机呢?你想一想假如 redis 存了几十万个 key ,每隔100ms就遍历所有的设置过期时间的 key 的话,就会给 CPU 带来很大的负载!
  • 惰性删除 :定期删除可能会导致很多过期 key 到了时间并没有被删除掉。所以就有了惰性删除。 假如你的过期 key,靠定期删除没有被删除掉,还停留在内存里,除非你的系统去查一下那个 key,才会被redis给删除掉。这就是所谓的惰性删除,也是够懒的哈!

redis 内存淘汰机制

  1. volatile-lru:从已设置过期时间的数据集(server.db[i].expires) 中挑选最近最少使用的数据淘汰
  2. olatile-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:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧! ** 4.0版本后新增加以下两种 **
  7. volatile-lfu:从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
  8. allkeys-lfu:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的key

问题(很重要)

1. redis单线程为什么这么快

- 单线程,避免线程之间的竞争
- 是内存中的,使用内存的,可以减少磁盘的io
- 多路 I/O 复用模型, 非阻塞 IO,用了缓冲区的概念,selector模型来进行的

2. 缓存雪崩

** 介绍: ** 缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。
** 解决方案: **

  • 事前:尽量保证整个 redis 集群的高可用性,发现机器宕机尽快补上。选择合适的内存淘汰策略,对缓存的信息加个随机1-5minute time 的过期时间。
  • 事中:本地ehcache缓存 + hystrix限流&降级,避免MySQL崩掉
  • 事后:利用 redis 持久化机制保存的数据尽快恢复缓存

3. 缓存穿透

** 介绍: ** 缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层。
** 解决方案: **

  • 缓存无效key,并设置一个很短的过期时间。这样下次访问,缓存就可以返回值了。
  • 布隆过滤器

4. 缓存击穿

** 介绍: ** 指一个Key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个Key在失效的瞬间,持续的大并发就穿破缓存。
** 解决方案: **

  • 热点数据永不过期
  • 对缓存查询加锁

5. 如何解决 Redis 的并发竞争 Key 问题

** 介绍: ** 所谓 Redis 的并发竞争 Key 的问题也就是多个系统同时对一个 key 进行操作,但是后执行的顺序和我 们期望的顺序不同,这样也就导致了结果的不同!
** 解决方案: **

  • 分布式锁(zookeeper 和 redis 都可以实现分布式锁)。(如果不存在 Redis 的并发竞争 Key 问题,不要使用分布式锁,这样会影响性能)
  • 基于zookeeper临时有序节点可以实现的分布式锁。大致思想为:每个客户端对某个方法加锁时,在 zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的 方式很简单,只需要判断有序节点中序号小的一个。 当释放锁的时候,只需将这个瞬时节点删除即 可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。完成业务流程后,删除对应的 子节点释放锁。
  • 在实践中,当然是从以可靠性为主。所以首推Zookeeper。

部署+运维

** docker,主从同步,从从同步,集群,以及各种运维问题,以后有机会再写。 **




各位大佬勿喷,就是强制自己一周,半个月或一个月写点东西,顺便练习一下markdown语法,不然人都要废了。小声哔哔