MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Redis设置键过期时间的精准控制方法

2022-05-066.5k 阅读

Redis 过期时间基础概念

Redis 是一个高性能的键值对存储数据库,在很多场景中,我们需要让某些键在特定时间后失效,这就涉及到设置键的过期时间。过期时间在 Redis 中有多种用途,比如缓存数据,防止数据长期占用内存,自动清理临时数据等。

在 Redis 中,每个键都可以设置一个过期时间(以秒为单位),一旦设置了过期时间,当到达这个时间点后,键会被自动删除。Redis 通过一个过期字典来维护所有设置了过期时间的键及其过期时间戳。这个过期字典是一个哈希表,键是指向 Redis 对象的指针,值是过期时间的毫秒级时间戳。

设置过期时间的基本命令

Redis 提供了多个命令来设置键的过期时间。

  • EXPIRE:该命令用于设置键的过期时间,以秒为单位。例如,要设置键 mykey 在 60 秒后过期,可以使用以下命令:
EXPIRE mykey 60
  • PEXPIRE:与 EXPIRE 类似,但设置的过期时间是以毫秒为单位。比如设置键 mykey 在 1000 毫秒(1 秒)后过期:
PEXPIRE mykey 1000
  • EXPIREAT:用于设置键在指定的时间点过期,时间点以秒级时间戳表示。假设当前时间戳是 1677721600,要设置键 mykey 在 10 分钟(600 秒)后过期,即时间戳 1677721600 + 600 = 1677722200,命令如下:
EXPIREAT mykey 1677722200
  • PEXPIREAT:同样是设置键在指定时间点过期,但时间点以毫秒级时间戳表示。

查看键的剩余过期时间

可以使用 TTL(以秒为单位返回剩余过期时间)和 PTTL(以毫秒为单位返回剩余过期时间)命令来查看键的剩余过期时间。例如:

TTL mykey

如果键不存在或者没有设置过期时间,TTL 会返回 -2-1 分别表示键不存在和没有设置过期时间。

精准控制过期时间的需求场景

在实际应用中,简单地设置一个固定的过期时间往往不能满足复杂的业务需求,需要对过期时间进行精准控制。

基于业务逻辑的动态过期时间

例如在电商系统中,对于用户的购物车数据,如果用户在一段时间内没有操作,购物车数据可以自动清理。但是不同用户的活跃程度不同,对于活跃用户,购物车数据可以保留较长时间,而对于不活跃用户,购物车数据保留时间可以相对较短。这就需要根据用户的活跃度动态计算购物车键的过期时间。

假设我们通过用户的登录频率和操作频率来判断用户活跃度,活跃度高的用户购物车数据保留 7 天(604800 秒),活跃度中等的用户保留 3 天(259200 秒),活跃度低的用户保留 1 天(86400 秒)。在代码中可以这样实现(以 Python 为例,使用 redis - py 库):

import redis

r = redis.Redis(host='localhost', port=6379, db = 0)

# 假设通过某种方式获取用户活跃度
user_activity = "high"

if user_activity == "high":
    expiration_time = 604800
elif user_activity == "medium":
    expiration_time = 259200
else:
    expiration_time = 86400

cart_key = "user:1:cart"
r.setex(cart_key, expiration_time, "购物车数据")

与其他事件关联的过期时间

在分布式系统中,可能存在多个服务相互协作。比如一个文件上传服务,当文件上传成功后,会在 Redis 中设置一个键来标记该文件的相关信息,并设置过期时间。但是如果在过期时间内,另一个服务对该文件进行了处理并持久化存储,那么这个 Redis 键就应该提前过期。

以 Java 为例,使用 Jedis 库:

import redis.clients.jedis.Jedis;

public class FileUploadExample {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);

        // 文件上传成功,设置键和过期时间
        String fileKey = "file:123";
        jedis.setex(fileKey, 3600, "文件相关信息");

        // 模拟另一个服务处理文件并持久化
        boolean isFileProcessed = true;
        if (isFileProcessed) {
            jedis.del(fileKey);
        }
    }
}

精准控制过期时间的实现方法

时间戳的精确计算

为了实现精准控制过期时间,首先要确保时间戳的计算准确。在大多数编程语言中,都提供了获取当前时间和进行时间计算的方法。

在 Python 中,可以使用 datetime 模块:

from datetime import datetime, timedelta

# 获取当前时间
now = datetime.now()
# 计算 30 分钟后的时间
expiration_time = now + timedelta(minutes = 30)
# 转换为秒级时间戳
timestamp = int(expiration_time.timestamp())

r = redis.Redis(host='localhost', port=6379, db = 0)
key = "mykey"
r.setex(key, (timestamp - int(now.timestamp())), "数据")

在 Java 中,可以使用 java.time 包:

import java.time.Duration;
import java.time.Instant;
import redis.clients.jedis.Jedis;

public class TimeCalculationExample {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);

        // 获取当前时间
        Instant now = Instant.now();
        // 计算 15 分钟后的时间
        Instant expirationTime = now.plus(Duration.ofMinutes(15));

        long currentTimestamp = now.getEpochSecond();
        long expirationTimestamp = expirationTime.getEpochSecond();

        String key = "mykey";
        jedis.setex(key, (int) (expirationTimestamp - currentTimestamp), "数据");
    }
}

