Redis从入门到精通,至少要看看这篇!

作者:微信小助手

发布时间:2019-10-14T18:59:09

常用的 SQL 数据库的数据都是存在磁盘中的,虽然在数据库底层也做了对应的缓存来减少数据库的 IO 压力。


图片来自 Pexels


由于数据库的缓存一般是针对查询的内容,而且粒度也比较小,一般只有表中的数据没有发生变动的时候,数据库的缓存才会产生作用。


但这并不能减少业务逻辑对数据库的增删改操作的 IO 压力,因此缓存技术应运而生,该技术实现了对热点数据的高速缓存,可以大大缓解后端数据库的压力。


主流应用架构


客户端在对数据库发起请求时,先到缓存层查看是否有所需的数据,如果缓存层存有客户端所需的数据,则直接从缓存层返回,否则进行穿透查询,对数据库进行查询。


如果在数据库中查询到该数据,则将该数据回写到缓存层,以便下次客户端再次查询能够直接从缓存层获取数据。


缓存中间件 Memcache 和 Redis 的区别


Memcache 的代码层类似 Hash,特点如下:

  • 支持简单数据类型

  • 不支持数据持久化存储

  • 不支持主从

  • 不支持分片


Redis 特点如下:

  • 数据类型丰富

  • 支持数据磁盘持久化存储

  • 支持主从

  • 支持分片


为什么 Redis 能这么快


