Redis学习专栏(基础知识):Redis数据结构和常用实现

一. Redis数据结构

String

最基本的数据类型,其值最大可存储512M,二进制安全(Redis的String可以包含任何二进制数据,包含jpg对象等)。
Redis学习专栏(基础知识):Redis数据结构和常用实现_第1张图片
注:如果重复写入key相同的键值对,后写入的会将之前写入的覆盖。

Hash

String元素组成的字典,适用于存储对象。
Redis学习专栏(基础知识):Redis数据结构和常用实现_第2张图片

List

列表,按照String元素插入顺序排序。其顺序为后进先出。由于其具有栈的特性,所以可以实现如“最新消息排行榜”这类的功能。
Redis学习专栏(基础知识):Redis数据结构和常用实现_第3张图片

Set

String元素组成的无序集合,通过哈希表实现(增删改查时间复杂度为O(1)),不允许重复。
Redis学习专栏(基础知识):Redis数据结构和常用实现_第4张图片
另外,当我们使用smembers遍历set中的元素时,其顺序也是不确定的,是通过hash运算过后的结果。Redis还对集合提供了求交集、并集、差集等操作,可以实现如同共同关注,共同好友等功能。

Sorted Set

通过分数来为集合中的成员进行从小到大的排序。
Redis学习专栏(基础知识):Redis数据结构和常用实现_第5张图片

更高级的Redis类型

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

二. Keys命令

KEYS [pattern]:查找所有符合给定模式pattern的key

使用 keys [pattern] 指令可以找到所有符合pattern条件的key,但是keys会一次性返回所有符合条件的key,所以会造成redis的卡顿,假设redis此时正在生产环境下,使用该命令就会造成隐患,另外如果一次性返回所有key,对内存的消耗在某些条件下也是巨大的。

例:

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

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常用实现

1.Redis实现分布式锁

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

分布式锁需要解决的问题如下:

互斥性:任意时刻只有一个客户端获取到锁,不能有两个客户端同时获取到锁。

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

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

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

使用SETNX实现

SETNX key value:如果key不存在,则创建并赋值。该命令时间复杂度为O(1),如果设置成功,则返回1,否则返回0。
Redis学习专栏(基础知识):Redis数据结构和常用实现_第6张图片
由于SETNX指令操作简单,且是原子性的,所以初期的时候经常被人们作为分布式锁,我们在应用的时候,可以在某个共享资源区之前先使用SETNX指令,查看是否设置成功,如果设置成功则说明前方没有客户端正在访问该资源,如果设置失败则说明有客户端正在访问该资源,那么当前客户端就需要等待。

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

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

用法:EXPIRE key seconds

Redis学习专栏(基础知识):Redis数据结构和常用实现_第7张图片
程序:

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

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

解决:从Redis2.6.12版本开始,我们就可以使用Set操作,将Setnx和expire融合在一起执行,具体做法如下。

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

EX second:设置键的过期时间为second秒。

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

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();
}

2. 实现异步队列

  • 使用Redis中的List作为队列

使用上文所说的Redis的数据结构中的List作为队列 Rpush生产消息,LPOP消费消息。
Redis学习专栏(基础知识):Redis数据结构和常用实现_第8张图片
此时我们可以看到,该队列是使用rpush生产队列,使用lpop消费队列。在这个生产者-消费者队列里,当lpop没有消息时,证明该队列中没有元素,并且生产者还没有来得及生产新的数据

缺点:lpop不会等待队列中有值之后再消费,而是直接进行消费。

弥补:可以通过在应用层引入Sleep机制去调用LPOP重试。

  • 使用BLPOP key [key…] timeout

BLPOP key [key …] timeout:阻塞直到队列有消息或者超时。
Redis学习专栏(基础知识):Redis数据结构和常用实现_第9张图片
Redis学习专栏(基础知识):Redis数据结构和常用实现_第10张图片
Redis学习专栏(基础知识):Redis数据结构和常用实现_第11张图片
缺点:按照此种方法,我们生产后的数据只能提供给各个单一消费者消费,能否实现生产一次就能让多个消费者消费呢?

  • pub/sub:主题订阅者模式

发送者(pub)发送消息,订阅者(sub)接收消息。

订阅者可以订阅任意数量的频道
Redis学习专栏(基础知识):Redis数据结构和常用实现_第12张图片
pub/sub模式的缺点:

消息的发布是无状态的,无法保证可达。对于发布者来说,消息是“即发即失”的,此时如果某个消费者在生产者发布消息时下线,重新上线之后,是无法接收该消息的,要解决该问题需要使用专业的消息队列,如kafka…此处不再赘述。

你可能感兴趣的:(redis)