深入探索Redis:高性能键值存储数据库

系列文章目录



前言

Redis(Remote Dictionary Server)是一个开源的,内存中的数据结构存储系统,它具有高性能和灵活性,被广泛应用于缓存、消息队列、实时统计分析等场景。本篇博客将深入探索Redis的特性、用途和优势,帮助读者更好地理解和利用这一强大的键值存储数据库。


一、Redis简介

Redis(Remote Dictionary Server),是一个由 C语言 编写的,基于内存,支持网络的键值对存储模型的数据结构存储系统。

1.1 为什么需要Redis

1.1.1 高性能

假设这么个场景,你有个操作,一个请求过来,吭哧吭哧你各种乱七八糟操作 mysql,半天查出来一个结果,耗时 600ms。但是这个结果可能接下来几个小时都不会变了,或者变了也可以不用立即反馈给用户。那么此时咋办?

缓存啊,折腾 600ms 查出来的结果,扔缓存里,一个 key 对应一个 value,下次再有人查,别走 mysql 折腾 600ms 了,直接从缓存里,通过一个 key 查出来一个 value,2ms 搞定。性能提升 300 倍

就是说对于一些需要复杂操作耗时查出来的结果,且确定后面不怎么变化,但是有很多读请求,那么直接将查询出来的结果放在缓存中,后面直接读缓存就好。

Redis的数据存储在内存中,因此具有极快的读写速度。它使用高效的数据结构和算法,可以在微秒级别处理大量请求,使其成为处理实时数据的理想选择。

1.1.2 高并发

MySQL 这么重的数据库,压根儿设计不是让你玩儿高并发的,虽然也可以玩儿,但是天然支持不好。MySQL 单机支撑到 2000QPS 也开始容易报警了。

所以要是你有个系统,高峰期一秒钟过来的请求有 1 万,那一个 mysql 单机绝对会死掉。你这个时候就只能上缓存,把很多数据放缓存,别放 mysql。缓存功能简单,说白了就是 key-value 式操作,单机支撑的并发量轻松一秒几万十几万,支撑高并发 so easy。Redis单机承载并发量是 MySQL 单机的几十倍

缓存是走内存的,内存天然就支撑高并发。

Redis提供了一些分布式系统的特性,如主从复制、分片和高可用性。这些功能使得Redis可以方便地扩展,处理大规模的数据和并发请求。

1.2 Redis的应用场景

  • 缓存层:Redis最常见的应用场景之一是作为高性能缓存层来加快数据访问速度。将频繁读取的数据存储在Redis中,可以避免频繁查询数据库,减轻数据库的负载,提高整体应用程序的性能。提高应用程序的响应速度和性能。
  • 会话存储:Redis可以用作会话存储(session),将用户的登录状态、权限信息和其他会话数据存储在内存中。这样可以快速验证用户身份,并支持跨多个应用服务器的会话共享-分布式解决方案之一
  • 消息队列:Redis的发布/订阅功能和列表数据结构经常用于构建消息队列系统。它可以用于实现异步任务和事件驱动的开发模式,提供高吞吐量和可靠的消息传递。
  • 计数器和统计数据:Redis的原子操作和计数器功能可用于实时统计和数据分析。它可以用于跟踪网站的访问量用户行为计数排行榜等数据,提供实时更新和查询功能。
  • 分布式锁:Redis提供的原子操作可以用于构建分布式锁系统,实现在分布式环境中协调和同步访问资源的能力。它可以确保只有一个进程或线程能够访问临界资源,避免并发冲突和数据不一致问题。
  • 地理位置服务:Redis的有序集合数据结构可以用于存储地理位置信息,并支持空间查询和最近邻搜索。这使得Redis在构建地理位置服务应用(如附近的人、商家和地点推荐)时非常有用。

除了上述场景,Redis还可以用于任务队列、缓存预热、限流和速率控制、实时排名和计算等许多其他应用中。其灵活性、高性能和丰富的数据结构使得Redis成为了广泛应用于不同领域的强大工具。

二、Redis 下载安装

2.1 window 系统使用Redis

Redis在Windows上不受官方支持,所以官网没有 windows 版本可以下载。微软团队维护了开源的 windows 版本。

下载:redis-window

推荐下载zip包。

在这里插入图片描述
解压后,分别启动Redis 服务端(resdis-server)与 Redis 客户端(redis-cli)即可使用。
在这里插入图片描述

2.2 Linux 系统使用Redis

请先熟悉Linux系统常用命令,参考:

2.2.1 部署docker

参考:

2.2.2 拉取镜像

docker pull redis

2.2.3 创建挂载路径

挂载:即将宿主的文件和容器内部目录相关联,相互绑定,在宿主机内修改文件的话也随之修改容器内部文件

//自行选择存放目录 一般为/home 或者定义/data 
mkdir -p /data/redis/data
mkdir -p /data/redis/conf

/data/redis/conf目录,新建redis配置文件

vi /data/redis/conf/redis.conf

写入后保存

# bind 192.168.1.100 10.0.0.1
# bind 127.0.0.1 ::1
#bind 127.0.0.1

protected-mode no
port 6379
tcp-backlog 511
requirepass 000415
timeout 0
tcp-keepalive 300
daemonize no
supervised no
pidfile /var/run/redis_6379.pid
loglevel notice
logfile ""
databases 30
always-show-logo yes
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb
dir ./
replica-serve-stale-data yes
replica-read-only yes
repl-diskless-sync no
repl-disable-tcp-nodelay no
replica-priority 100
lazyfree-lazy-eviction no
lazyfree-lazy-expire no
lazyfree-lazy-server-del no
replica-lazy-flush no
appendonly yes
appendfilename "appendonly.aof"
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-load-truncated yes
aof-use-rdb-preamble yes
lua-time-limit 5000
slowlog-max-len 128
notify-keyspace-events ""
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
list-max-ziplist-size -2
list-compress-depth 0
set-max-intset-entries 512
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
hll-sparse-max-bytes 3000
stream-node-max-bytes 4096
stream-node-max-entries 100
activerehashing yes
hz 10
dynamic-hz yes
aof-rewrite-incremental-fsync yes
rdb-save-incremental-fsync yes

2.2.4 启动docker 容器

docker run --name=redis --volume=/data/redis/conf/redis.conf:/usr/local/etc/redis/redis.conf --volume=/data/redis/data:/data --volume=/data --workdir=/data -p 6379:6379 --restart=no --detach=true redis redis-server /usr/local/etc/redis/redis.conf --appendonly yes
#检查镜像是否正常运行
docker ps
 
#进入容器
docker exec -it mysql bash
 
#用默认密码登陆账号
auth "密码"

三、Redis 数据结构与使用

Redis支持多种数据结构,并提供相应的命令用于对这些数据结构进行操作。以下是常见的Redis数据结构及其对应的常用命令:

3.1 字符串(String)

存储单个值,例如文本、整数或二进制数据。
常用命令:SET、GET、INCR、DECR、APPEND等。

SET key value:设置指定键的值。
GET key:获取指定键的值。
INCR key:将指定键的值增加1。
DECR key:将指定键的值减少1。
APPEND key value:在指定键的值后追加字符串。

3.2 哈希表(Hash)

存储键值对的集合,用于存储对象的字段和值。
常用命令:HSET、HGET、HGETALL、HDEL等。

HSET key field value:设置指定键的哈希表字段值。
HGET key field:获取指定键的哈希表字段值。
HGETALL key:获取指定键的所有哈希表字段和值。
HDEL key field:删除指定键的哈希表字段。

3.3 列表(List)