Redis 的效率很高,官方给出的数据是 100000+QPS,这是因为:
  • Redis 完全基于内存,绝大部分请求是纯粹的内存操作,执行效率高。

  • Redis 使用单进程单线程模型的(K,V数据库,将数据存储在内存中,存取均不会受到硬盘 IO 的限制,因此其执行速度极快。

    另外单线程也能处理高并发请求,还可以避免频繁上下文切换和锁的竞争,如果想要多核运行也可以启动多个实例。

  • 数据结构简单,对数据操作也简单,Redis 不使用表,不会强制用户对各个关系进行关联,不会有复杂的关系限制,其存储结构就是键值对,类似于 HashMap,HashMap 最大的优点就是存取的时间复杂度为 O(1)。

  • Redis 使用多路 I/O 复用模型,为非阻塞 IO。


注:Redis 采用的 I/O 多路复用函数:epoll/kqueue/evport/select。


选用策略:
  • 因地制宜,优先选择时间复杂度为 O(1) 的 I/O 多路复用函数作为底层实现。

  • 由于 Select 要遍历每一个 IO,所以其时间复杂度为 O(n),通常被作为保底方案。

  • 基于 React 设计模式监听 I/O 事件。


Redis 的数据类型


String


最基本的数据类型,其值最大可存储 512M,二进制安全(Redis 的 String 可以包含任何二进制数据,包含 jpg 对象等)。

注:如果重复写入 key 相同的键值对,后写入的会将之前写入的覆盖。


Hash


String 元素组成的字典,适用于存储对象。

List


列表,按照 String 元素插入顺序排序。其顺序为后进先出。由于其具有栈的特性,所以可以实现如“最新消息排行榜”这类的功能。

Set


String 元素组成的无序集合,通过哈希表实现(增删改查时间复杂度为 O(1)),不允许重复。

另外,当我们使用 Smembers 遍历 Set 中的元素时,其顺序也是不确定的,是通过 Hash 运算过后的结果。


Redis 还对集合提供了求交集、并集、差集等操作,可以实现如同共同关注,共同好友等功能。

Sorted Set


通过分数来为集合中的成员进行从小到大的排序。

更高级的 Redis 类型


用于计数的 HyperLogLog、用于支持存储地理位置信息的 Geo。

从海量 Key 里查询出某一个固定前缀的 Key


假设 Redis 中有十亿条 Key,如何从这么多 Key 中找到固定前缀的 Key?


方法 1: 使用 Keys [pattern]:查找所有符合给定模式 Pattern 的 Key


使用 Keys [pattern] 指令可以找到所有符合 Pattern 条件的 Key,但是 Keys 会一次性返回所有符合条件的 Key,所以会造成 Redis 的卡顿。


假设 Redis 此时正在生产环境下,使用该命令就会造成隐患,另外如果一次性返回所有 Key,对内存的消耗在某些条件下也是巨大的。


例:
keys test* //返回所有以test为前缀的key


方法 2: 使用 SCAN cursor [MATCH pattern] [COUNT count]


注:

  • cursor:游标

  • MATCH pattern:查询 Key 的条件

  • Count:返回的条数


SCAN 是一个基于游标的迭代器,需要基于上一次的游标延续之前的迭代过程。


SCAN 以 0 作为游标,开始一次新的迭代,直到命令返回游标 0 完成一次遍历。


此命令并不保证每次执行都返回某个给定数量的元素,甚至会返回 0 个元素,但只要游标不是 0,程序都不会认为 SCAN 命令结束,但是返回的元素数量大概率符合 Count 参数。另外,SCAN 支持模糊查询。

例:

SCAN 0 MATCH test* COUNT 10 //每次返回10条以test为前缀的key


如何通过 Redis 实现分布式锁


分布式锁


分布式锁是控制分布式系统之间共同访问共享资源的一种锁的实现。如果一个系统,或者不同系统的不同主机之间共享某个资源时,往往需要互斥,来排除干扰,满足数据一致性。

分布式锁需要解决的问题如下:
  • 互斥性:任意时刻只有一个客户端获取到锁,不能有两个客户端同时获取到锁。

  • 安全性:锁只能被持有该锁的客户端删除,不能由其他客户端删除。

  • 死锁:获取锁的客户端因为某些原因而宕机继而无法释放锁,其他客户端再也无法获取锁而导致死锁,此时需要有特殊机制来避免死锁。

  • 容错:当各个节点,如某个 Redis 节点宕机的时候,客户端仍然能够获取锁或释放锁。


如何使用 Redis 实现分布式锁


使用 SETNX 实现,SETNX key value:如果 Key 不存在,则创建并赋值。


该命令时间复杂度为 O(1),如果设置成功,则返回 1,否则返回 0。

由于 SETNX 指令操作简单,且是原子性的,所以初期的时候经常被人们作为分布式锁,我们在应用的时候,可以在某个共享资源区之前先使用 SETNX 指令,查看是否设置成功。


如果设置成功则说明前方没有客户端正在访问该资源,如果设置失败则说明有客户端正在访问该资源,那么当前客户端就需要等待。


但是如果真的这么做,就会存在一个问题,因为 SETNX 是长久存在的,所以假设一个客户端正在访问资源,并且上锁,那么当这个客户端结束访问时,该锁依旧存在,后来者也无法成功获取锁,这个该如何解决呢?


由于 SETNX 并不支持传入 EXPIRE 参数,所以我们可以直接使用 EXPIRE 指令来对特定的 Key 来设置过期时间。

用法:

EXPIRE key seconds


程序:

RedisService redisService = SpringUtils.getBean(RedisService.class);
long status = redisService.setnx(key,"1");
if(status == 1){
  redisService.expire(key,expire);
  doOcuppiedWork();
}


这段程序存在的问题:假设程序运行到第二行出现异常,那么程序来不及设置过期时间就结束了,则 Key 会一直存在,等同于锁一直被持有无法释放。


出现此问题的根本原因为:原子性得不到满足。


解决: 从 Redis 2.6.12 版本开始,我们就可以使用 Set 操作,将 SETNX 和 EXPIRE 融合在一起执行,具体做法如下:
  • EX second:设置键的过期时间为 Second 秒。

  • PX millisecond:设置键的过期时间为 MilliSecond 毫秒。

  • NX:只在键不存在时,才对键进行设置操作。

  • XX:只在键已经存在时,才对键进行设置操作。

SET KEY value [EX seconds] [PX milliseconds] [NX|XX]


注:SET 操作成功完成时才会返回 OK,否则返回 nil。


有了 SET 我们就可以在程序中使用类似下面的代码实现分布式锁了:

RedisService redisService = SpringUtils.getBean(RedisService.class);
String result = redisService.set(lockKey,requestId,SET_IF_NOT_EXIST,SET_WITH_EXPIRE_TIME,expireTime);
if("OK.equals(result)"){
  doOcuppiredWork();
}

如何实现异步队列


①使用 Redis 中的 List 作为队列


使用上文所说的 Redis 的数据结构中的 List 作为队列 Rpush 生产消息,LPOP 消费消息。