本文使用「署名 4.0 国际 (CC BY 4.0)」许可协议,欢迎转载、或重新修改使用,但需要注明来源。 [署名 4.0 国际 (CC BY 4.0)](https://creativecommons.org/licenses/by/4.0/deed.zh) 本文作者: 苏洋 创建时间: 2024年11月30日 统计字数: 18260字 阅读时间: 37分钟阅读 本文链接: https://soulteary.com/2024/11/30/basic-skills-redis-from-getting-started-to-docker-deployment.html ----- # 折腾基本功:Redis 从入门到 Docker 部署 前面写过了两篇 “Redis” 相关的内容,今天补一篇“基本功”内容,让后续折腾系列文章可以篇幅更短、内容更专注。 ## 前言 在日常工作中,我们构建应用时总是离不开一些基础组件,Redis 就是其中特别常用的一个。之前我写过不少文章,通常会把多个组件放在一起讲,介绍它们是如何协同工作的。随着目前积累了上千篇内容后,这样的内容并不适合维护、也不方便大家查阅和学习。比如你今天只想看看 Redis 怎么用,却要在文章里翻来翻去找 Redis 相关的部分。 所以这次,我想换个思路,把这些基础组件分开来讲,一个一个地仔细说说。今天,我们就先聊聊 Redis。 为什么选 Redis 作为开头呢?因为它实在是太常用了。无论是做网站、做 App,还是做企业内部系统,都少不了它。缓存数据、管理用户会话、计数排行、消息队列...Redis 就像个百宝箱,总能帮我们解决各种各样的问题。 本文我们主要会聊四块内容: - Redis 的基础使用:常用的数据类型和命令使用 - Redis 的实际使用:常用业务场景中的具体使用 - Redis 的容器化使用(Docker 部署细节):靠谱的 Redis 环境 - Redis 的最佳实践:一些基础建议 本文使用的是 2024 年下半年的最新版本 Redis 7.4.1。另外提一句,Redis 8.0 也快要来了,会带来不少令人兴奋的原本在企业版本中的新特性。比如内置的搜索功能、JSON 支持、时间序列数据处理等等。 好,话不多说,让我们先从基础开始吧! ## 一、Redis 简介 [Redis(Remote Dictionary Server)](https://github.com/redis/redis)是一个开源的内存数据结构存储系统,它可以用作数据库、缓存、消息代理和队列。我们可以将 Redis 内置的 [丰富的数据类型](https://redis.io/docs/latest/develop/data-types/)分为核心数据类型和扩展数据类型两大类。 ### 1. 核心数据类型 **[字符串(String)](https://redis.io/docs/latest/develop/data-types/strings/)**:字符串是 Redis 最基础的数据类型。它不仅可以存储普通的文本,还可以存储数字、序列化的对象(二进制)等。单个字符串最大支持 512MB。 ```bash # 基本的设置和获取 # 设置键值对 SET user:1 "李逍遥" # 获取值 GET user:1 # 返回: "李逍遥" # 常用于缓存场景 # 设置过期时间(秒) SETEX session:token123 3600 "user_session_data" # 3600秒后过期 # 原子递增/递减(适合计数场景) SET article:1:views 0 INCR article:1:views # 返回: 1 INCRBY article:1:views 10 # 返回: 11 ``` 多数字符串操作复杂度都是 O(1),`SUBSTR`、`GETRANGE`、`SETRANGE` 复杂度可能是 O(n)。 **[列表(List)](https://redis.io/docs/latest/develop/data-types/lists/)**:是按插入顺序排序的字符串列表。可以用来实现队列、栈或者最新动态这类场景,比如常用于消息队列场景。 ```bash # 添加元素 LPUSH notifications:user1 "你有一条新消息" # 从左侧添加 RPUSH notifications:user1 "系统维护通知" # 从右侧添加 # 获取列表内容 LRANGE notifications:user1 0 -1 # 获取所有元素 LRANGE notifications:user1 0 4 # 获取前5条消息 # 列表长度 LLEN notifications:user1 # 移除元素 LPOP notifications:user1 # 从左侧移除并返回 BLPOP notifications:user1 5 # 阻塞式弹出,最多等待5秒 # 列表修剪(保持固定长度) LTRIM notifications:user1 0 99 # 只保留最新的100条消息 ``` 除了访问头部或尾部的列表复杂度是 O(1),其他的操作复杂度通常为 O(n) **[集合(Set)](https://redis.io/docs/latest/develop/data-types/sets/)**:是无序的唯一字符串集合。适合用于标签系统、关注关系等场景,可以高效地实现交集、并集操作。 ```bash # 添加元素 SADD user:1:interests "编程" "读书" "音乐" # 删除元素 SREM user:1:interests "音乐" # 判断元素是否存在 SISMEMBER user:1:interests "编程" # 返回1表示存在 # 获取集合元素 SMEMBERS user:1:interests # 获取所有元素 SCARD user:1:interests # 获取元素数量 # 集合运算 SADD user:2:interests "编程" "游戏" "电影" SINTER user:1:interests user:2:interests # 共同兴趣(交集) SUNION user:1:interests user:2:interests # 所有兴趣(并集) SDIFF user:1:interests user:2:interests # 独有兴趣(差集) ``` 集合最大大小为 2^32-1 (~40亿),除了 `SMEMBERS` 是 O(n) 复杂度外,基本都是 O(1)。 **[哈希(Hash)](https://redis.io/docs/latest/develop/data-types/hashes/)**:哈希表是“字段(Key)-值(Value)对”的集合,类似于 Python 字典或 Java HashMap。特别适合用来存储对象数据,比如用户信息、商品信息等。 ```bash # 设置单个字段 HSET user:1 name "李逍遥" age "25" city "临安" # 获取字段值 HGET user:1 name # 获取单个字段 HMGET user:1 name age # 获取多个字段 HGETALL user:1 # 获取所有字段和值 # 检查字段是否存在 HEXISTS user:1 phone # 检查是否设置了手机号 # 递增数字字段 HINCRBY user:1 login_count 1 # 登录次数加1 # 删除字段 HDEL user:1 temporary_field # 获取所有字段名或字段值 HKEYS user:1 # 获取所有字段名 HVALS user:1 # 获取所有字段值 ``` 每个哈希最大大小为 2^32-1 (~40亿),多数操作都是 O(1)。 **[有序集合(Sorted Set)](https://redis.io/docs/latest/develop/data-types/sorted-sets/)**:每个成员关联一个分数的有序集合,成员唯一,分数可以重复。常用于排行榜、优先级队列等需要排序的场景。 ```bash # 添加带分数的元素 ZADD leaderboard 89.5 "player1" 95.2 "player2" 78.3 "player3" # 获取排名 ZRANK leaderboard "player1" # 从低到高排名(0开始) ZREVRANK leaderboard "player1" # 从高到低排名 # 获取分数 ZSCORE leaderboard "player1" # 获取排名范围 ZRANGE leaderboard 0 2 WITHSCORES # 获取前3名(升序) ZREVRANGE leaderboard 0 2 WITHSCORES # 获取前3名(降序) # 按分数范围获取 ZRANGEBYSCORE leaderboard 80 100 WITHSCORES # 获取80-100分的玩家 # 增加分数 ZINCRBY leaderboard 5.0 "player1" # 增加5分 ``` 大多数排序集合复杂度为 O(log(n) + m),n 是获取成员数量,m 是返回数据数量。 **[流(Stream)](https://redis.io/docs/latest/develop/data-types/streams/)**:Stream 是 Redis 5.0 引入的数据类型,是追加式日志数据结构,用于记录和处理时序事件,支持消费者组模式。适合用于消息队列、事件流等场景。 ```bash # 添加消息 XADD events * type "login" user_id "123" # * 表示自动生成ID # 读取消息 XREAD COUNT 2 STREAMS events 0 # 读取最早的2条消息 # 创建消费者组 XGROUP CREATE events group1 $ # $ 表示从最新的消息开始 # 消费者组读取 XREADGROUP GROUP group1 consumer1 COUNT 1 STREAMS events > # > 表示未被消费的消息 ``` **[地理空间索引(Geospatial)](https://redis.io/docs/latest/develop/data-types/geospatial/)**:支持存储地理位置信息,并提供经纬度距离计算、范围查询等功能,特别适合在位置服务中使用。 ```bash # 添加地理位置 # 语法:GEOADD key longitude latitude member GEOADD locations 116.397428 39.909734 "中国科学院" GEOADD locations 116.336956 39.986119 "清华大学" GEOADD locations 116.310003 39.991957 "北京大学" # 获取坐标 GEOPOS locations "中国科学院" # 返回经纬度 # 计算两点间距离 # 单位可以是 m(米)、km(千米)、mi(英里)、ft(英尺) GEODIST locations "中国科学院" "清华大学" km # 返回距离,单位千米 # 获取指定范围内的位置 # GEORADIUS 以给定经纬度为中心查询 # GEORADIUSBYMEMBER 以已存在的位置为中心查询 GEORADIUS locations 116.397428 39.909734 5 km WITHCOORD WITHDIST GEORADIUSBYMEMBER locations "中国科学院" 5 km WITHCOORD WITHDIST # 获取 GEOHASH 编码 GEOHASH locations "中国科学院" # 实际应用示例:寻找附近的商家 # 添加多个商家位置 GEOADD shops 116.397428 39.909734 "shop:1" GEOADD shops 116.396728 39.910734 "shop:2" GEOADD shops 116.398428 39.908734 "shop:3" # 查找某个位置 3km 范围内的商家,并返回距离和坐标 GEORADIUS shops 116.397428 39.909734 3 km WITHCOORD WITHDIST WITHHASH ASC # 获取指定商家到其他商家的距离 GEORADIUSBYMEMBER shops "shop:1" 1 km WITHDIST ``` **[位图(Bitmap)](https://redis.io/docs/latest/develop/data-types/bitmaps/)**:支持对字符串进行位操作 ,适合高效存储布尔信息。适合用于记录状态、统计数据、用户行为统计等场景,可以大大节省内存。 ```bash # 设置位图的某一位 SETBIT user:login:2024-01-01 123 1 # 记录用户ID为123的登录状态 # 获取某一位的值 GETBIT user:login:2024-01-01 123 # 统计为1的位数 BITCOUNT user:login:2024-01-01 # 统计当天登录用户数 # 位操作 BITOP AND result user:login:2024-01-01 user:login:2024-01-02 # 连续两天都登录的用户 ``` `SETBIT` 和 `GETBIT` 都是 O(1), `BITOP` 是 O(n),其中 n 是比较中最长的字符串的长度。 **[位域(Bitfield)](https://redis.io/docs/latest/develop/data-types/bitfields/)**:可在字符串中高效编码多个计数器 ,支持原子性的获取、设置和递增操作 ,提供多种溢出处理策略。简单来说,我们可以在 Bitfield 中存储多个整数值,每个整数可以指定不同的位宽,非常适合存储大量小整数或计数器。 ```bash # 设置一个 8 位无符号整数(u8),偏移量为 0 BITFIELD counters SET u8 0 100 # 在位置0设置一个值为100的8位无符号整数 # 设置多个值 BITFIELD counters SET u8 0 100 SET u16 8 300 # 设置一个8位和一个16位整数 # 获取值 BITFIELD counters GET u8 0 # 获取第一个8位整数 BITFIELD counters GET u16 8 # 获取16位整数 # 递增操作(有溢出控制) # 溢出策略:WRAP(回绕)、SAT(饱和)、FAIL(失败) BITFIELD counters OVERFLOW SAT INCRBY u8 0 1 # 增加1,使用饱和溢出控制 # 一次性获取多个值 BITFIELD counters GET u8 0 GET u16 8 # 实际应用示例:记录每小时的计数器 # 假设要记录用户每小时的登录次数,使用 6 位整数(最大值63) # 每个小时占用 6 位,一天 24 个小时总共需要 144 位 BITFIELD daily:logins:user123 SET u6 0 1 # 0点登录1次 BITFIELD daily:logins:user123 INCRBY u6 6 1 # 1点登录次数+1 BITFIELD daily:logins:user123 GET u6 12 # 获取2点的登录次数 ``` `BITFIELD` 是 O(n),其中 n 是访问的计数器的数量。 ### 2. 扩展数据类型(Redis Stack / Enterprise) 在 Redis 8.0 社区版本到来之前([8.0-M02 社区预览版包含这些模块](https://github.com/redis/redis/releases/tag/8.0-m02)),Redis Stack 和 Redis Enterprise 版本还提供了一些高级数据类型: **[JSON](https://redis.io/docs/latest/develop/data-types/json/)** 1. 支持结构化数据存储 2. 可直接操作 JSON 文档 3. 支持复杂的查询操作 4. 传递给命令的 JSON 值的深度限制最大为 128。 **[概率数据类型](https://redis.io/docs/latest/develop/data-types/probabilistic/)** 1. HyperLogLog:基数估算 2. Bloom filter:成员检测 3. Cuckoo filter:可删除元素的成员检测 4. t-digest:百分位数估算 5. Top-K:排名估算 6. Count-min sketch:频率估算 **[时间序列](https://redis.io/docs/latest/develop/data-types/timeseries/)** 1. 针对时间序列数据优化 2. 支持高效的时间范围查询 3. 适合监控和分析应用 因为 8.0 还没有正式发布,程序或许还有一些改进或变动的空间,所以我不计划在这篇文章中展开这三个类型的细节。 或许在后面的文章里,我会单独聊聊编译这些组件,以及实战的内容,就像是在之前使用 Redis 作为向量数据库使用一样:《[使用 Redis 构建轻量的向量数据库应用:图片搜索引擎(一)](https://soulteary.com/2023/11/15/use-redis-to-build-a-lightweight-vector-database-application-image-search-engine-part-1.html)》、《[使用 Redis 构建轻量的向量数据库应用:图片搜索引擎(二)](https://soulteary.com/2023/11/16/use-redis-to-build-a-lightweight-vector-database-application-image-search-engine-part-2.html)》。 ## 二、Redis 实用场景 在介绍“核心数据类型”小节中,我提到了一些使用 Redis 的经典操作。就使用频率来说,最常见的场景还是**缓存层**(减轻数据库负载、提高响应速度、存储临时会话数据)、**计数器**(文章阅读量统计、用户点赞数、商品库存管理)、**消息队列**(任务排队、消息传递、事件处理)。 接下来我们来基于具体业务场景,再展开聊聊。 ### 统一的登录态管理 又快到一年结束了,大家或许还有一些年假没有休,当我们提交年假的时候,可能需要先登录某个 OA 系统,然后再从 OA 系统跳转人力系统,然后提交年假使用申请,接下来就是等待年假审批通过,哦耶。 这个场景下,我们可能使用了多个系统,但是我们并不需要在系统间重复登录,因为系统会自动识别用户身份。这周无缝的用户体验,就可以使用 Redis 来实现。 具体实现方式是:当用户首次登录时,系统生成一个 Session ID,并将用户信息存储在 Redis 中。示例代码如下: ```python # 用户登录时 session_id = generate_unique_id() user_data = { "user_id": "employee_001", "name": "小李", "department": "技术部", "permissions": ["oa_access", "hr_access"] } # 设置 session 数据,有效期 2 小时 redis_client.setex(f"session:{session_id}", 7200, json.dumps(user_data)) ``` 当用户访问其他系统时,这些系统可以通过共同的 Session ID 从 Redis 中获取用户信息,从而实现统一登录: ```python # 其他系统验证用户身份时 user_data = redis_client.get(f"session:{session_id}") if user_data: user = json.loads(user_data) # 验证权限并允许访问 ``` ### 高并发场景下的缓存应用 每当有热门活动或者有大量集中访问高峰的时候,有一些系统就会变得很卡,尤其是需要大量数据库关联查询的场景。 比如每个月月初,大家集中向系统中查询上个月考勤是否有异常的时候,毕竟错过了补打卡还是挺麻烦的。这时,Redis 的缓存功能就派上了用场。 比如,对于考勤数据的缓存策略可以是这样的: ```python def get_attendance_report(employee_id, month): # 先尝试从 Redis 获取缓存的报告 cache_key = f"attendance:{employee_id}:{month}" report = redis_client.get(cache_key) if report: return json.loads(report) # 缓存未命中,从数据库计算报告 report = calculate_attendance_from_db(employee_id, month) # 将结果缓存到 Redis,设置适当的过期时间 redis_client.setex(cache_key, 3600, json.dumps(report)) return report ``` ### 实时数据统计与分析 日常工作中,或许有的同学折腾过项目上线后的实时监控系统,来关注项目的各项指标,比如接口响应时间、用户访问量、错误率等。这些需要实时更新的计数器和统计数据,用 Redis 来处理再合适不过。 ```python # 记录接口调用次数 redis_client.incr(f"api:calls:{api_name}") # 记录接口响应时间 redis_client.lpush(f"api:response_times:{api_name}", response_time) redis_client.ltrim(f"api:response_times:{api_name}", 0, 999) # 只保留最近 1000 条 # 使用 HyperLogLog 统计独立用户数 # https://redis.io/docs/latest/develop/data-types/probabilistic/hyperloglogs/ redis_client.pfadd(f"api:unique_users:{api_name}", user_id) ``` ### 任务队列与消息传递 现在的 OA 系统中,基本都具备审批功能。当用户提交了一个需要多人审批的任务后,系统需要自动发送邮件或通知,来通知下一个审批人。这种异步任务就可以通过 Redis 的列表结构来实现一个简单的任务队列: ```python # 添加审批任务到队列 task = { "type": "approval_notification", "document_id": "doc_001", "approver": "manager_001", "deadline": "2024-12-01" } redis_client.lpush("approval_tasks", json.dumps(task)) # 消费者处理任务 while True: # 获取并处理任务 task = redis_client.brpop("approval_tasks") if task: task_data = json.loads(task[1]) send_approval_notification(task_data) ``` ### 限流与热点防护 假设我们的系统中,支持用户使用手机验证码来进行登录。为了避免短信发送过频繁,对用户造成骚扰,对平台成本造成浪费。通常我们可以使用 Redis 实现简单的访问频率限制: ```python def can_send(user_id): key = f"send_limit:{user_id}" # 获取用户最近的发送次数 count = redis_client.get(key) if not count: # 第一次发送,设置计数器 redis_client.setex(key, 3600, 1) return True count = int(count) if count > 10: # 每小时最多发送 10 次 return False # 增加计数器 redis_client.incr(key) return True ``` ### 实时排行榜 不少公司都有内部的学习平台,有不少都会展示内部课程的热度排行榜,包括最近一周最受欢迎的课程、学习时长最多的员工等。这种实时更新的排序场景,正好可以利用 Redis 的有序集合来实现: ```python # 记录课程学习人次 def record_course_view(course_id, user_id): # 课程热度加一 redis_client.zincrby("hot_courses:weekly", 1, course_id) # 记录用户学习该课程的时间戳 redis_client.zadd(f"course:viewers:{course_id}", {user_id: time.time()}) # 获取热门课程排行 def get_hot_courses(limit=10): return redis_client.zrevrange("hot_courses:weekly", 0, limit-1, withscores=True) # 记录学习时长 def record_learning_time(user_id, minutes): # 累计学习时长 redis_client.zincrby("learner:leaderboard", minutes, user_id) # 同时更新当日学习时长 today = datetime.now().strftime("%Y%m%d") redis_client.zincrby(f"learner:daily:{today}", minutes, user_id) ``` ### 地理位置服务 聊一个可能不少同学有体感的事情,上下班的时候需要掏出手机定位,打卡。当你超出公司办公室一定距离的时候,打卡是打不上的。这类“判断/查找空间点坐标距离”的需求,可以使用 Redis 的地理空间索引特性: ```python # 添加公司位置信息 def add_company_position(room_id, latitude, longitude): redis_client.geoadd("company", longitude, latitude, room_id) # 检查公司是否在 200 米范围内 def check_nearby_company(latitude, longitude, radius=200): # 返回 200 米范围内的公司坐标,并附带距离信息 return redis_client.georadius( "company", longitude, latitude, radius, unit="m", withcoord=True, withdist=True, sort="ASC" ) ``` ### 智能表单与数据校验 有时候在做表单功能的时候,需要进行字段的实时验证,比如检查提交的项目编号是否唯一、员工编号是否有效等。使用 Redis 的集合可以高效地实现这类验证: ```python # 初始化有效的员工编号集合 def init_employee_ids(): employee_ids = fetch_valid_employee_ids_from_db() redis_client.delete("valid:employee_ids") redis_client.sadd("valid:employee_ids", *employee_ids) # 验证员工编号 def is_valid_employee(employee_id): return redis_client.sismember("valid:employee_ids", employee_id) # 项目编号查重 def check_project_code(code): # 使用位图标记已使用的项目编号 key = f"project:codes:{datetime.now().year}" # 将项目编号哈希到一个整数 code_hash = hash(code) % (10 ** 6) exists = redis_client.getbit(key, code_hash) if not exists: redis_client.setbit(key, code_hash, 1) return not exists ``` ### 接口限流与熔断 在一些场景中,我们需要进行一些流量控制。Redis 可以实现简单而有效的限流器: ```python class RateLimiter: def __init__(self, redis_client, service_name, max_requests, time_window): self.redis = redis_client self.service = service_name self.max_requests = max_requests self.time_window = time_window def is_allowed(self, api_name): key = f"ratelimit:{self.service}:{api_name}" current = self.redis.get(key) if not current: pipeline = self.redis.pipeline() pipeline.set(key, 1) pipeline.expire(key, self.time_window) pipeline.execute() return True current = int(current) if current >= self.max_requests: return False self.redis.incr(key) return True # 使用示例 rate_limiter = RateLimiter(redis_client, "user_service", 100, 60) if rate_limiter.is_allowed("get_user_info"): # 处理请求 pass else: # 触发限流处理 pass ``` ### 分布式锁实现 当我们的业务系统中出现订单处理的场景时,尤其是涉及到实体货物、票等资源等时候,需要确保不能超售、避免用户重复提交。可以使用 Redis 来实现一个简单的(分布式)“锁”: ```python class RedisLock: def __init__(self, redis_client, lock_name, expire_seconds=10): self.redis = redis_client self.lock_name = f"lock:{lock_name}" self.expire_seconds = expire_seconds def acquire(self): # 使用 setnx 实现加锁 lock_value = str(uuid.uuid4()) acquired = self.redis.set( self.lock_name, lock_value, ex=self.expire_seconds, nx=True ) if acquired: # 记录锁的值,用于安全释放 self.lock_value = lock_value return True return False def release(self): # 使用 Lua 脚本确保原子性释放 script = """ if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end """ self.redis.eval(script, 1, self.lock_name, self.lock_value) # 使用示例 def process_order(order_id): lock = RedisLock(redis_client, f"order:{order_id}") if lock.acquire(): try: # 处理订单 pass finally: lock.release() ``` ## 三、使用 Docker 运行 Redis Redis 官方提供了[社区版本的容器镜像](https://hub.docker.com/_/redis),包含多种 CPU 架构。 ### 1. 基础 Docker 命令 官方文档中的命令使用非常简单: ```python # 启动一个服务名称为 some-redis 的 Redis 容器 docker run --name some-redis -d redis # 启用一个每 60 秒保存数据的,具备数据持久化的 Redis 容器 docker run --name some-redis -d redis redis-server --save 60 1 --loglevel warning # 使用 Redis CLI 来链接 Redis 服务 docker run -it --network some-network --rm redis redis-cli -h some-redis ``` 在 Docker 进入 27 版本后,不同系统的 Docker 默认行为发生了一些变化(比如是否会自动下载命令中未下载的镜像),所以我们从头开始: ```bash # 拉取 Redis 官方最新镜像 docker pull redis:latest # 日常使用更推荐固定版本的 Redis docker pull redis:7.4.1-alpine3.20 # 启动 Redis 容器,将容器内的默认端口和宿主机端口打通 docker run --name my-redis -d -p 6379:6379 redis:7.4.1-alpine3.20 # 进入 Redis 容器,使用 Redis CLI 进入交互式终端 docker exec -it my-redis redis-cli ``` 接下来,就可以使用上文中的命令进行练手或者实际业务啦。 ### 2. 使用 Docker Compose 当然,为了方便管理,命令式的使用,显然没有声明式的配置来的靠谱,创建一个 `docker-compose.yml` 文件: ```yaml name: cache services: redis: image: redis:7.4.1-alpine3.20 container_name: my-redis ports: - "6379:6379" ``` 这份配置文件实现了上面命令相同的功能,启动服务还是使用熟悉的命令即可: ```bash docker-compose up -d ``` ### 3. Redis 的数据持久化 Redis 提供两种持久化方式: - **RDB(Redis Database)**:按指定时间间隔执行数据快照,适合备份场景。 - **AOF(Append Only File)**:记录所有写操作,提供更好的持久化保证。 而在 Docker 中基础的数据持久化也分为两种,一种是 Docker Volume,一种是文件挂载到磁盘。 使用 Docker Volume 进行 Redis 数据持久化: ```yaml name: cache services: redis: image: redis:7.4.1-alpine3.20 container_name: my-redis ports: - "6379:6379" volumes: - redis-data:/data volumes: redis-data: ``` 服务启动后 Redis 的数据将被存储在 Docker Volume 中,我们可以使用下面的命令查看 Volume 的细节: ```bash # 获取 Redis 的数据 Volume # docker volume ls | grep redis local cache_redis-data # 使用 Inspect 命令查看具体信息 # docker volume inspect cache_redis-data [ { "CreatedAt": "2024-11-30T17:39:36+08:00", "Driver": "local", "Labels": { "com.docker.compose.project": "cache", "com.docker.compose.version": "2.29.7", "com.docker.compose.volume": "redis-data" }, "Mountpoint": "/var/lib/docker/volumes/cache_redis-data/_data", "Name": "cache_redis-data", "Options": null, "Scope": "local" } ] ``` 如果我们希望数据直接挂载到宿主机磁盘中,只需要做一些简单的改动: ```yaml name: cache services: redis: image: redis:7.4.1-alpine3.20 container_name: my-redis ports: - "6379:6379" volumes: - ./redis-data:/data ``` 当服务运行后,我们就能够在当前目录的 `redis-data` 中看到 Redis 的数据啦。 如果我们希望使用 AOF 方式来替换 RDB 快照模式来存储数据(牺牲数据恢复速度,但是具备最好的数据可恢复性),可以继续修改配置: ```yaml name: cache services: redis: image: redis:7.4.1-alpine3.20 container_name: my-redis command: redis-server --appendonly yes ports: - "6379:6379" volumes: - ./redis-data:/data ``` ### 4. Redis 服务自愈 如果我们希望服务能够在遇到异常自动恢复,并且不时的进行自检,判断是否需要进行服务自动恢复: ```yaml name: cache services: redis: image: redis:7.4.1-alpine3.20 container_name: my-redis ports: - "6379:6379" volumes: - ./redis-data:/data command: redis-server --appendonly yes restart: always healthcheck: test: ["CMD", "redis-cli","ping"] interval: 1s timeout: 3s retries: 5 ``` 除了进行服务自愈之外,还能够在我们使用 `docker ps` 检查服务状态的时候,提供更直观的状态展示(`healthy`): ```bash # docker compose ps NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS my-redis redis:7.4.1-alpine3.20 "docker-entrypoint.s…" redis 2 seconds ago Up 2 seconds (healthy) 0.0.0.0:6379->6379/tcp, [::]:6379->6379/tcp ``` 很多时候,为了节约资源,我们会选择多个服务部署在同一台机器,Redis 相比较一些后起之秀,不支持将数据持久化到磁盘。随着数据增长,内存占用也会变大,为了避免无限制增长影响其他的业务,我们可以设置一个资源限制: ```yaml name: cache services: redis: image: redis:7.4.1-alpine3.20 container_name: my-redis ports: - "6379:6379" volumes: - ./redis-data:/data command: redis-server --appendonly yes deploy: resources: limits: memory: 10G # 限制内存使用 restart: always healthcheck: test: ["CMD", "redis-cli","ping"] interval: 1s timeout: 3s retries: 5 ``` ### 5. Redis 和宿主机时间共享 有一些时候,我们会希望 Redis 和宿主机的时间保持一致,那么这个时候可以指定容器中的时区,并共享本机的时间和容器的时间: ```yaml name: cache services: redis: image: redis:7.4.1-alpine3.20 container_name: my-redis ports: - "6379:6379" environment: - TZ=Asia/Shanghai # 设置时区 volumes: # 共享时间 - /etc/localtime:/etc/localtime:ro - ./redis-data:/data:rw command: redis-server --appendonly yes deploy: resources: limits: memory: 10G restart: always healthcheck: test: ["CMD", "redis-cli","ping"] interval: 1s timeout: 3s retries: 5 ``` ### 6. Redis 配置细节调整 当我们面向特别的场景的时候,很多时候会基于 [Redis 配置](https://redis.io/docs/latest/operate/oss_and_stack/management/config-file/)进行有倾向性的调整,来满足需求。假如我们要存 10 亿数据到 Redis 中,希望服务最多使用 32G 内存,可以使用下面的配置文件 `redis.conf`: ```bash # 内存配置 maxmemory 32gb maxmemory-policy allkeys-lru # 持久化配置 appendonly yes appendfsync everysec auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb # 连接配置 timeout 300 tcp-keepalive 60 maxclients 10000 # 安全配置 requirepass yourpassword ``` 同时,可以对我们的配置进行一些调整,让 Redis 使用配置启动: ```yaml name: cache services: redis: image: redis:7.4.1-alpine3.20 container_name: my-redis environment: - TZ=Asia/Shanghai volumes: # 共享时间 - /etc/localtime:/etc/localtime:ro - ./redis.conf:/etc/redis.conf:ro - ./redis-data:/data:rw command: redis-server /etc/redis.conf deploy: resources: limits: memory: 32G network_mode: "host" ulimits: nofile: soft: 65535 hard: 65535 restart: always healthcheck: test: ["CMD", "redis-cli","ping"] interval: 1s timeout: 3s retries: 5 ``` 最后,根据自己的情况调整系统配置: ```bash # 在宿主机上执行 sudo sysctl -w net.core.somaxconn=65535 # 永久生效 echo "net.core.somaxconn=65535" >> /etc/sysctl.conf sudo sysctl -p ``` ## 四、最佳实践建议 使用 Redis 时,最需要注意的就是内存管理问题。 在使用的过程中,务必设置合理的内存上限,通常会建议设置为最大内存的 70%~80%,为系统和其他程序预留空间。 同时,内存淘汰策略的选择也很重要。如果只是使用它作为缓存,那么使用 volatile-lru 或 allkeys-lru 策略即可,如果用于重要数据存储,应该设置为不淘汰(noeviction)。当我们存储了太多数据之后,可以定期执行 `MEMORY PURGE` 来清理内存碎片。 当然,如果是缓存场景,设置合理的数据过期时间,避免数据长期占用内存能够节约大量的内存资源。如果查询大量数据,在能够使用 `SCAN` 的时候,尽量使用 `SCAN` 来替代 `KEYS` 命令。在默认的配置文件中异步删除过期数据的配置是关闭的(`lazyfree-lazy-expire no`),打开之后,可以降低对服务性能的影响。 在使用 Redis 命令时要特别注意避免 O(N) 复杂度的命令,比如 KEYS、FLUSHALL、FLUSHDB 等,这些命令在数据量大时会阻塞主线程。与此同时,应该使用批量命令来提升性能,比如用 `MGET/MSET` 替代多次 `GET/SET` 操作。以及 Pipeline 可以将多个命令打包一起发送给 Redis 服务,显著提升吞吐量。如果需要保证操作的原子性,可以使用 Lua 脚本,这样能够减少多次请求的网络往返消耗。 在进行持久化存储的时候,页要根据场景来进行选择,选择合适的策略和具体的配置项目。DB 持久化可以配置多个条件触发,比如 900 秒内至少有 1 个键被修改、300 秒内至少有 10 个键被修改、60 秒内至少有 1 万个键被修改等。AOF 持久化可以将 appendfsync 设置为 everysec,这样每秒刷盘一次,在性能和安全性之间取得平衡。 在《[ThinkPad + Redis:构建亿级数据毫秒级查询的平民方案](https://soulteary.com/2024/11/21/thinkpad-with-redis-a-civilian-solution-for-building-millisecond-level-queries-on-billions-of-data.html)》的实战部分,我们提到过一些优化策略,也可以进行参考。至于主从和多机配置,如果读多写少,除了更换技术架构之外,读写分离是经典的策略。而多副本主从复制,则是大量在线业务主要考虑的选型。 最后,存储的键名设计,可以使用统一的前缀(如`业务名:对象名:id`),并尽量控制键名长度(缩写替代),避免使用特殊字符。连接 Redis 时使用连接池管理 Redis 连接,设置合理的超时时间,尽量在大量写入数据的时候使用异步处理。 ## 总结 好了,不知不觉写了这么多,这篇文章就先到这里。 --EOF