有序的字符串元素集合,可在开头或末尾进行插入和删除操作。
常用命令:LPUSH、RPUSH、LPOP、RPOP、LRANGE等。

LPUSH key value [value ...]:将一个或多个值插入到列表的左侧。
RPUSH key value [value ...]:将一个或多个值插入到列表的右侧。
LPOP key:移除并返回列表左侧的元素。
RPOP key:移除并返回列表右侧的元素。
LRANGE key start stop:获取列表指定范围内的元素。

3.4 集合(Set)

无序的唯一字符串集合,不允许重复元素。
常用命令:SADD、SMEMBERS、SISMEMBER、SREM等。

SADD key member [member ...]:向集合添加一个或多个成员。
SMEMBERS key:获取集合中的所有成员。
SISMEMBER key member:判断指定成员是否在集合中。
SREM key member [member ...]:从集合中移除一个或多个成员。

3.5 有序集合(Sorted Set)

类似于集合,但每个成员都与一个分数相关联,可按照分数进行排序。
常用命令:ZADD、ZRANGE、ZSCORE、ZREM等。

ZADD key score member [score member ...]:向有序集合添加一个或多个成员,并指定分数。
ZRANGE key start stop [WITHSCORES]:按分数范围获取有序集合的成员。
ZSCORE key member:获取有序集合中指定成员的分数。
ZREM key member [member ...]:从有序集合中移除一个或多个成员。

3.6 位图(Bitmap)

用于处理位级数据,可以进行位切换和位计数等操作。
常用命令:SETBIT、GETBIT、BITCOUNT、BITOP等。

3.7 超级日志(HyperLogLog)

用于进行近似基数统计的数据结构,可以估计唯一元素的数量。
常用命令:PFADD、PFCOUNT、PFMERGE等。
地理空间索引(Geospatial Index)

3.8 存储地理位置数据

进行附近位置搜索和计算距离等操作。
常用命令:GEOADD、GEORADIUS、GEODIST等。

完整请参考:redis 官方文档

四、Redis 底层实现机制

以下知识涉及大量c语言的基础知识,阅读前有c语言技能储备更佳。

Redis底层的实现机制主要包括以下几个方面:

  • 内存存储:Redis将数据存储在内存中,这使得它能够提供非常高的读写性能。Redis通过使用自己实现的内存分配器来管理内存,并且使用简单的数据结构来存储数据。
  • 单线程模型:Redis采用单线程模型来处理客户端请求。这样可以避免多线程之间的竞争和锁等问题,简化了并发控制,提高了性能。通过使用异步I/O和非阻塞方式(NIO)处理客户端请求,Redis能够快速响应大量的请求。
  • 数据结构:Redis支持多种数据结构,包括字符串、哈希表、列表、集合、有序集合等。每种数据结构都有其专门的底层实现方式,以提供高效的操作和内存利用率。
  • RDB持久化:Redis可以将内存中的数据周期性地保存到磁盘上的RDB文件中,以实现持久化存储。这种方式中,Redis会fork出一个子进程来处理数据的持久化工作。
  • AOF持久化:除了RDB持久化外,Redis还支持AOF(Append Only File)持久化方式。在AOF持久化中,Redis会将每个修改命令追加到AOF文件中,以记录数据的修改操作。
  • 事件驱动:Redis使用事件驱动的方式处理客户端请求和网络操作。它使用I/O多路复用(如epoll)监听并处理事件,当有事件发生时,Redis会根据事件的类型执行相应的操作。
  • 哨兵和集群:Redis提供了哨兵和集群功能来实现高可用性和水平扩展。哨兵用于监控和管理Redis实例,当主节点失效时,哨兵自动进行故障转移。集群则是将多个Redis节点组成一个逻辑上的整体,集群实现数据分片和负载均衡

4.1 Redis 数据结构与其底层实现

Redis(Remote Dictionary Server) 由 C语言编写,其每种数据结构都定义为结构体,以面向对象的方式进行的开发,以下就对每种数据结构进行简单的介绍。

参考:Redis源码

在这里插入图片描述
value的结构体是redisObject,定义在redis.h文件中。该结构体的定义如下:

typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
    int refcount;
    void *ptr;
} robj;

其中,redisObject是Redis中通用的数据结构,robj是它的别名。

该结构体包含以下字段:

  • type字段:标识数据的类型,包括字符串、列表、哈希表等。在字符串数据结构中,该字段的值为REDIS_STRING,即字符串类型。
  • encoding字段:标识数据的编码方式,Redis常见的字符串编码方式有REDIS_ENCODING_RAW和REDIS_ENCODING_INT等。在字符串数据结构中,该字段的值为REDIS_ENCODING_RAW,表示使用原始的字符串编码。
  • lru字段:记录对象的最近使用时间,用于实现LRU(Least Recently Used)算法进行对象的回收控制。
  • refcount字段:引用计数,记录该对象的被引用次数。
  • ptr字段:指向实际存储数据的指针。对于字符串数据结构,指向数据的char array。

通过redisObject结构体,Redis能够方便地管理和操作不同类型的数据,包括字符串、列表、哈希表等。redis中每一个value都可以理解为是一个RedisObject

简单来说,Redis 的 key 为字符串,而其 value 则为 redisObject 对象。

4.1.1 动态字符串(SDS)

简单动态字符串 - SDS (simple dynamic string),是Redis中用于表示字符串的数据结构。相比于C语言中的字符串,SDS具有更好的性能和可靠性,同时支持动态扩容和缩容。

其数据结构定义在redis/src/sds.h目录下。该结构体的定义如下:

struct sdshdr {
    int len;     // 字符串长度
    int free;    // 空闲空间长度
    char buf[];  // 字节数组,存储字符串内容
};

SDS的优点包括:

  • 高效的字符串操作:SDS支持O(1)时间复杂度的字符串长度计算、字符串拼接、字符串截取等操作,相比于C语言中的字符串操作更加高效。
  • 动态扩容:当SDS中的字符串长度超过了当前空间的大小时,SDS会自动扩容,避免了C语言中需要手动管理内存的问题。

4.1.2 哈希表(Hash)

在Redis中,哈希表(Hash)是一种用于存储键值对的数据结构。Redis的哈希表实现使用了开放地址法和链地址法的混合技术来解决哈希冲突。

Redis的哈希表由一个数组和一些链表组成每个数组元素称为桶(bucket),而每个桶可以存储多个键值对。

其数据结构定义在redis/src/dict.h目录下,哈希表的结构体定义如下:

typedef struct dict {
    dictType *type;             // 哈希表类型
    void *privdata;             // 私有数据
    dictht ht[2];               // 哈希表的两个桶,允许在进行rehash时同时使用新旧两个哈希表
    long rehashidx;             // rehash索引,标识当前rehash所处的位置
    int iterators;              // 哈希表迭代器的数量
} dict;

dictht结构体定义如下:

typedef struct dictht {
    dictEntry **table;          // 哈希桶数组
    unsigned long size;         // 哈希表大小
    unsigned long sizemask;     // 哈希表掩码,用于计算桶索引
    unsigned long used;         // 已使用的桶数量
} dictht;

dictEntry结构体用于存储哈希表中的键值对,定义如下:

typedef struct dictEntry {
    void *key;                  // 键
    union {
        void *val;              // 值
        uint64_t u64;           // 64位无符号整数类型的值
        int64_t s64;            // 64位有符号整数类型的值
        double d;               // 双精度浮点数类型的值
    } v;
    struct dictEntry *next;     // 指向下一个键值对,用于解决哈希冲突
} dictEntry;

