Redis 系列目录
第一篇:Redis 基础命令
第二篇:Redis 常见应用场景
第三篇:Redis Cluster集群搭建
第四篇:Redis 主从及哨兵搭建
第五篇:Redis 主从及集群
第六篇:Redis 持久化
第七篇:Redis 分布式锁
第八篇:Redis 底层数据存储结构
第八篇:Redis 面试常问问题
文章目录
前言
redis常用五种数据类型的应用场景。[个人笔记]
一、string类型
1.缓存
作为Key-Value形态的内存数据库,Redis 最先会被想到的应用场景便是作为数据缓存。而使用 Redis 缓存数据非常简单,只需要通过string类型将序列化后的对象存起来即可,不过也有一些需要注意的地方:
- 必须保证不同对象的 key 不会重复,并且使 key 尽量短,一般使用类名(表名)加主键拼接而成。
- 选择一个优秀的序列化方式也很重要,目的是提高序列化的效率和减少内存占用。
缓存内容与数据库的一致性(单例模式),这里一般有两种做法:
- 只在数据库查询后将对象放入缓存,如果对象发生了修改或删除操作,直接清除对应缓存(或设为过期)。
- 在数据库新增和查询后将对象放入缓存,修改后更新缓存,删除后清除对应缓存(或设为过期)。
示例:
public function getUserInfo($id)
{
if (isset(self::$user->id) && $user->id == $id) {
return self::$user;
}
$user = unserialize(Redis::get('user_'.$id));
if ($user->id == $id) {
self::$user = $user;
return $user;
}
$user = User::find($id);
if ($user->id) {
self::$user = $user;
Redis::set('user_'.$id, serialize($user));
return $user;
}
return null;
}
2.分布式Session
分布式 session
就是将多个服务器的 session
都存储到中,可使用 redis
来实现session共享。一般的PHP框架都会有可以配置session的存储方式。以lavael
为例,下面是 config/session.php
的内容:
<?php
use Illuminate\Support\Str;
return [
改为redis存储session,直接修改.env配置文件,SESSION_DRIVER改为redis。
'driver' => env('SESSION_DRIVER', 'file'), // 存储 Session 数据的位置,如:file、cookie、database、memcached / redis、dynamodb、array。
'lifetime' => env('SESSION_LIFETIME', 120), // 会话生存时间
'expire_on_close' => false, // 有效期开关
'encrypt' => false, // 会话加密
'files' => storage_path('framework/sessions'), // 文件存储位置
'connection' => env('SESSION_CONNECTION', null), // 会话数据库连接(使用数据库和redis时可配置)
'table' => 'sessions', // 使用数据库时存储session的表名
'store' => env('SESSION_STORE', null), // 会话缓存存储
'lottery' => [2, 100], // 某些会话驱动程序必须手动扫描其存储位置,才能从存储中删除旧会话。以下是在给定的请求下发生这种情况的可能性。默认情况下,概率为百分之二
'cookie' => env( // 会话Cookie名称
'SESSION_COOKIE',
Str::slug(env('APP_NAME', 'laravel'), '_').'_session'
),
'path' => '/', // 会话Cookie路径
'domain' => env('SESSION_DOMAIN', null), // 域名
'secure' => env('SESSION_SECURE_COOKIE'), // 证书认证
'http_only' => true, // 仅HTTP访问
'same_site' => 'lax', // 相同网站Cookie
];
3.分布式锁
set lock_key locked NX EX 1
# 一般带上过期时间(具体情况会比这里写的复杂的多,都可以单独写一篇博客了)
由于这个操作是原子性的,可以简单地以此实现一个分布式的锁
setnx
命令:表示SET if Not eXists,即如果 key 不存在,才会设置它的值,否则什么也不做。
两个客户端同时向redis写入try_lock,客户端1写入成功,即获取分布式锁成功。客户端2写入失败,则获取分布式锁失败。
set
命令加过期时间:
//参数一: $key 缓存键
//参数二: $value 缓存值
//参数三: $expireResolution EX/PX
// EX 设置键key的过期时间,单位时秒(seconds)
// PX 设置键key的过期时间,单位时毫秒(milliseconds)
//参数四: $expireTTL 缓存键过期时间,单位由参数三决定
//参数五: $flag NX/XX
// NX 只有键key不存在的时候才会设置key的值
// XX 只有键key存在的时候才会设置key的值
set($key, $value, $expireResolution = null, $expireTTL = null, $flag = null)
laravel示例:
Redis::set('shop:shop_'.$id, 1, "EX", 10, "NX");
Redis::setex("shop:shop_".$id, 10, '手机'); // 会覆盖旧值
setnx + expire
,需配合 LUA
脚本来保证原子性(下面只是普通用法,不涉及LUA脚本):
// 不存在则创建,存在不做任何操作
Redis::setnx('shop:shop_'.$id, 34);
// 设置过期时间
Redis::expire("shop:shop_".$id, 10); # 秒
Redis::pexpire("shop:shop_".$id, 10000); # 毫秒
一般使用第一种就够了,如果需要使用 redis 操作的命令较多,需配合使用 LUA
脚本实现原子性。
参考:Redis实现分布式锁。
4.全局ID
int类型,incrby,利用原子性
incrby userid 1000
分库分表的场景,一次性拿一段
$id = Redis::incr('id'); # 递增 阶梯为1
$id = Redis::incrby('id', 2); # 递增 阶梯为2
相关命令:
Redis::decr('id'); # 递减 阶梯为1
Redis::decrby('id', 2); # 递减 阶梯为2
5.计数器
int类型,incr方法
例如:文章的阅读量、微博点赞数、允许一定的延迟,先写入Redis再定时同步到数据库。
计数功能应该是最适合 Redis 的使用场景之一了,因为它高频率读写的特征可以完全发挥 Redis 作为内存数据库的高效。在 Redis 的数据结构中,string、hash和sorted set都提供了incr方法用于原子性的自增操作,下面举例说明一下它们各自的使用场景:
- 如果应用需要显示每天的注册用户数,便可以使用string作为计数器,设定一个名为REGISTERED_COUNT_TODAY的 key,并在初始化时给它设置一个到凌晨 0 点的过期时间,每当用户注册成功后便使用incr命令使该 key 增长 1,同时当每天凌晨 0 点后,这个计数器都会因为 key 过期使值清零。
- 每条微博都有点赞数、评论数、转发数和浏览数四条属性,这时用hash进行计数会更好,将该计数器的 key 设为weibo:weibo_id,hash的 field 为like_number、comment_number、forward_number和view_number,在对应操作后通过hincrby使hash 中的 field 自增。
- 如果应用有一个发帖排行榜的功能,便选择sorted set吧,将集合的 key 设为POST_RANK。当用户发帖后,使用zincrby将该用户 id 的 score 增长 1。sorted set会重新进行排序,用户所在排行榜的位置也就会得到实时的更新。
文章阅读量:
Redis::incr('article_read_num_'.$id);
6.限流
int类型,incr方法
以访问者的ip和其他信息作为key,访问一次增加一次计数,超过次数则返回false
限制用户访问次数:
$userid = 1;
$accessTime = 10; # 10s内最多访问20次
$$accessNum= 20;
$key = md5($_SERVER['REMOTE_ADDR'].$userid);
$access = Redis::get('access:'.$key);
if ($access && $access >= $accessNum){
echo '用户访问超过限制';
}
Redis::incr('access:'.$key);
!$access && Redis::expire($key, $accessTime); // 设置锁的有效时间
echo '正常访问';
二、hash类型
1.存储对象
例如:商品的库存,价格,评论数,关注度经常变换;计数器(参考string计算器)
$id = 1;
//$shopInfo = Shop::find($id);
$shopInfo = [
'id' => 1,
'name' => "红米手机",
'price' => 999.99,
'stock' => 100, # 库存
'salesVolume' => 1000, # 销售量
];
foreach ($shopInfo as $key => $value){
Redis::hset('shop:'.$id, $key, $value);
}
//卖出后,销售量加一,库存减一
Redis::hincrby('shop:'.$id, 'salesVolume', 1);
$stock = Redis::hget('shop:'.$id, 'stock'); # 由于hash类型没有递减命令,只能先取出计算再重新存入
$stock = $stock - 1 >= 0 ? $stock - 1 : 0;
Redis::hset('shop:'.$id, 'stock', $stock);
三、list类型
1.时间轴(Timeline)
list作为双向链表,不光可以作为队列使用。如果将它用作栈便可以成为一个公用的时间轴。当用户发完微博后,都通过lpush将它存放在一个 key 为LATEST_WEIBO的list中,之后便可以通过lrange取出当前最新的微博。
// 模拟用户发微博
for ($i = 1; $i < 10; $i++){
Redis::lpush('LATEST_WEIBO', 'weibo_'.$i);
}
$list = Redis::lrange('LATEST_WEIBO', 0, 5);
dd($list);
//array:6 [▼
// 0 => "weibo_9"
// 1 => "weibo_8"
// 2 => "weibo_7"
// 3 => "weibo_6"
// 4 => "weibo_5"
// 5 => "weibo_4"
//]
2.消息队列
Redis 中list的数据结构实现是双向链表,所以可以非常便捷的应用于消息队列(生产者 / 消费者模型)。消息的生产者只需要通过lpush将消息放入 list,消费者便可以通过rpop取出该消息,并且可以保证消息的有序性。如果需要实现带有优先级的消息队列也可以选择sorted set。而pub/sub功能也可以用作发布者 / 订阅者模型的消息。无论使用何种方式,由于 Redis 拥有持久化功能,也不需要担心由于服务器故障导致消息丢失的情况。
List提供了两个阻塞的弹出操作:blpop/brpop,可以设置超时时间
blpop:blpop key1 timeout 移除并获取列表的第一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
brpop:brpop key1 timeout 移除并获取列表的最后一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
队列:先进先出:rpush blpop,左头右尾,右边进入队列,左边出队列
栈:先进后出:rpush brpop
用户评论后需要人工审核,需要先把评论放入待审核队列中,等待审核:
// 模拟用户评论,
for ($i = 1; $i < 10; $i++){
Redis::lpush('comment', 'comment'.$i);
}
// 取出审核
$comment = Redis::brpop('comment');
$status = $this->verify($comment);
if (!$status){
echo "审核失败";
} else {
echo "审核通过";
}
四、set类型
1.抽奖
利用set结构的无序性,通过 Spop( Redis Spop 命令用于移除集合中的指定 key 的一个或多个随机元素,移除后会返回移除的元素。 ) 随机获得值
//填充奖池
Redis::sadd('jackpot', 'a', 'b', 'c', 'd', 'e', 'd'); // 会去重
//开始抽奖
$result = Redis::spop('jackpot');
dd($result); // a
2.点赞、签到、打卡
假如用户这条微博ID是
t1001
,用户ID是u3001
,下面使用 redis的 集合 来实现对这条微博的点赞操作:
用 like:t1001 来维护 t1001 这条微博的所有点赞用户
// u3001 点赞了这条微博:
Redis::sadd('like:t1001', 'u3001');
// 取消点赞:
Redis::srem('like:t1001', 'u3001');
// 是否点赞:
Redis::sismember('like:t1001', 'u3001'); // true
// 点赞的所有用户:
Redis::smembers('like:t1001');
//array:1 [▼
// 0 => "u3001"
//]
// 点赞数:
Redis::scard('like:t1001'); // 1
3.好友关系、用户关注、推荐模型
可通过set的交集、并集、差集命令实现:
// A的关注和粉丝,B的关注和粉丝
Redis::sadd('A:follow', 'B', 'C', 'D'); # A的关注
Redis::sadd('A:fans', 'B', 'E', 'F', 'H'); # A的粉丝
Redis::sadd('B:follow', 'C', 'D', 'X', 'Y'); # B的关注
Redis::sadd('B:fans', 'B', 'E', 'Z'); # B的粉丝
// 与A互相关注的用户
$res = Redis::sinter('A:follow', 'A:fans');
dd($res);
//array:1 [▼
// 0 => "B"
//]
// A和B共同关注的人
$res = Redis::sinter('A:follow', 'B:follow');
dd($res);
//array:2 [▼
// 0 => "D"
// 1 => "C"
//]
// A的关注人也关注了B
$res = Redis::sinter('A:follow', 'B:fans');
dd($res); # B也关注了自己
//array:1 [▼
// 0 => "B"
//]
// 推荐模型:通过关注人的关注进行推荐(差集)
// A 可能认识的人(A关注了B,给A推荐B的关注中A未关注的人)
$res = Redis::sdiff('B:follow', 'A:follow');
dd($res);
//array:2 [▼
// 0 => "X"
// 1 => "Y"
//]
4.倒排索引
倒排索引是构造搜索功能的最常见方式,在 Redis 中也可以通过set进行建立倒排索引,这里以搜索城市景点举例:
假设要搜索北京天安门,通过分词将这个词分为若干个前缀索引,有:北、北京、…门。将这些索引分别作为set的 key(例如:index:北)并存储北京的 id,倒排索引便建立好了。接下来只需要在搜索时通过关键词取出对应的set并得到其中的 id 即可。
// ‘北京天安门’ 分词后可得到下面数组:
$id = 1;
$terms = ['北','北京','北京天','北京天安','北京天安门','京','京天','京天安','京天安门','天','天安','天安门','安','安门','门'];
foreach ($terms as $v){
Redis::sadd('index:'.$v, $id);
}
五、zset类型
1.排行榜
使用sorted set(有序set)和一个计算热度的算法便可以轻松打造一个热度排行榜,zrevrangebyscore可以得到以分数倒序排列的序列,zrank可以得到一个成员在该排行榜的位置(是分数正序排列时的位置,如果要获取倒序排列时的位置需要用zcard-zrank)。
id 为6001 的新闻点击数加1:
zincrby hotNews:20190926 1 n6001
获取今天点击最多的15条:
zrevrange hotNews:20190926 0 15 withscores
为每一天的新闻点击添加排行
// 比如20200101这一天刚开始就有四个新闻点击:
Redis::zadd('news:20200101', 1, 6001); # 新闻id为6001
Redis::zadd('news:20200101', 1, 6002);
Redis::zadd('news:20200101', 1, 6003);
Redis::zadd('news:20200101', 1, 6004);
Redis::zadd('news:20200101', 1, 6005);
// 也可以直接使用zincrby添加到集合中
Redis::zincrby('news:20200101', 1, 6001);
// 获取某一天前十条点击数最多的新闻
Redis::zrevrange('news:20200101', 0, 10);
$res = Redis::zrevrange('news:20200101', 0, 10);
dd($res);
//array:5 [▼
// 0 => "6001"
// 1 => "6005"
// 2 => "6004"
// 3 => "6003"
// 4 => "6002"
//]
// 获取排序位置
dd(Redis::zrank('news:20200101', 6001)); // 2
// 获取集合数元素个数
dd(Redis::zcard("news:20200101")); // 5
参考地址:Redis 16 个常见使用场景。
命令详情可参考另一篇博客:Redis 基础命令。