前面写过了两篇 “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)是一个开源的内存数据结构存储系统,它可以用作数据库、缓存、消息代理和队列。我们可以将 Redis 内置的 丰富的数据类型分为核心数据类型和扩展数据类型两大类。
1. 核心数据类型
字符串(String):字符串是 Redis 最基础的数据类型。它不仅可以存储普通的文本,还可以存储数字、序列化的对象(二进制)等。单个字符串最大支持 512MB。
# 基本的设置和获取
# 设置键值对
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):是按插入顺序排序的字符串列表。可以用来实现队列、栈或者最新动态这类场景,比如常用于消息队列场景。
# 添加元素
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):是无序的唯一字符串集合。适合用于标签系统、关注关系等场景,可以高效地实现交集、并集操作。
# 添加元素
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):哈希表是“字段(Key)-值(Value)对”的集合,类似于 Python 字典或 Java HashMap。特别适合用来存储对象数据,比如用户信息、商品信息等。
# 设置单个字段
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):每个成员关联一个分数的有序集合,成员唯一,分数可以重复。常用于排行榜、优先级队列等需要排序的场景。
# 添加带分数的元素
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):Stream 是 Redis 5.0 引入的数据类型,是追加式日志数据结构,用于记录和处理时序事件,支持消费者组模式。适合用于消息队列、事件流等场景。
# 添加消息
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):支持存储地理位置信息,并提供经纬度距离计算、范围查询等功能,特别适合在位置服务中使用。
# 添加地理位置
# 语法: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):支持对字符串进行位操作 ,适合高效存储布尔信息。适合用于记录状态、统计数据、用户行为统计等场景,可以大大节省内存。
# 设置位图的某一位
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):可在字符串中高效编码多个计数器 ,支持原子性的获取、设置和递增操作 ,提供多种溢出处理策略。简单来说,我们可以在 Bitfield 中存储多个整数值,每个整数可以指定不同的位宽,非常适合存储大量小整数或计数器。
# 设置一个 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 社区预览版包含这些模块),Redis Stack 和 Redis Enterprise 版本还提供了一些高级数据类型:
- 支持结构化数据存储
- 可直接操作 JSON 文档
- 支持复杂的查询操作
- 传递给命令的 JSON 值的深度限制最大为 128。
- HyperLogLog:基数估算
- Bloom filter:成员检测
- Cuckoo filter:可删除元素的成员检测
- t-digest:百分位数估算
- Top-K:排名估算
- Count-min sketch:频率估算
- 针对时间序列数据优化
- 支持高效的时间范围查询
- 适合监控和分析应用
因为 8.0 还没有正式发布,程序或许还有一些改进或变动的空间,所以我不计划在这篇文章中展开这三个类型的细节。
或许在后面的文章里,我会单独聊聊编译这些组件,以及实战的内容,就像是在之前使用 Redis 作为向量数据库使用一样:《使用 Redis 构建轻量的向量数据库应用:图片搜索引擎(一)》、《使用 Redis 构建轻量的向量数据库应用:图片搜索引擎(二)》。
二、Redis 实用场景
在介绍“核心数据类型”小节中,我提到了一些使用 Redis 的经典操作。就使用频率来说,最常见的场景还是缓存层(减轻数据库负载、提高响应速度、存储临时会话数据)、计数器(文章阅读量统计、用户点赞数、商品库存管理)、消息队列(任务排队、消息传递、事件处理)。
接下来我们来基于具体业务场景,再展开聊聊。
统一的登录态管理
又快到一年结束了,大家或许还有一些年假没有休,当我们提交年假的时候,可能需要先登录某个 OA 系统,然后再从 OA 系统跳转人力系统,然后提交年假使用申请,接下来就是等待年假审批通过,哦耶。
这个场景下,我们可能使用了多个系统,但是我们并不需要在系统间重复登录,因为系统会自动识别用户身份。这周无缝的用户体验,就可以使用 Redis 来实现。
具体实现方式是:当用户首次登录时,系统生成一个 Session ID,并将用户信息存储在 Redis 中。示例代码如下:
# 用户登录时
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 中获取用户信息,从而实现统一登录:
# 其他系统验证用户身份时
user_data = redis_client.get(f"session:{session_id}")
if user_data:
user = json.loads(user_data)
# 验证权限并允许访问
高并发场景下的缓存应用
每当有热门活动或者有大量集中访问高峰的时候,有一些系统就会变得很卡,尤其是需要大量数据库关联查询的场景。
比如每个月月初,大家集中向系统中查询上个月考勤是否有异常的时候,毕竟错过了补打卡还是挺麻烦的。这时,Redis 的缓存功能就派上了用场。
比如,对于考勤数据的缓存策略可以是这样的:
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 来处理再合适不过。
# 记录接口调用次数
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 的列表结构来实现一个简单的任务队列:
# 添加审批任务到队列
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 实现简单的访问频率限制:
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 的有序集合来实现:
# 记录课程学习人次
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 的地理空间索引特性:
# 添加公司位置信息
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 的集合可以高效地实现这类验证:
# 初始化有效的员工编号集合
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 可以实现简单而有效的限流器:
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 来实现一个简单的(分布式)“锁”:
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 官方提供了社区版本的容器镜像,包含多种 CPU 架构。
1. 基础 Docker 命令
官方文档中的命令使用非常简单:
# 启动一个服务名称为 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 默认行为发生了一些变化(比如是否会自动下载命令中未下载的镜像),所以我们从头开始:
# 拉取 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
文件:
name: cache
services:
redis:
image: redis:7.4.1-alpine3.20
container_name: my-redis
ports:
- "6379:6379"
这份配置文件实现了上面命令相同的功能,启动服务还是使用熟悉的命令即可:
docker-compose up -d
3. Redis 的数据持久化
Redis 提供两种持久化方式:
- RDB(Redis Database):按指定时间间隔执行数据快照,适合备份场景。
- AOF(Append Only File):记录所有写操作,提供更好的持久化保证。
而在 Docker 中基础的数据持久化也分为两种,一种是 Docker Volume,一种是文件挂载到磁盘。
使用 Docker Volume 进行 Redis 数据持久化:
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 的细节:
# 获取 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"
}
]
如果我们希望数据直接挂载到宿主机磁盘中,只需要做一些简单的改动:
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 快照模式来存储数据(牺牲数据恢复速度,但是具备最好的数据可恢复性),可以继续修改配置:
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 服务自愈
如果我们希望服务能够在遇到异常自动恢复,并且不时的进行自检,判断是否需要进行服务自动恢复:
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
):
# 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 相比较一些后起之秀,不支持将数据持久化到磁盘。随着数据增长,内存占用也会变大,为了避免无限制增长影响其他的业务,我们可以设置一个资源限制:
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 和宿主机的时间保持一致,那么这个时候可以指定容器中的时区,并共享本机的时间和容器的时间:
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 配置进行有倾向性的调整,来满足需求。假如我们要存 10 亿数据到 Redis 中,希望服务最多使用 32G 内存,可以使用下面的配置文件 redis.conf
:
# 内存配置
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 使用配置启动:
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
最后,根据自己的情况调整系统配置:
# 在宿主机上执行
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:构建亿级数据毫秒级查询的平民方案》的实战部分,我们提到过一些优化策略,也可以进行参考。至于主从和多机配置,如果读多写少,除了更换技术架构之外,读写分离是经典的策略。而多副本主从复制,则是大量在线业务主要考虑的选型。
最后,存储的键名设计,可以使用统一的前缀(如业务名:对象名:id
),并尽量控制键名长度(缩写替代),避免使用特殊字符。连接 Redis 时使用连接池管理 Redis 连接,设置合理的超时时间,尽量在大量写入数据的时候使用异步处理。
总结
好了,不知不觉写了这么多,这篇文章就先到这里。
–EOF