哈希表通过哈希函数计算键的哈希值,然后将键值对存储到对应的桶中。当多个键具有相同的哈希值时,这些键值对会以链表的形式存储在同一个桶中,通过链表的方式解决哈希冲突

通过哈希表的结构,Redis能够高效地存储和查找键值对,提供了快速的读写操作。哈希表在Redis中应用广 泛,例如存储hash数据类型、存储键与值的元数据等。

4.1.3 列表(List)

在Redis中,列表(List)是一种有序、可重复的数据结构,它可以存储一个或多个字符串元素。Redis的列表使用双向链表实现,支持在表头和表尾进行快速的插入和删除操作。

Redis的列表数据结构即为双向链表,其数据结构定义在redis/src/quicklist.h目录下,结构体定义:

typedef struct listNode {
    struct listNode *prev;  // 前一个节点
    struct listNode *next;  // 后一个节点
    void *value;            // 节点的值
} listNode;

typedef struct list {
    listNode *head;         // 头节点
    listNode *tail;         // 尾节点
    unsigned long len;      // 列表长度
} list;

每个节点包含了一个指向值的指针,可以存储任意类型的数据。在Redis中,通常将节点的值存储为字符串。

Redis列表提供了一系列的API函数,可以用于在列表的头部和尾部进行元素的插入、删除和查询操作。一些常用的列表操作包括:

  • LPUSH:在列表头部插入一个或多个元素。
  • RPUSH:在列表尾部插入一个或多个元素。
  • LPOP:从列表头部弹出一个元素。
  • RPOP:从列表尾部弹出一个元素。
  • LINDEX:根据索引来获取列表中的元素。
  • LLEN:获取列表的长度。

Redis的列表数据结构常用于实现队列、栈、消息队列等场景,可以高效地进行元素的插入、删除和查询操作。双向链表的结构使得操作的时间复杂度为O(1),在Redis中被广泛使用。

zipList是一种压缩列表,用于存储一系列的连续元素。这里不详细介绍了,自己去看文档去…

4.1.4 集合(Set)

在Redis中,集合(Set)是一种无序且不重复的数据结构,它可以存储多个元素。Redis的集合数据结构内部使用哈希表或者跳跃表(Skip List)实现,具备高效的插入、删除和查询操作。

Redis的集合数据结构特点如下:

  • 集合中的元素是无序的。
  • 集合中的元素是唯一的,不会存在重复的元素。
  • 集合的存储和查找操作时间复杂度都是O(1)。
  • 集合适合用于存储和处理不重复的元素集合。

在Redis中,集合数据结构可以使用哈希表或跳跃表进行实现。哈希表用于存储集合中的元素,利用哈希函数进行高效的查找和删除操作。跳跃表则是一种有序的数据结构,可以用于支持按照元素的顺序进行插入和查询操作。

其数据结构定义在redis/src/set.h目录下,Redis的集合结构体定义如下:

typedef struct {
    dict *dict; // 哈希表
} redisSet;

其中,redisSet结构体包含了一个指向哈希表的指针。

Redis提供了一系列操作集合的命令,包括:

  • SADD:向集合中添加一个或多个元素。
  • SREM:从集合中删除一个或多个元素。
  • SCARD:获取集合的元素数量(集合的基数)。
  • SISMEMBER:判断一个元素是否存在于集合中。
  • SMEMBERS:返回集合中的所有元素。
  • SUNION:返回多个集合的并集。
  • SDIFF:返回多个集合的差集。
  • SINTER:返回多个集合的交集。

4.1.5 有序集合(Sorted Set)

Redis中的有序集合(Sorted Set)是一种数据结构,它可以存储多个成员(member)并为每个成员关联一个分数(score)。有序集合根据分数对成员进行排序,并且每个成员在集合中是唯一的。

有序集合支持的操作包括:

  • 添加成员:可以通过ZADD命令向有序集合中添加一个或多个成员,每个成员关联一个分数。
  • 删除成员:可以通过ZREM命令从有序集合中删除一个或多个成员。
  • 修改分数:可以通过ZINCRBY命令增加或减少成员的分数。
  • 查询成员:可以通过ZRANK、ZSCORE等命令查询成员的排名或分数。
  • 范围查询:可以通过ZRANGE、ZREVRANGE等命令根据分数范围或排名范围查询成员。

有序集合在Redis中的应用广泛,例如:

  • 排行榜:可以使用有序集合来存储用户的分数,并根据分数排名来展示排行榜。
  • 带权重的任务队列:可以使用有序集合来存储带有优先级的任务,并根据优先级进行处理。
  • 数据统计:可以使用有序集合来存储统计数据,并根据分数进行排序和查询。

Redis 的有序集合数据结构底层使用了跳跃表(Skip List)和哈希表(Hash Table)两种数据结构组成。 这两种数据结构被封装在 Redis 源码中的结构体中。

zskiplist 结构体: 该结构体用于表示跳跃表的节点。其定义在 redis/src/server.h 文件中,并包含以下成员:

typedef struct zskiplistNode {
    sds ele;
    double score;
    struct zskiplistNode *backward;
    struct zskiplistLevel {
        struct zskiplistNode *forward;
        unsigned int span;
    } level[];
} zskiplistNode;

成员说明:

  • ele:指向存储成员的 sds (简单动态字符串)对象的指针。
  • score:表示成员的分数,以 double 类型存储。
  • backward:指向前一个节点的指针,用于反向查找。
  • level[]:用于存储每个节点的层级信息(索引层级),每个层级包括指向下一个节点和跨越的节点数(span)的指针。

跳跃表是一种有序的数据结构,它通过在原始有序链表上增加多级索引的方式来提高查找效率。每一级索引都是原始链表的一个子集,且层级越高,元素数量越少。这样一来,跳跃表实现了类似二分查找的效果,使得在有序链表中快速定位元素成为可能。

跳跃表的节点包含一个值和多个指向同一级或下一级节点的指针。通过这些指针,我们可以在跳跃表中迅速导航到目标节点。跳跃表的插入、删除和查找操作时间复杂度都是O(log n),非常适合用于需要高效查找的场景。

跳跃表在Redis中被广泛使用,特别是在有序集合(Sorted Set)的实现中。它可以让我们在O(log n)的时间复杂度内进行有序集合中元素的插入、删除和范围查找等操作。

4.2 Redis IO模型

Redis采用了单线程和非阻塞I/O模型来处理客户端连接和网络操作。下面是Redis中使用的I/O模型的详细说明:

  • 单线程模型:Redis是单线程的,主要是为了避免多线程之间的竞争和锁等问题,简化并发控制,提高性能。单线程模型使得Redis能够使用异步I/O和非阻塞方式处理客户端请求,以及并发地处理多个连接。
  • 非阻塞I/O模型:Redis使用非阻塞I/O模型来处理网络连接。它通过将套接字设置为非阻塞模式,并使用I/O多路复用技术(如select、poll、epoll等)来监听多个套接字上的事件。这样,在有连接、读取或写入事件发生时,Redis可以及时处理请求,而无需等待阻塞。
  • 事件驱动机制:Redis使用事件驱动的方式处理客户端请求和网络操作。它通过事件循环机制,监听多个套接字上的事件,如新连接、读取和写入事件等。当事件就绪时,Redis会根据事件类型执行相应的操作,例如接受新连接、读取数据或发送响应。
  • 异步操作和Pipeline:Redis充分利用非阻塞I/O模型和异步操作的优势。例如,在处理批量请求时,Redis可以使用Pipeline技术将多个命令合并为一个批量的发送和接收过程,减少通信开销和延迟,提高性能。