利用 Lua 脚本

Lua 脚本在 Redis 中可以保证原子性执行,这对于精准控制过期时间非常有用。比如在一些复杂的业务逻辑中,需要在设置键值对的同时,根据一些条件精准设置过期时间。

以下是一个 Lua 脚本示例,用于根据传入的参数动态设置键的过期时间(以 Redis 命令行调用 Lua 脚本为例):

-- 获取参数
local key = ARGV[1]
local value = ARGV[2]
local expiration_time = ARGV[3]

-- 设置键值对并设置过期时间
redis.call('SET', key, value)
redis.call('EXPIRE', key, expiration_time)

在 Redis 命令行中调用该脚本:

EVAL "local key = ARGV[1]; local value = ARGV[2]; local expiration_time = ARGV[3]; redis.call('SET', key, value); redis.call('EXPIRE', key, expiration_time)" 0 mykey "数据" 60

在 Python 中使用 redis - py 库调用该 Lua 脚本:

import redis

r = redis.Redis(host='localhost', port=6379, db = 0)

lua_script = """
local key = ARGV[1]
local value = ARGV[2]
local expiration_time = ARGV[3]
redis.call('SET', key, value)
redis.call('EXPIRE', key, expiration_time)
return true
"""

sha = r.script_load(lua_script)
result = r.evalsha(sha, 0, "mykey", "数据", 60)
print(result)

过期时间精准控制中的问题与解决

时钟漂移问题

在分布式系统中,不同节点的时钟可能存在一定的偏差,这就会导致在设置过期时间时出现误差。比如在一个主从 Redis 架构中,主节点和从节点的时间可能不一致。

为了解决时钟漂移问题,可以采用以下方法:

  • NTP 同步:在所有服务器节点上配置 NTP(Network Time Protocol)服务,确保各个节点的时钟与标准时间同步。这样可以将时钟偏差控制在很小的范围内。
  • 使用统一时间源:在应用层面,可以选择一个可靠的时间源,比如专门的时间服务器,所有节点都从这个时间源获取时间,而不是依赖本地时钟。在代码实现上,可以通过调用时间服务器的 API 来获取准确时间。

过期时间抖动

Redis 在删除过期键时,采用的是惰性删除和定期删除相结合的策略。惰性删除是在每次访问键时检查键是否过期,如果过期则删除;定期删除是 Redis 每隔一段时间随机检查一些设置了过期时间的键,并删除过期的键。

这种策略可能会导致过期时间抖动,即键可能不会在精确的过期时间点被删除。为了尽量减少过期时间抖动,可以调整定期删除的频率和每次检查的键数量。在 Redis 配置文件中,可以通过 hz 参数来调整定期删除的频率,hz 默认值是 10,表示每秒执行 10 次定期删除操作。可以适当提高 hz 值,但要注意这会增加 CPU 开销。

结合应用场景优化过期时间控制

缓存场景

在缓存场景中,精准控制过期时间可以提高缓存命中率,减少数据库压力。比如在一个新闻网站中,文章缓存的过期时间可以根据文章的热度来动态调整。热门文章缓存时间可以设置较长,而冷门文章缓存时间可以相对较短。

以 Node.js 为例,使用 ioredis 库:

const Redis = require('ioredis');
const redis = new Redis();

// 假设通过某种方式获取文章热度
const articleHeat = 100;

let expirationTime;
if (articleHeat > 50) {
    expirationTime = 3600; // 热门文章缓存 1 小时
} else {
    expirationTime = 600; // 冷门文章缓存 10 分钟
}

const articleKey = "article:1";
const articleContent = "文章内容";

redis.setex(articleKey, expirationTime, articleContent);

会话管理场景

在 Web 应用的会话管理中,精准控制会话过期时间可以提高用户体验和安全性。比如对于长时间未操作的用户会话,可以提前过期。同时,对于一些特殊的用户,如管理员用户,会话过期时间可以设置得更长。

在 PHP 中,使用 Predis 库:

<?php
require 'vendor/autoload.php';

$redis = new Predis\Client();

// 判断用户是否为管理员
$isAdmin = true;

$sessionKey = "session:123";
$sessionData = serialize(array('user_id' => 1, 'username' => 'test'));

if ($isAdmin) {
    $expirationTime = 7200; // 管理员会话保留 2 小时
} else {
    $expirationTime = 1800; // 普通用户会话保留 30 分钟
}

$redis->setex($sessionKey, $expirationTime, $sessionData);
?>

通过以上方法和示例,可以在 Redis 中实现对键过期时间的精准控制,满足各种复杂的业务需求,提高系统的性能和稳定性。无论是在缓存、会话管理还是其他需要对数据生命周期进行控制的场景中,精准控制过期时间都具有重要意义。同时,要注意解决在精准控制过期时间过程中可能遇到的时钟漂移、过期时间抖动等问题,确保系统的准确性和可靠性。在不同的应用场景中,结合业务逻辑对过期时间进行优化,能够充分发挥 Redis 的优势,提升整个系统的效率和用户体验。