Redis在Web项目中的应用与实践

原文连接:Redis在Web项目中的应用与实践git

Redis做为一个开源的(BSD)基于内存的高性能存储系统,已经被各大互联网公司普遍使用,而且有着诸多的应用场景。本篇文章将基于PHP来详细讲解Redis在Web项目中的主要应用与实践。github

缓存

这里所介绍的缓存是指能够丢失或过时的数据。经常使用的命令有 set, hset, get, hget,使用redis做为缓存时须要注意一下几个问题:web

  • 因为redis的可用内存是有限的,不能容忍redis内存的无限增加,建议设置 maxmemory 最大内存。
  • 在开启maxmemory的状况下,能够启用lru机制,设置key的expire,当到达Redis最大内存时,Redis会根据最近最少用算法对key进行自动淘汰。
  • Redis的持久化策略和Redis故障恢复时间是一个博弈的过程,若是你但愿在发生故障时可以尽快恢复,应该启用dump备份机制,但这样须要更多的可用内存空间来进行持久化。若是可以容忍Redis漫长的故障恢复时间,可使用AOF持久化机制,同时关闭dump机制,这样不须要额外的内存空间。

存储

在web项目中,redis可存储读写很是频繁的数据来缓解MySQL等数据库的压力。redis若是做为存储系统的话,为了防止数据丢失,持久化必须开启。redis

典型场景算法

  • 计数器

计数器的需求很是广泛,例如微博点赞数、帖子收藏数、文章分享数、用户关注数等。数据库

  • 社交列表

好比使用Sets结构存储关注列表、收藏列表、点赞列表等。缓存

  • Session

借助redis高性能的key-value存储,可将用户登陆状态保存到redis中。安全

  • ...

队列

简单队列bash

通常使用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

咱们可能会考虑使用 setnxexpire 命令来实现加锁,即当没有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在Web项目中的应用与实践

扫码关注微信公众号: Learn2Code

相关文章
相关标签/搜索