4.2.1 套接字(SOCKET)

Redis 的网络通信方式为 套接字(SOCKET)。参考:SOCKET网络编程。

Socket是一种用于网络通信的编程接口(类比于Java的接口,其实现方式可以是TCP/UDP等方式进行网络通信),它提供了一组用于网络数据传输的函数和方法。通过Socket,应用程序可以在不同的主机之间进行数据交换和通信。

Socket在网络编程中起着重要的作用,它允许应用程序通过网络进行数据传输,包括发送和接收数据。Socket提供了一种标准的接口,使得不同操作系统和编程语言的应用程序能够进行跨平台的网络通信

以下是一些知名的开源软件和中间件,它们使用了Socket进行网络通信:

  • Apache HTTP Server:Apache是一个广泛使用的开源Web服务器软件,它使用Socket进行与客户端的HTTP通信。
  • Nginx:Nginx是一个高性能的开源Web服务器和反向代理服务器,它也使用Socket进行与客户端的HTTP通信。
  • MySQL:MySQL是一个流行的开源关系型数据库管理系统,它使用Socket进行与客户端的数据库连接和数据传输。
  • Redis:Redis是一个高性能的开源内存数据存储系统,它使用Socket进行与客户端的通信,支持多种数据结构的操作。
  • ZeroMQ:ZeroMQ是一个开源的消息传递库,它提供了高性能的消息队列和分布式通信功能,使用Socket进行网络通信。
  • RabbitMQ:RabbitMQ是一个开源的消息队列中间件,它使用Socket进行与客户端的通信,支持可靠的消息传递和异步通信。

这些开源软件和中间件使用Socket作为底层的网络通信接口(Netty封装了底层的Socket编程细节),通过Socket实现了与客户端或其他服务之间的数据传输和通信。

4.2.2 非阻塞I/O模型

可以类比于 Java NIO,参考:Java NIO

  • IO多路复用一个进程来维护多个 Socket多个请求复用了一个进程,这就是多路复用
  • select/poll/epoll: Linux 下的三种提供 I/O 多路复用的 API,内核提供给用户态的多路复用系统调用,进程可以通过一个系统调用函数从内核中获取多个事件

IO多路复用实现方式:

  • select :将已连接的 Socket 都放到一个文件描述符集合。select使用固定长度的 BitsMap,表示文件描述符集合,但是在Linux 系统中,由内核中的 FD_SETSIZE 限制, 默认最大值为 1024,只能监听 0~1023 的文件描述符。
  • poll :使用动态数组,以链表形式来组织。poll 和 select 并没有太大的本质区别,都是使用「线性结构」存储进程关注的 Socket 集合,因此都需要遍历文件描述符集合来找到可读或可写的 Socket,时间复杂度为 O(n)
  • epoll :在内核里使用红黑树来跟踪进程所有待检测的文件描述字,时间复杂度是 O(logn)。且epoll 使用事件驱动的机制,内核里维护了一个链表来记录就绪事件,不需要像 select/poll 那样轮询扫描整个 socket 集合,大大提高了检测的效率。

总的来说,Redis使用单线程和非阻塞I/O模型,结合事件驱动的机制,能够高效地处理客户端请求和网络操作。这使得Redis具有出色的性能和响应能力,在各种场景下被广泛应用。

4.3 Redis 持久化

Redis提供两种持久化方式来确保数据在重启或故障恢复后的可靠性:RDB(Redis数据库文件)和AOF(Redis日志文件)。

4.3.1 RDB(Redis数据库文件)持久化

RDB是Redis的默认持久化方式,它会将数据库在某个时间点的快照保存到一个二进制文件中。

生成RDB文件:

  1. 手动执行save命令触发方式:该命令会阻塞当前Redis服务器,执行save命令期间,Redis不能处理其他命令,直到RDB过程完成为止。
  2. 手动执行bgsave命令触发方式:执行该命令时,Redis会在后台异步进行快照操作,快照同时还可以响应客户端请求。阻塞只发生在fork阶段,一般时间很短。基本上 Redis 内部所有的RDB操作都是采用 bgsave 命令。
  3. 修改redis.conf配置文件自动备份save m n m :表示m秒内数据集存在n次修改时,自动触发bgsave。

RDB优势:

  • RDB文件紧凑,全量备份,非常适合用于进行备份和灾难恢复
  • 生成RDB文件的时候,redis主进程会fork()一个子进程来处理所有保存工作,主进程不需要进行任何磁盘IO操作
  • RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快

RDB劣势:

  • RDB快照是一次全量备份,存储的是内存数据的二进制序列化形式,存储上非常紧凑。当进行快照持久化时,会开启一个子进程专门负责快照持久化,子进程会拥有父进程的内存数据,父进程修改内存子进程不会反应出来,所以在快照持久化期间修改的数据不会被保存,可能丢失数据

4.3.2 AOF(Redis日志文件)持久化

AOF持久化将Redis的每个写操作记录为一个追加日志的方式,因此会导致日志文件越来越大,需要定期处理。

AOF文件包含了将Redis从初始状态恢复到当前状态所需的所有写命令。可以通过配置Redis来控制AOF文件的刷新频率和文件的压缩策略。在Redis重启时,会读取AOF文件中的日志,并将日志重新执行一次,恢复数据到内存中。

AOF默认是关闭的,通过redis.conf配置文件进行开启

## 此选项为aof功能的开关,默认为“no”,可以通过“yes”来开启aof功能  
## 只有在“yes”下,aof重写/文件同步等特性才会生效  
appendonly yes  

## 指定aof文件名称  
appendfilename appendonly.aof  

## 指定aof操作中文件同步策略,有三个合法值:always everysec no,默认为everysec  
appendfsync everysec  
## 在aof-rewrite期间,appendfsync是否暂缓文件同步,"no"表示“不暂缓”,“yes”表示“暂缓”,默认为“no”  
no-appendfsync-on-rewrite no  

## aof文件rewrite触发的最小文件尺寸(mb,gb),只有大于此aof文件大于此尺寸是才会触发rewrite,默认“64mb”,建议“512mb”  
auto-aof-rewrite-min-size 64mb  

## 相对于“上一次”rewrite,本次rewrite触发时aof文件应该增长的百分比  
## 每一次rewrite之后,redis都会记录下此时“新aof”文件的大小(例如A)
## aof文件增长到A*(1 + p)之后,触发下一次rewrite,每一次aof记录的添加,都会检测当前aof文件的尺寸。  
auto-aof-rewrite-percentage 100

在上述配置文件中,可观察到Redis中提供了3中AOF记录同步选项:

  • always:每一条AOF记录都立即同步到文件,性能很低,但较为安全。
  • everysec:每秒同步一次,性能和安全都比较中庸的方式,也是redis推荐的方式。如果遇到物理服务器故障,可能导致最多1秒的AOF记录丢失。
  • no:Redis永不直接调用文件同步,而是让操作系统来决定何时同步磁盘。性能较好,但很不安全。

重写(rewrite)机制

  • AOF Rewrite虽然是“压缩”AOF文件的过程,但并非采用“基于原AOF文件”来重写或压缩,而是采取了类似RDB快照的方式:基于Copy On Write,全量遍历内存中数据,然后逐个序列到AOF文件中。因此AOF rewrite能够正确反应当前内存数据的状态。
  • AOF重写(bgrewriteaof)和RDB快照写入(bgsave)过程类似,二者都消耗磁盘IO。Redis采取了“schedule”策略:无论是“人工干预”还是系统触发,快照和重写需要逐个被执行。
  • 重写过程中,对于新的变更操作将仍然被写入到原AOF文件中,同时这些新的变更操作也会被Redis收集起来。当内存中的数据被全部写入到新的AOF文件之后,收集的新的变更操作也将被一并追加到新的AOF文件中。然后将新AOF文件重命名为appendonly.aof,使用新AOF文件替换老文件,此后所有的操作都将被写入新的AOF文件

