共计 3578 个字符,预计需要花费 9 分钟才能阅读完成。
原文链接:Redis 在 Web 项目中的应用与实践
Redis 作为一个开源的 (BSD) 基于内存的高性能存储系统,已经被各大互联网公司广泛使用,并且有着诸多的应用场景。本篇文章将基于 PHP 来详细讲解 Redis 在 Web 项目中的主要应用与实践。
缓存
这里所介绍的缓存是指可以丢失或过期的数据。常用的命令有 set, hset, get, hget,使用 redis 作为缓存时需要注意一下几个问题:
由于 redis 的可用内存是有限的,不能容忍 redis 内存的无限增长,建议设置 maxmemory 最大内存。
在开启 maxmemory 的情况下,可以启用 lru 机制,设置 key 的 expire,当到达 Redis 最大内存时,Redis 会根据最近最少用算法对 key 进行自动淘汰。
Redis 的持久化策略和 Redis 故障恢复时间是一个博弈的过程,如果你希望在发生故障时能够尽快恢复,应该启用 dump 备份机制,但这样需要更多的可用内存空间来进行持久化。如果能够容忍 Redis 漫长的故障恢复时间,可以使用 AOF 持久化机制,同时关闭 dump 机制,这样不需要额外的内存空间。
存储
在 web 项目中,redis 可存储读写非常频繁的数据来缓解 MySQL 等数据库的压力。redis 如果作为存储系统的话,为了防止数据丢失,持久化必须开启。
典型场景
计数器
计数器的需求非常普遍,例如微博点赞数、帖子收藏数、文章分享数、用户关注数等。
社交列表
比如使用 Sets 结构存储关注列表、收藏列表、点赞列表等。
Session
借助 redis 高性能的 key-value 存储,可将用户登录状态保存到 redis 中。
…
队列
简单队列
一般使用 redis 的 list 结构作为队列,rpush 生产消息,lpop 消费消息,当 lpop 没有消息的时候,要进行适当的 sleep 操作。
$queueKey = “queue”;
// 生产者
$redis->rpush($queueKey, $data)
// 消费者
while (true) {
$data = $redis->lpop($queueKey);
if (null === $data) {
usleep(100000);
continue;
}
// 业务逻辑
…
}
由于没有消息时使用的 sleep 事件不好控制,生产环境尽量不要使用 sleep 来休眠,可使用 blpop 来消费消息,在没有新消息的时候它会阻塞到消息到来。
延时队列
延时队列可使用 redis 的 sorted set 数据结构,使用时间戳作为 score,消息内容作为 member,使用 zadd 命令来生产消息,消费者使用 zrangebyscore 命令获取指定时间之前的消息数据轮询进行处理。
$queueKey = “queue”;
// 生产消息
// 消费时间, 这里设置为 1 小时候
$consumeTimestamp = time() + 3600;
// $data 需要添加随机串前缀(or 后缀),防止出现重复 member 被丢弃
$data = $data . md5(uniqid(rand(), true));
$redis->zadd($queueKey, $consumeTimestamp, $data);
// 消费消息
while (tue) {
$arrData = $redis->zrangebyscore($queueKey, 0, time());
if (!$arrData) {
usleep(100000);
continue;
}
// 业务逻辑
foreach ($arrData as $data) {
$data = substr($data, 0, strlen($data) – 32);
// 消费 $data
}
}
多消费者
使用 pub/sub 主题订阅者模式,可以实现 1:N 的消息队列。这种模式中在消费者下线的情况下,生产的消息会丢失,在这里不推荐使用。
需要强调的是不推荐使用 redis 作为消息队列服务,这不是 redis 的设计目标。如果一定要用可考虑 disque,是由 redis 的作者开发。
分布式锁
分布式锁主要解决的几个问题:
互斥性: 同一时刻只能有一个服务 (或应用) 访问资源
安全性: 锁只能被持有该锁的服务 (或应用) 释放
容错: 在持有锁的服务 crash 时,锁仍能得到释放
避免死锁
方案 1
我们可能会考虑使用 setnx 和 expire 命令来实现加锁,即当没有 key 存在时才会成功写入 value:
$lockStatus = $redis->setnx($lockKey, 1);
if (1 === $lockStatus) {
// 加锁成功,为锁设置超时时间
$redis->expire($lockKey, 300);
// 进行后续操作
} elseif (0 === $lockStatus) {
// 加锁失败
} else {
// 其他异常
}
但这种操作不是原子性的,如果在进行 setnx 时服务崩溃,没有来得及对 Key 进行超时设置,该锁将一直无法释放。
方案 2
我们推荐 set key value [EX seconds] [PX milliseconds] [NX|XX] 命令来进行加锁
EX: key 在多少秒之后过期
PX:key 在多少毫秒之后过期
NX: 当 key 不存在的时候,才创建 key,效果等同于 setnx
XX:当 key 存在的时候,覆盖 key
$lockStatus = $this->redis->set($lockKey, 1, “EX”, 30, “NX”);
if (“OK” === $lockStatus) {
// 加锁成功,可进行后续操作
// 业务逻辑执行完毕,释放锁
$this->redis->del($lockKey);
} elseif (null === $lockStatus) {
// 加锁失败
}
如上代码所示,如果 set 命令返回 OK,那么客户端就可以获得锁(如果返回 null,那么应用服务可以在一段时间之后重新尝试获取锁),并且可以通过 del 命令来释放锁。
此方法需要注意的问题:
a 服务获得的锁(键 key)已经由于已到过期时间被 redis 服务器删除,但是这个时候 a 服务还去执行 DEL 命令。而 b 服务经在 a 设置的过期时间之后重新获取了这个同样 key 的锁,那么 a 执行 del 就会释放了 b 服务加好的锁。
当同一时刻有大量的 key 过期的时候,删除 key 时会增加 redis 压力,会影响服务稳定。
可以通过如下优化使得上面的锁系统变得更加健壮:
不要设置固定的字符串,而是设置为随机的大字符串,可以称为 token。
通过脚本删除指定锁的 key,而不是 del 命令。
在设置 key 过期时间的时候加上一个随机值。
优化后的代码可参考如下:
$lockToken = md5(uniqid(rand(), true));
// 此处超时时间根据具体业务逻辑配置
$expire = rand(280, 320);
$lockStatus = $this->redis->set($lockKey, $lockToken, “EX”, $expire, “NX”);
if (“OK” === $lockStatus) {
// 加锁成功,可进行后续操作
// 业务逻辑执行完毕,释放锁
// 删除锁之前需要判断是否是自己上的锁
$currentToken = $this->redis->get($lockKey);
if ($currentToken === $lockToken) {
$this->redis->del($lockKey);
}
} elseif (null === $lockStatus) {
// 加锁失败
}
计算
redis 提供的原子自增减方法以及有序集合结构等可以承担一些计算任务,例如浏览量统计等。
浏览计数
文章浏览量 +1
$redis->incr($postsKey);
批量获取文章浏览量
$arrPostsKey = [
//…
];
$arrPostsViewNum = $redis->mget($arrPostsKey);
排行榜
可以使用 redis 的有序集合来实现排行榜的功能,score 作为权重排序并取前 n 条记录。
// 存储数据
$sortKey = “sort_key”;
$redis->zadd($sortKey, 100, “tom”);
$redis->zadd($sortKey, 80, “Jon”);
$redis->zadd($sortKey, 59, “Lilei”);
$redis->zadd($sortKey, 87, “Hanmeimei”);
// 获取排行
// 由大到小排序
$arrRet = $redis->zrevrange($sortKey, 0, -1, true);
// 由小到大排序
$arrRet = $redis->zrange($sortKey, 0, -1, true);
结尾
redis 涉及的应用实践非常繁多的,由于篇幅所限无法全部顾及,本文只针对 web 应用中最常用的几个场景进行了展开介绍,渴望进一步拓展 redis 知识的同学可参考以下链接进一步学习。
Redis 官网
Antirez
原文链接:Redis 在 Web 项目中的应用与实践扫码关注微信公众号: Learn2Code