系列文章目录
文章目录
系列文章目录前言一、Redis简介1.1 为什么需要Redis1.1.1 高性能1.1.2 高并发 1.2 Redis的应用场景 二、Redis 下载安装2.1 window 系统使用Redis2.2 Linux 系统使用Redis2.2.1 部署docker2.2.2 拉取镜像2.2.3 创建挂载路径2.2.4 启动docker 容器 三、Redis 数据结构与使用3.1 字符串(String)3.2 哈希表(Hash)3.3 列表(List)3.4 集合(Set)3.5 有序集合(Sorted Set)3.6 位图(Bitmap)3.7 超级日志(HyperLogLog)3.8 存储地理位置数据 四、Redis 底层实现机制4.1 Redis 数据结构与其底层实现4.1.1 动态字符串(SDS)4.1.2 哈希表(Hash)4.1.3 列表(List)4.1.4 集合(Set)4.1.5 有序集合(Sorted Set) 4.2 Redis IO模型4.2.1 套接字(SOCKET)4.2.2 非阻塞I/O模型 4.3 Redis 持久化4.3.1 RDB(Redis数据库文件)持久化4.3.1 AOF(Redis日志文件)持久化 4.4 Redis 哨兵与集群 五、Spring 整合 Redis5.1 引入依赖与配置5.1.1 添加依赖5.1.2 配置Redis连接 5.2 访问 Redis5.2.1 redis 序列化配置5.2.2 Redis 工具类 六、Redis 应用6.1 缓存6.2 会话存储6.3 发布/订阅系统6.4 排行榜/计数器6.5 实时数据分析6.6 分布式锁6.7 地理位置服务 总结前言
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/datamkdir -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.1protected-mode noport 6379tcp-backlog 511requirepass 000415timeout 0tcp-keepalive 300daemonize nosupervised nopidfile /var/run/redis_6379.pidloglevel noticelogfile ""databases 30always-show-logo yessave 900 1save 300 10save 60 10000stop-writes-on-bgsave-error yesrdbcompression yesrdbchecksum yesdbfilename dump.rdbdir ./replica-serve-stale-data yesreplica-read-only yesrepl-diskless-sync norepl-disable-tcp-nodelay noreplica-priority 100lazyfree-lazy-eviction nolazyfree-lazy-expire nolazyfree-lazy-server-del noreplica-lazy-flush noappendonly yesappendfilename "appendonly.aof"no-appendfsync-on-rewrite noauto-aof-rewrite-percentage 100auto-aof-rewrite-min-size 64mbaof-load-truncated yesaof-use-rdb-preamble yeslua-time-limit 5000slowlog-max-len 128notify-keyspace-events ""hash-max-ziplist-entries 512hash-max-ziplist-value 64list-max-ziplist-size -2list-compress-depth 0set-max-intset-entries 512zset-max-ziplist-entries 128zset-max-ziplist-value 64hll-sparse-max-bytes 3000stream-node-max-bytes 4096stream-node-max-entries 100activerehashing yeshz 10dynamic-hz yesaof-rewrite-incremental-fsync yesrdb-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文件:
手动执行save命令触发方式:该命令会阻塞当前Redis服务器,执行save命令期间,Redis不能处理其他命令,直到RDB过程完成为止。手动执行bgsave命令触发方式:执行该命令时,Redis会在后台异步进行快照操作,快照同时还可以响应客户端请求。阻塞只发生在fork阶段,一般时间很短。基本上 Redis 内部所有的RDB操作都是采用 bgsave 命令。修改redis.conf配置文件自动备份:save m n m
:表示m秒内数据集存在n次修改时,自动触发bgsave。
RDB优势:
RDB文件紧凑,全量备份,非常适合用于进行备份和灾难恢复。生成RDB文件的时候,redis主进程会fork()一个子进程来处理所有保存工作,主进程不需要进行任何磁盘IO操作。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
RDB劣势:
RDB快照是一次全量备份,存储的是内存数据的二进制序列化形式,存储上非常紧凑。当进行快照持久化时,会开启一个子进程专门负责快照持久化,子进程会拥有父进程的内存数据,父进程修改内存子进程不会反应出来,所以在快照持久化期间修改的数据不会被保存,可能丢失数据。
4.3.1 AOF(Redis日志文件)持久化
AOF持久化将Redis的每个写操作记录为一个追加日志的方式。
AOF文件包含了将Redis从初始状态恢复到当前状态所需的所有写命令。可以通过配置Redis来控制AOF文件的刷新频率和文件的压缩策略。在Redis重启时,会读取AOF文件中的日志,并将日志重新执行一次,恢复数据到内存中。
4.4 Redis 哨兵与集群
五、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: 0lettuce:pool:min-idle: 10max-idle: 100max-active: 100max-wait: 10000
全部配置详解
# redis 配置项# 连接URL,配置后会覆盖host、port等配置,eg: redis://user:password@:6379spring.redis.url=# 连接地址spring.redis.host=127.0.0.1# Redis服务器连接端口spring.redis.port=6379# 连接工厂使用的数据库索引(0-15),默认为0spring.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# 连接池配置# 连接池池中“空闲”连接的最大数量。使用负值表示无限数量的空闲连接,默认为8spring.redis.lettuce.pool.max-idle=8# 中维护的最小空闲连接数,默认为0spring.redis.lettuce.pool.min-idle=0# 连接池可以分配的最大连接数。使用负值表示没有限制,默认为8spring.redis.lettuce.pool.max-active=8# 当连接池耗尽时,在抛出异常之前,连接分配应阻塞的最长时间。使用负值无限期等待,默认为-1spring.redis.lettuce.pool.max-wait=-1# 空闲连接从运行到退出的时间间隔。当为正时,空闲连接回收线程启动,否则不执行空闲连接回收spring.redis.lettuce.pool.time-between-eviction-runs=# 宕机超时时间,默认100msspring.redis.lettuce.shutdown-timeout=100
5.2 访问 Redis
5.2.1 redis 序列化配置
@Configurationpublic class RedisConfig {@Beanpublic 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 管理,就不需要自己实现单例模式。@Componentpublic class RedisUtil {@Resourceprivate 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.2 会话存储
6.3 发布/订阅系统
6.4 排行榜/计数器
6.5 实时数据分析
6.6 分布式锁
6.7 地理位置服务
总结
其实,我对你是有一些失望的。当初给你定级P7,是高于你面试时的水平的。我是希望进来后,你能够拼一把,快速成长起来的。这个层级,不是把事情做好就可以的。你需要有体系化思考的能力。你做的事情,他的价值点在哪里?你是否作出了壁垒,形成了核心竞争力?你做的事情,和公司内其他团队的差异化在哪里?你的事情,是否沉淀了一套可复用的物理资料和方法论?为什么是你来做,其他人不能做吗?你需要有自己的判断力,而不是我说什么你就做什么。后续,把你的思考沉淀到日报周报月报里,我希望看到你的思考,而不仅仅是进度。另外,提醒一下,你的产出,和同层级比,是有些单薄的,马上要到年底了,加把劲儿。