触发机制

  1. 手动触发 调用bgrewriteaof命令
redis-cli -h ip -p port bgrewriteaof
  1. 自动触发
根据auto-aof-rewrite-min-size和auto-aof-rewrite-percentage参数确定自动触发时机

auto-aof-rewrite-min-size:表示运行AOF重写时文件最小体积,默认为64MB(我们线上是512MB)。

auto-aof-rewrite-percentage:代表当前AOF文件空间(aof_current_size)和上一次重写后AOF文件空间(aof_base_size)的值
自动触发时机:

(aof_current_size > auto-aof-rewrite-min-size ) && (aof_current_size - aof_base_size) / aof_base_size >= auto-aof-rewrite-percentage

其中aof_current_size和aof_base_size可以在info Persistence统计信息中查看。

AOF的优缺点

  • 优点:AOF只是追加写日志文件,对服务器性能影响较小,写入速度比RDB要快,消耗的内存较少
  • 缺点:AOF方式生成的日志文件太大,需要不断AOF重写,进行瘦身。即使经过AOF重写瘦身,由于文件是文本文件,文件体积较大(相比于RDB的二进制文件)。 AOF重演命令式的恢复数据,恢复速度显然比RDB要慢

4.3.3 Redis 4.0 混合持久化

Redis 4.0 为了解决这个问题,带来了一个新的持久化选项——混合持久化。将 rdb 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是自持久化开始到持久化结束的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小。相当于:

  • 大量数据使用粗粒度(时间上)的rdb快照方式,性能高,恢复时间快
  • 增量数据使用细粒度(时间上)的AOF日志方式,尽量保证数据的不丢失

在 Redis 重启的时候,可以先加载 rdb 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。

4.4 Redis 哨兵与集群

4.4.1 单机模式

Redis 单副本,采用单个 Redis 节点部署架构,没有备用节点实时同步数据,不提供数据持久化和备份策略,适用于数据可靠性要求不高的纯缓存业务场景。

优点:

  • 架构简单,部署方便。
  • 高性价比:缓存使用时无需备用节点(单实例可用性可以用 supervisor 或 crontab保证),当然为了满足业务的高可用性,也可以牺牲一个备用节点,但同时刻只有一个实例对外提供服务。
  • 高性能。

缺点:

  • 不保证数据的可靠性
  • 在缓存使用,进程重启后,数据丢失,即使有备用的节点解决高可用性,但是仍然不能解决缓存预热问题,因此不适用于数据可靠性要求高的业务。
  • 高性能受限于单核 CPU 的处理能力(Redis 是单线程机制),CPU为主要瓶颈,所以适合操作命令简单,排序、计算较少的场景。也可以考虑用 Memcached 替代。

4.4.2 主从架构

主(master)和 从(slave)部署在不同的服务器上,当主节点服务器写入数据时会同步到从节点的服务器上,一般主节点负责写入数据从节点负责读取数据

从节点设置只读属性,而主节点没有只写属性,因此,主节点可读可以写

优点:

  • 读写分离,提高效率
  • 主节点负责写操作,从节点负责读操作;如果写少读多场景,配置多个从节点的话,效率非常高
  • 数据热备份,提供多个副本。
  • 从节点宕机,影响较小

缺点:

  • 主节点故障,集群则无法进行工作,可用性比较低,从节点升主节点需要人工手动干预。因为只有主节点能进行写操作,一旦主节点宕机,整个服务就无法使用。当然此时从节点仍可以进行读操作,但是对于整个服务流程来说,是无法使用的。
  • Master的写的压力难以降低。
  • 如果写操作比较多,那么只有一个主节点的话,无法分担压力。
  • 主节点存储能力受到单击限制。
  • 主节点只能有一个,因此单节点内存大小不会太大,因此存储数据量受限。
  • 主从数据同步,可能产生部分的性能影响甚至同步风暴。风暴问题,对于任何集群分布式来说都存在,要合理分布节点。

4.4.3 哨兵

为了解决这两个问题,在2.8版本之后redis正式提供了sentinel架构。在redis3.0以前的版本要实现集群一般是借助哨兵sentinel工具来监控master节点的状态。如果master节点异常,则会做主从切换,

将某一台slave作为master,哨兵的配置略微复杂,并且性能和高可用性等各方面表现一般。

优点:

  • 对节点进行监控,来完成自动的故障发现与转移

缺点:

  • 特别是在主从切换的瞬间存在访问瞬断的情况,等待时间比较长,至少十来秒不可用。
  • 哨兵模式只有一个主节点对外提供服务,没法支持很高的并发
  • 单个主节点内存也不宜设置得过大,否则会导致持久化文件过大,影响数据恢复或主从同步的效率。
  • 与主从相比,哨兵仅解决了手动切换主从节点问题,至于其他的问题,基本上仍然存在。

哨兵的主要问题还是由于中心架构,仅存在一个master节点引起的,写的效率太低

4.4.4 集群

Redis Cluster 是3.0版后推出的Redis 分布式集群解决方案,主要解决 Redis 分布式方面的需求,比如,当遇到单机内存,并发和流量等瓶颈的时候,Redis Cluster 能起到很好的负载均衡的目的。

Redis Cluster 集群节点最小配置6个节点以上(3主3 从),其中主节点提供读写操作,从节点作为备用节点,不提供请求,只作为故障转移使用。Redis Cluster 采用虚拟槽分区,所有的键根据哈希函数映射到 0~16383 个整数槽内,每个节点负责维护一部分槽以及槽所印映射的键值数据。

  • 注意:集群模式下 从节点不提供读写,与主从模式不一样。 总结一经验,分布式场景下:集群模式一般从节点不参与读写,仅作为备用节点。而主从一般都要负责读或写,都要参与具体的工作。

优点:

  • 无中心架构。即有多个master节点,不像哨兵模式下仅有一个。这样写的压力就可以分散了;并且存储量也可以扩展了,因为多个主节点都可以存储一部分数据,总量要远大于单主节点架构。
  • 数据按照 slot 存储分布在多个节点,节点间数据共享,可动态调整数据分布。
  • 可扩展性:可线性扩展到1000 多个节点,节点可动态添加或删除。
  • 高可用性:部分节点不可用时,集群仍可用。通过增加 Slave 做 standby 数据副本,能够实现故障自动failover,节点之间通过 gossip 协议交换状态信息,用投票机制完成 Slave 到 Master 的角色提升。

当然,如果某个槽归属的小群内都不可用时,整个服务仍然是不可用的!通过cluster-require-full-coverageyes 控制该特性,默认yes 即需要集群完整,方可对外提供服务,设置为no ,其他的小集群仍然可以对外提供服务。

缺点:

  • 如果主节点A和它的从节点A1都宕机了,那么该集群就无法再提供服务了。

五、Spring 整合 Redis

5.1 引入依赖与配置

5.1.1 添加依赖

在你的项目中添加Spring Data Redis的依赖。可以使用Maven或Gradle来管理依赖关系,这里使用Maven 在spring boot 中添加依赖。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

5.1.2 配置Redis连接

在Spring的配置文件(例如application.properties或application.yml)中,配置Redis的连接信息,包括主机、端口、密码等。你可以使用Spring提供的不同方式的配置,如XML配置、注解或Java配置类。

spring:
   redis:
     host: "127.0.0.1"
     port: "6379"
     password: "6379"
     database: 0
     lettuce:
       pool:
         min-idle: 10
         max-idle: 100
         max-active: 100
         max-wait: 10000

全部配置详解

# redis 配置项
# 连接URL,配置后会覆盖host、port等配置,eg: redis://user:password@example.com:6379
spring.redis.url=
# 连接地址
spring.redis.host=127.0.0.1
# Redis服务器连接端口
spring.redis.port=6379
# 连接工厂使用的数据库索引(0-15),默认为0
spring.redis.database=0
# Redis服务器连接用户
spring.redis.username=
# Redis服务器连接密码(默认为空)
spring.redis.password=123456
# 是否启用SSL支持
spring.redis.ssl=false
# 读取超时
spring.redis.timeout=5000
# 连接超时
spring.redis.connect-timeout=10000
# 在与CLIENT SETNAME的连接上设置的客户端名称
spring.redis.client-name=
# 要使用的客户端类型。默认情况下,根据类路径自动检测
spring.redis.client-type=lettuce


# Redis哨兵属性
# Redis服务器名称
spring.redis.sentinel.master=sentinel-redis
# 哨兵节点,以逗号分隔的“ host:port”对列表
spring.redis.sentinel.nodes=127.0.0.1:7000,127.0.0.1:7001
# 用于使用哨兵进行身份验证的密码
spring.redis.sentinel.password=123456

# 集群属性
# 以逗号分隔的“ host:port”对列表,这表示集群节点的“初始”列表,并且要求至少具有一个条目
spring.redis.cluster.nodes=127.0.0.1:7000,127.0.0.1:7001
# 在集群中执行命令时要遵循的最大重定向数
spring.redis.cluster.max-redirects=1
# 拓扑动态感应即客户端能够根据 redis cluster 集群的变化,动态改变客户端的节点情况,完成故障转移。
spring.redis.lettuce.cluster.refresh.adaptive=true
# 是否发现和查询所有群集节点以获取群集拓扑。设置为false时,仅将初始种子节点用作拓扑发现的源
spring.redis.lettuce.cluster.refresh.dynamic-refresh-sources=false
# 集群拓扑刷新周期
spring.redis.lettuce.cluster.refresh.period=1000

# 连接池配置
# 连接池池中“空闲”连接的最大数量。使用负值表示无限数量的空闲连接,默认为8
spring.redis.lettuce.pool.max-idle=8
# 中维护的最小空闲连接数,默认为0
spring.redis.lettuce.pool.min-idle=0
# 连接池可以分配的最大连接数。使用负值表示没有限制,默认为8
spring.redis.lettuce.pool.max-active=8
# 当连接池耗尽时,在抛出异常之前,连接分配应阻塞的最长时间。使用负值无限期等待,默认为-1
spring.redis.lettuce.pool.max-wait=-1
# 空闲连接从运行到退出的时间间隔。当为正时,空闲连接回收线程启动,否则不执行空闲连接回收
spring.redis.lettuce.pool.time-between-eviction-runs=
# 宕机超时时间,默认100ms
spring.redis.lettuce.shutdown-timeout=100

5.2 访问 Redis

5.2.1 redis 序列化配置

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper mapper = new ObjectMapper();
        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        jackson2JsonRedisSerializer.setObjectMapper(mapper);
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(stringRedisSerializer);
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

}

redis要序列化对象是使对象可以跨平台存储和进行网络传输。 因为存储和网络传输都需要把一个对象状态保存成一种跨平台识别的字节格式,然后其他的平台才可以通过字节信息解析还原对象信息,所以进行“跨平台存储”和”网络传输”的数据都需要进行序列化。

5.2.2 Redis 工具类

// 使用@Component将类交给 Spring 管理,就不需要自己实现单例模式。
@Component
public class RedisUtil {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    public void set(String key, Object value, long time) {
        redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
    }

    public void set(String key, Object value) {
        redisTemplate.opsForValue().set(key, value);
    }

    public Object get(String key) {
        return redisTemplate.opsForValue().get(key);
    }

    public Boolean del(String key) {
        return redisTemplate.delete(key);
    }

    public Long del(List<String> keys) {
        return redisTemplate.delete(keys);
    }

    public Boolean expire(String key, long time) {
        return redisTemplate.expire(key, time, TimeUnit.SECONDS);
    }

    public Long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }

    public Boolean hasKey(String key) {
        return redisTemplate.hasKey(key);
    }

    public Long incr(String key, long delta) {
        return redisTemplate.opsForValue().increment(key, delta);
    }

    public Long incrExpire(String key, long time) {
        Long count = redisTemplate.opsForValue().increment(key, 1);
        if (count != null && count == 1) {
            redisTemplate.expire(key, time, TimeUnit.SECONDS);
        }
        return count;
    }

    public Long decr(String key, long delta) {
        return redisTemplate.opsForValue().increment(key, -delta);
    }

    public Object hGet(String key, String hashKey) {
        return redisTemplate.opsForHash().get(key, hashKey);
    }

    public Boolean hSet(String key, String hashKey, Object value, long time) {
        redisTemplate.opsForHash().put(key, hashKey, value);
        return expire(key, time);
    }

    public void hSet(String key, String hashKey, Object value) {
        redisTemplate.opsForHash().put(key, hashKey, value);
    }

    public Map hGetAll(String key) {
        return redisTemplate.opsForHash().entries(key);
    }

    public Boolean hSetAll(String key, Map<String, Object> map, long time) {
        redisTemplate.opsForHash().putAll(key, map);
        return expire(key, time);
    }

    public void hSetAll(String key, Map<String, ?> map) {
        redisTemplate.opsForHash().putAll(key, map);
    }

    public void hDel(String key, Object... hashKey) {
        redisTemplate.opsForHash().delete(key, hashKey);
    }

    public Boolean hHasKey(String key, String hashKey) {
        return redisTemplate.opsForHash().hasKey(key, hashKey);
    }

    public Long hIncr(String key, String hashKey, Long delta) {
        return redisTemplate.opsForHash().increment(key, hashKey, delta);
    }

    public Long hDecr(String key, String hashKey, Long delta) {
        return redisTemplate.opsForHash().increment(key, hashKey, -delta);
    }

    public Double zIncr(String key, Object value, Double score) {
        return redisTemplate.opsForZSet().incrementScore(key, value, score);
    }

    public Double zDecr(String key, Object value, Double score) {
        return redisTemplate.opsForZSet().incrementScore(key, value, -score);
    }

    public Map<Object, Double> zReverseRangeWithScore(String key, long start, long end) {
        return redisTemplate.opsForZSet().reverseRangeWithScores(key, start, end)
                .stream()
                .collect(Collectors.toMap(ZSetOperations.TypedTuple::getValue, ZSetOperations.TypedTuple::getScore));
    }

    public Double zScore(String key, Object value) {
        return redisTemplate.opsForZSet().score(key, value);
    }

    public Map<Object, Double> zAllScore(String key) {
        return Objects.requireNonNull(redisTemplate.opsForZSet().rangeWithScores(key, 0, -1))
                .stream()
                .collect(Collectors.toMap(ZSetOperations.TypedTuple::getValue, ZSetOperations.TypedTuple::getScore));
    }

    public Set<Object> sMembers(String key) {
        return redisTemplate.opsForSet().members(key);
    }

    public Long sAdd(String key, Object... values) {
        return redisTemplate.opsForSet().add(key, values);
    }

    public Long sAddExpire(String key, long time, Object... values) {
        Long count = redisTemplate.opsForSet().add(key, values);
        expire(key, time);
        return count;
    }

    public Boolean sIsMember(String key, Object value) {
        return redisTemplate.opsForSet().isMember(key, value);
    }

    public Long sSize(String key) {
        return redisTemplate.opsForSet().size(key);
    }

    public Long sRemove(String key, Object... values) {
        return redisTemplate.opsForSet().remove(key, values);
    }

    public List<Object> lRange(String key, long start, long end) {
        return redisTemplate.opsForList().range(key, start, end);
    }

    public Long lSize(String key) {
        return redisTemplate.opsForList().size(key);
    }

    public Object lIndex(String key, long index) {
        return redisTemplate.opsForList().index(key, index);
    }

    public Long lPush(String key, Object value) {
        return redisTemplate.opsForList().rightPush(key, value);
    }

    public Long lPush(String key, Object value, long time) {
        Long index = redisTemplate.opsForList().rightPush(key, value);
        expire(key, time);
        return index;
    }

    public Long lPushAll(String key, Object... values) {
        return redisTemplate.opsForList().rightPushAll(key, values);
    }

    public Long lPushAll(String key, Long time, Object... values) {
        Long count = redisTemplate.opsForList().rightPushAll(key, values);
        expire(key, time);
        return count;
    }

    public Long lRemove(String key, long count, Object value) {
        return redisTemplate.opsForList().remove(key, count, value);
    }

    public Boolean bitAdd(String key, int offset, boolean b) {
        return redisTemplate.opsForValue().setBit(key, offset, b);
    }

    public Boolean bitGet(String key, int offset) {
        return redisTemplate.opsForValue().getBit(key, offset);
    }

    public Long bitCount(String key) {
        return redisTemplate.execute((RedisCallback<Long>) con -> con.bitCount(key.getBytes()));
    }

    public List<Long> bitField(String key, int limit, int offset) {
        return redisTemplate.execute((RedisCallback<List<Long>>) con ->
                con.bitField(key.getBytes(),
                        BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(limit)).valueAt(offset)));
    }

    public byte[] bitGetAll(String key) {
        return redisTemplate.execute((RedisCallback<byte[]>) con -> con.get(key.getBytes()));
    }

    public Long hyperAdd(String key, Object... value) {
        return redisTemplate.opsForHyperLogLog().add(key, value);
    }

    public Long hyperGet(String... key) {
        return redisTemplate.opsForHyperLogLog().size(key);
    }

    public void hyperDel(String key) {
        redisTemplate.opsForHyperLogLog().delete(key);
    }

    public Long geoAdd(String key, Double x, Double y, String name) {
        return redisTemplate.opsForGeo().add(key, new Point(x, y), name);
    }

    public List<Point> geoGetPointList(String key, Object... place) {
        return redisTemplate.opsForGeo().position(key, place);
    }

    public Distance geoCalculationDistance(String key, String placeOne, String placeTow) {
        return redisTemplate.opsForGeo()
                .distance(key, placeOne, placeTow, RedisGeoCommands.DistanceUnit.KILOMETERS);
    }

    public GeoResults<RedisGeoCommands.GeoLocation<Object>> geoNearByPlace(String key, String place, Distance distance, long limit, Sort.Direction sort) {
        RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().includeCoordinates();
        // 判断排序方式
        if (Sort.Direction.ASC == sort) {
            args.sortAscending();
        } else {
            args.sortDescending();
        }
        args.limit(limit);
        return redisTemplate.opsForGeo()
                .radius(key, place, distance, args);
    }

    public List<String> geoGetHash(String key, String... place) {
        return redisTemplate.opsForGeo()
                .hash(key, place);
    }

}

六、Redis 应用

6.1 缓存

6.1.1 缓存策略

  • 内存淘汰:redis自动进行,当redis内存达到咱们设定的max-memery的时候,会自动触发淘汰机制,淘汰掉一些不重要的数据(可以自己设置策略方式)
  • 超时剔除:当我们给redis设置了过期时间ttl之后,redis会将超时的数据进行删除,方便咱们继续使用缓存
  • 主动更新:我们可以手动调用方法把缓存删掉,通常用于解决缓存和数据库不一致问题

数据库缓存不一致解决方案:

  • Cache Aside Pattern: 人工编码方式,缓存调用者在更新完数据库后再去更新缓存,也称之为双写方案
  • Read/Write Through Pattern : 由系统本身完成,数据库与缓存的问题交由系统本身去处理
  • Write Behind Caching Pattern :调用者只操作缓存,其他线程去异步处理数据库,实现最终一致

删除缓存还是更新缓存?

  • 更新缓存:每次更新数据库都更新缓存,无效写操作较多
  • 删除缓存:更新数据库时让缓存失效,查询时再更新缓存

先操作缓存还是先操作数据库?

  • 先删除缓存,再操作数据库。
  • 先删除缓存,在多线程情况下会导致数据长时间不一致的问题。
  • 先操作数据库,再删除缓存

如何保证缓存与数据库的操作的同时成功或失败?

  • 单体系统,将缓存与数据库操作放在一个事务
  • 分布式系统,利用TCC等分布式事务方案

示例:

	/**
     * 查询资源详情
     * @param id
     * @return
     */
    @Override
    public ResModel<Resource> getResource(Integer id) {
        String key = CACHE_RESOURCE_KEY + id;
        //查询缓存
        String json = redisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(json)){//缓存存在
            Resource resource = JSONUtil.toBean(json,Resource.class);
            return ResModel.success(Code.SUCCESS,resource);
        }
        //缓存不存在,查询数据库
        Resource resource = getById(id);
        if (resource==null){
            return ResModel.error(Code.NON_EXISTENT);
        }
        //添加缓存
        redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(resource), Duration.ofMinutes(30));
        return ResModel.success(Code.SUCCESS,resource);
    }

	/**
     * 更新资源信息
     * @param resource
     * @return
     */
    public ResModel updateRes(Resource resource) {
        Integer id = resource.getId();
        if (id==null){
            return ResModel.error(Code.FAIL);
        }
        updateById(resource); 
        redisTemplate.delete(CACHE_RESOURCE_KEY+id);
        return ResModel.success(Code.UPDATE,resource);
    }

6.1.2 缓存穿透(Cache Penetration)

缓存穿透:缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库,在高并发的场景下,可能会直接打死数据库。

常见的解决方案有两种:

缓存空对象

  • 优点:实现简单,维护方便
  • 缺点:额外的内存消耗,可能造成短期的不一致

布隆过滤

  • 优点:内存占用较少,没有多余key
  • 缺点:实现复杂存在误判可能’

示例:

	/**
     * 查询资源详情
     * @param id
     * @return
     */
    @Override
    public ResModel<Resource> getResource(Integer id) {
        String key = CACHE_RESOURCE_KEY + id;
        //查询缓存
        String json = redisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(json)){//缓存存在
            Resource resource = JSONUtil.toBean(json,Resource.class);
            return ResModel.success(Code.SUCCESS,resource);
        }
        //命中空值
        if (json!=null){
            return ResModel.error(Code.NON_EXISTENT);
        }
        //缓存不存在,查询数据库
        Resource resource = getById(id);
        if (resource==null){
            // 缓存空值解决 缓存穿透,设置较短的过期时间
            redisTemplate.opsForValue().set(key,"",Duration.ofMinutes(2));
            return ResModel.error(Code.NON_EXISTENT);
        }
        //添加缓存
        redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(resource), Duration.ofMinutes(30));
        return ResModel.success(Code.SUCCESS,resource);
    }

6.1.3 缓存击穿(Hotspot Invalid)

缓存击穿,就是说某个 key 非常热点,访问非常频繁,处于集中式高并发访问的情况,当这个 key 在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。

不同场景下的解决方式可如下:

  • 若缓存的数据是基本不会发生更新的,则可尝试将该热点数据设置为永不过期
  • 若缓存的数据更新不频繁,且缓存刷新的整个流程耗时较少的情况下,则可以采用基于 Redis、zookeeper等分布式中间件的分布式互斥锁,或者本地互斥锁以保证仅少量的请求能请求数据库并重新构建缓存,其余线程则在锁释放后能访问到新缓存。
  • 若缓存的数据更新频繁或者在缓存刷新的流程耗时较长的情况下,可以利用定时线程在缓存过期前主动地重新构建缓存或者延后缓存的过期时间,以保证所有的请求能一直访问到对应的缓存。

互斥锁:

  • 利用 redis 的 setnx 方法来表示获取锁,该方法含义是redis中如果没有这个key,则插入成功,返回1,在stringRedisTemplate中返回true,
    如果有这个key则插入失败,则返回0,在stringRedisTemplate返回false,我们可以通过true,或者是false,来表示是否有线程成功插入key,成功插入的key的线程我们认为他就是获得到锁的线程。
	/**
     * 互斥锁解决 缓存击穿
     * @param id
     * @return
     */
    public ResModel<Resource> queryWithMutex(Integer id){
        String key = CACHE_RESOURCE_KEY + id;
        //查询缓存
        String json = redisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(json)){//缓存存在
            Resource resource = JSONUtil.toBean(json,Resource.class);
            return ResModel.success(Code.SUCCESS,resource);
        }
        //命中空值
        if (json!=null){
            return ResModel.error(Code.NON_EXISTENT);
        }
        String lockKey=Mutex_KEY+id;
        Resource resource = null;
        try {
            //尝试获得锁
            boolean b = tryLock(lockKey);
            //没有获得锁,重试
            if (!b){
                return queryWithMutex(id);
            }
            //缓存不存在,查询数据库
            resource = getById(id);
            if (resource==null){
                // 缓存空值解决 缓存穿透
                redisTemplate.opsForValue().set(key,"",Duration.ofMinutes(2));
                return ResModel.error(Code.NON_EXISTENT);
            }
            //添加缓存
            redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(resource), Duration.ofMinutes(30));
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            //释放锁
            unlock(lockKey);
        }
        return ResModel.success(Code.SUCCESS,resource);
    }

    /**
     * 获得锁
     * @param key
     * @return
     */
    private boolean tryLock(String key) {
        Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, "1", Duration.ofSeconds(10));
        return BooleanUtil.isTrue(flag);
    }

    /**
     * 释放锁
     * @param key
     */
    private void unlock(String key) {
        redisTemplate.delete(key);
    }

逻辑过期:

  • 思路分析:当用户开始查询redis时,判断是否命中,如果没有命中则直接返回空数据,不查询数据库,而一旦命中后,将value取出,判断value中的过期时间是否满足,如果没有过期,则直接返回redis中的数据,如果过期,则在开启独立线程后直接返回之前的数据,独立线程去重构数据,重构完成后释放互斥锁。
	 //创建线程池
    private static final ExecutorService THREAD_POOL= Executors.newSingleThreadExecutor();
	/**
     * 逻辑过期时间解决 缓存击穿
     * @param id
     * @return
     */
    public ResModel<Resource> queryWithLogicalExpire(Integer id) {
        String key = CACHE_RESOURCE_KEY + id;
        //查询缓存
        String json = redisTemplate.opsForValue().get(key);
        if (StrUtil.isBlank(json)){//缓存不存在
            return ResModel.error(Code.NON_EXISTENT);
        }
        //缓存存在
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        Resource resource = JSONUtil.toBean((JSONObject) redisData.getData(), Resource.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        if (expireTime.isAfter(LocalDateTime.now())){
            //未过期
            return ResModel.success(Code.SUCCESS,resource);
        }
        //过期
        String lockKey=Mutex_KEY+id;
        boolean b = tryLock(lockKey);
        if (b){
            THREAD_POOL.submit(()->{
               try {
                    Resource res = getById(id);
                    RedisData rd = new RedisData();
                    rd.setData(res);
                    //更新逻辑过期时间
                    rd.setExpireTime(LocalDateTime.now().plusMinutes(30));
                    //添加缓存
                    redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(rd));
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    //释放锁
                    unlock(key);
                }
            });
        }
        return ResModel.success(Code.SUCCESS,resource);
    }
     /**
     * 获得锁
     * @param key
     * @return
     */
    private boolean tryLock(String key) {
        Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, "1", Duration.ofSeconds(10));
        return BooleanUtil.isTrue(flag);
    }

    /**
     * 释放锁
     * @param key
     */
    private void unlock(String key) {
        redisTemplate.delete(key);
    }

6.1.3 缓存雪崩(Cache Avalanche)

缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

缓存雪崩的事前事中事后的解决方案如下:

  • 事前:Redis 高可用,主从+哨兵,Redis cluster,避免全盘崩溃。
  • 事中:本地 ehcache 缓存 + hystrix 限流&降级,避免 MySQL 被打死
  • 事后:Redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据

6.2 会话存储

基于Redis实现分布式Session,基于缓存,不详细描述。

在这里插入代码片

6.3 发布/订阅系统

一般使用MQ来进行发布订阅,这里不详细描述。

参考:MQ;Redis实现发布订阅

6.4 排行榜/计数器

redis sorts sets实现排行榜

    /**
     * 单个新增
     */
    @Test
    public void add() {
        redisTemplate.opsForZSet().add(SCORE_RANK, "李四", 8899);
    }

    /**
     * 获取排行列表
     */
    @Test
    public void list() {

        Set<String> range = redisTemplate.opsForZSet().reverseRange(SCORE_RANK, 0, 10);
        System.out.println("获取到的排行列表:" + JSON.toJSONString(range));
        Set<ZSetOperations.TypedTuple<String>> rangeWithScores = redisTemplate.opsForZSet().reverseRangeWithScores(SCORE_RANK, 0, 10);
        System.out.println("获取到的排行和分数列表:" + JSON.toJSONString(rangeWithScores));
    }

redis的increment()方法实现计数器功能

    @Override
    public Long incr(String key, long delta) {
        return redisTemplate.opsForValue().increment(key, delta);
    }

6.5 实时数据分析

6.6 分布式锁

分布式锁一般有三种实现方式:

  1. 数据库乐观锁;
  2. 基于Redis的分布式锁;
  3. 基于ZooKeeper的分布式锁。

这里介绍第二种方式,基于Redis实现分布式锁。

分布式锁的特性:

  • 互斥性。在任意时刻,只有一个客户端能持有锁。
  • 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
  • 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
  • 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

6.7 地理位置服务

总结

其实,我对你是有一些失望的。当初给你定级P7,是高于你面试时的水平的。我是希望进来后,你能够拼一把,快速成长起来的。这个层级,不是把事情做好就可以的。你需要有体系化思考的能力。你做的事情,他的价值点在哪里?你是否作出了壁垒,形成了核心竞争力?你做的事情,和公司内其他团队的差异化在哪里?你的事情,是否沉淀了一套可复用的物理资料和方法论?为什么是你来做,其他人不能做吗?你需要有自己的判断力,而不是我说什么你就做什么。后续,把你的思考沉淀到日报周报月报里,我希望看到你的思考,而不仅仅是进度。另外,提醒一下,你的产出,和同层级比,是有些单薄的,马上要到年底了,加把劲儿。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值