> **Redis 场景使用**
[TOC]
## 说明 ##
**最近在整理Redis的一些使用场景, 包含我在工作当中的例子和网上看到比较好的文章。**
----------
##参考文章##
> 平凡希:https://www.cnblogs.com/xiaoxi/p/7007695.html
## String ##
<h5>介绍</h5>
String 数据结构是简单的key-value类型,value其实不仅是String,也可以是数字。
常用命令:get、set、incr、decr、mget等。
应用场景:String是最常用的一种数据类型,普通的key/ value 存储都可以归为此类,即可以完全实现目前 Memcached 的功能,并且效率更高。还可以享受Redis的定时持久化,操作日志及 Replication等功能。除了提供与 Memcached 一样的get、set、incr、decr 等操作外,Redis还提供了下面一些操作:
获取字符串长度
往字符串append内容
设置和获取字符串的某一段内容
设置及获取字符串的某一位(bit)
批量设置一系列字符串的内容
使用场景:常规key-value缓存应用。常规计数: 微博数, 粉丝数。
实现方式:String在redis内部存储默认就是一个字符串,被redisObject所引用,当遇到incr,decr等操作时会转成数值型进行计算,此时redisObject的encoding字段为int。
<h5>存储信息1</h5>
用户ID为查找的key,存储的value用户对象包含姓名,年龄,生日等信息,如果用普通的key/value结构来存储,主要有以下2种存储方式:
![请输入图片描述][1]
将用户相关的信息转换为JSON字符串,存储在string类型中
$userInfo=['name'=>'depp', 'age'=>'25','sex'=>'age',];
Redis::set("user:1",json_encode($userInfo));
dd(json_decode(Redis::get("user:1")));
第一种方式将用户ID作为查找key,把其他信息封装成一个对象以序列化的方式存储,这种方式的缺点是,增加了序列化/反序列化的开销,并且在需要修改其中一项信息时,需要把整个对象取回,并且修改操作需要对并发进行保护,引入CAS等复杂问题。
<h5>存储信息2</h5>
第二种方法是这个用户信息对象有多少成员就存成多少个key-value对儿,用用户ID+对应属性的名称作为唯一标识来取得对应属性的值,虽然省去了序列化开销和并发问题,但是用户ID为重复存储,如果存在大量这样的数据,内存浪费还是非常可观的
![请输入图片描述][2]
$shopConfing =[
's:1:mset'=>1, //是否开启会员体系
's:1:svset'=>1, //是否开启短视频功能
's:1:lset'=>1, //是否开始直播功能
];
Redis::mset($shopConfing);
dd(Redis::keys("shop:1:*"));
<h5>计数器</h5>
Redis string提供的incr 跟 decr 方法也可以实现简单的计数器功能。
Redis::set("v:z:1",1); //设置点赞
//增加点赞数
Redis::incr("v:z:1",1);
//得到点赞数
dd(Redis::get("v:z:1"));
> 其他计数器,统计数类似场景:微博的评论数、点赞数、分享数,抖音作品的收藏数,京东商品的销售量、评价数等
<h5>短信验证码小例子</h5>
//发送短信证码
$code=rand(1000,9999);
// 发送短信处理
/** send message **/
//存储验证码
Redis::setex("code:17600128033","120",$code);
// 接收验证码
$inCode="7580";
$mobileNumber="17600128033";
if($inCode == Redis::get("code:".$mobileNumber)){
dd ("验证码正确");
}else{
dd ("验证码错误,验证码为:".Redis::get("code:".$mobileNumber));
}
## Hash ##
Redis hash 是一个 string 类型的 field(字段) 和 value(值) 的映射表,hash 特别适合用于存储对象。
常用命令:hget,hset,hgetall 等。
<h5>存储信息3</h5>
除了上述string 存储信息的方式外,我们还可以用Hash类型来存储。
Redis的Hash实际是内部存储的Value为一个HashMap,并提供了直接存取这个Map成员的接口,使得Hash既没有序列化开销和并发问题,用户ID也不会重复存储,非常适合存储对象。
![请输入图片描述][3]
Redis::hset('user:1','name','zhangsan');
Redis::hset('user:1','sex','男');
Redis::hset('user:1','age','20');
dd(Redis::hgetall('user:1'));
<h5>注意点</h5>
这里同时需要注意,Redis提供了接口(hgetall)可以直接取到全部的属性数据,但是如果内部Map的成员很多,那么涉及到遍历整个内部Map的操作,由于Redis单线程模型的缘故,这个遍历操作可能会比较耗时,而另其它客户端的请求完全不响应,这点需要格外注意。
使用场景:存储部分变更数据,如用户信息等。
实现方式:
上面已经说到Redis Hash对应Value内部实际就是一个HashMap,实际这里会有2种不同实现,这个Hash的成员比较少时Redis为了节省内存会采用类似一维数组的方式来紧凑存储,而不会采用真正的HashMap结构,对应的value redisObject的encoding为zipmap,当成员数量增大时会自动转成真正的HashMap,此时encoding为ht。
<h5>计数、统计</h5>
同时Hash类型灵活的结构也适合给某一类事物进行各种维度的计数。
例如:电商类:商品维度有各种计数(点赞数,评论数,浏览数)
例如:直播类:主播维度有各种计数(动态数、关注数、粉丝数)
Redis::hset('video:1','likes',1); //视频点赞数
Redis::hset('video:1','collections',1); //视频收藏
//增加视频、点赞
Redis::hIncrBy('video:1','likes',1);
Redis::hIncrBy('video:1','collections',1);
//减少点赞数
Redis::hIncrBy('video:1','likes',-1);
//获取点赞数
Redis::hget('video:1','goods:4');
<h5>简单购物车</h5>
简单的购物车功能就可以使用Hash结构快速实现。以用户id为key,商品id为field,商品数量为value,恰好构成了购物车的3个要素,如下图所示。
![请输入图片描述][4]
Redis::hset('card:user:1','goods:1','1'); //用户1 增加1个商品1到购物车
Redis::hset('card:user:1','goods:2','2'); //用户2 增加2个商品2到购物车
Redis::hset('card:user:2','goods:1','2'); //用户2 增加2个商品1到购物车
//添加商品购物车
Redis::hset('card:user:1','goods:4','2');
//获取购物车内容
Redis::hgetall('card:user:1');
//增加数量
Redis::hIncrBy('card:user:1','goods:4','2');
//减少数量
Redis::hIncrBy('card:user:1','goods:4','-2');
//删除一个商品
Redis::hdel('card:user:1','goods:4');
//清空购物车
Redis::del('card:user:1');
<h5>hash 和 string的选择</h5>
string 和 hash 两种类型都可以用作对象存储。以下的思路可以供大家参考。
当一个对象的属性相对整体而且而且不易变化时,比较适合用string存储
比如:
用户:姓名、年龄、地址、爱好、民族、已婚等等
主播:房间号、姓名、年龄、直播领域
----------
当对象的某个属性需要频繁修改,且属性比较零散时,比较适合用hash存储
比如:
用户:喜欢的视频数、喜欢的商品数、点赞数
主播:粉丝数、订阅数
## List ##
<h5>介绍</h5>
常用命令:lpush,rpush,lpop,rpop,lrange等。
应用场景:
Redis list的应用场景非常多,也是Redis最重要的数据结构之一,比如twitter的关注列表,粉丝列表等都可以用Redis的list结构来实现。
List 就是链表,相信略有数据结构知识的人都应该能理解其结构。使用List结构,我们可以轻松地实现最新消息排行等功能。List的另一个应用就是消息队列,
可以利用List的PUSH操作,将任务存在List中,然后工作线程再用POP操作将任务取出进行执行。Redis还提供了操作List中某一段的api,你可以直接查询,删除List中某一段的元素。
实现方式:
Redis list的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销,Redis内部的很多实现,包括发送缓冲队列等也都是用的这个数据结构。
Redis的list是每个子元素都是String类型的双向链表,可以通过push和pop操作从列表的头部或者尾部添加或者删除元素,这样List即可以作为栈,也可以作为队列。
<h5>消息队列系统</h5>
使用list可以构建队列系统,使用sorted set甚至可以构建有优先级的队列系统。
比如:将Redis用作日志收集器
实际上还是一个队列,多个端点将日志信息写入Redis,然后一个worker统一将所有日志写到磁盘。
取最新N个数据的操作
记录前N个最新登陆的用户Id列表,超出的范围可以从数据库中获得。
//把当前登录人添加到链表里
for ($a=1;$a<20;$a++){
$ret = Redis::lpush("l:uid", $a); //login:user_id
}
//保持链表只有10位
$ret = Redis::ltrim("l:uid", 0, (10-1));
//获得前10个最新登陆的用户Id列表
dd(Redis::lrange("l:uid", 0, (10-1)));
结果:
array:10 [▼
0 => "19"
1 => "18"
2 => "17"
3 => "16"
4 => "15"
5 => "14"
6 => "13"
7 => "12"
8 => "11"
9 => "10"
]
<h5>sina微博热数据:</h5>
在Redis中我们的最新微博ID使用了常驻缓存,这是一直更新的。但是我们做了限制不能超过5000个ID,因此我们的获取ID函数会一直询问Redis。只有在start/count参数超出了这个范围的时候,才需要去访问数据库。
我们的系统不会像传统方式那样“刷新”缓存,Redis实例中的信息永远是一致的。SQL数据库(或是硬盘上的其他类型数据库)只是在用户需要获取“很远”的数据时才会被触发,而主页或第一个评论页是不会麻烦到硬盘上的数据库了。
<h5>秒杀场景的简单实现</h5>
队列的特性在日常开发中还可以用于流量削锋跟解耦。这里做流量削锋的秒杀抢购场景的简单示例。
//监听已抢购的数量
Redis::watch("sk:1:num"); //已经秒杀完的商品数量
$skNum = Redis::hget("sk:h:1",'gum'); //秒杀商品Hash信息
$isSkNum = (int)Redis::get("sk:1:num");
if($isSkNum < $skNum ){
$uid = $this->rand(5);//随机生成用户id
// 暂时用setnx 跟 expire 处理限购 问题是并发情况下会不止10个人进来,但是不影响限购啊
// 没有考虑到更好的解决方案,先这么处理吧
// Redis::set("sk:su:1", 1 , 'NX', 'EX', "1000"); 不清楚predis下set 同时设置 NX和EX为什么老是不生效,暂时用下边的方法处理
if(Redis::setnx("sk:su:".$uid,$uid)){
Redis::expire("sk:su:".$uid,10); //设置过期时间,保证10秒内一个用户只能秒杀成功一次
}else{
Rddis::incr('fail');
echo "10秒内允许抢购一次。";
}
//上述代码不能放在multi之内。 否则if(Redis::setnx("sk:su:".$uid,$uid)) 会报错
//放在multi当中,相当于未执行,结果不会返回,所以会一直报错
Redis::multi();
Redis::incr('sk:1:num');
Redis::lpush("sk:l:1",$uid);
Redis::exec();
}else{
Redis::incr('fail');
echo "不好意思,秒杀已经结束了。";
}
----------
ab 1000请求 100并发的请求结果:
127.0.0.1:6379> get sk:1:num
"10"
127.0.0.1:6379> lrange sk:l:1 0 -1
1) "t106A0502910239"
2) "160A50HR21k23R9"
3) "160i7O502f12Y39"
4) "160j5021W23UzZ9"
5) "Wd160c5i0212539"
6) "1Pw60I502Ae1239"
7) "146050RgG213239"
8) "1605p0aL212p3u9"
9) "16050wyAa2123Y9"
10) "16050fi2012f3j9"
<h5>热点数据更新</h5>
场景描述:后台在更新咨询。前台用户在观看数据。
比如说数据A B C D E F G,一次取两条数据的, 用户的进来是A B两条数据,此时后台运营人员在后台插入H F两条数据,此时链表变成了H F A B C D E F G,此时用户往上滑如何保证是C D
记录用户后一个值,传到后台,不能下标后移处理。
## 结尾 ##
<p style="background-image: -webkit-linear-gradient(left, #3498db, #f47920 10%, #d71345 20%, #f7acbc 30%,#ffd400 40%, #3498db 50%, #f47920 60%, #d71345 70%, #f7acbc 80%, #ffd400 90%, #3498db);color: transparent;-webkit-text-fill-color: transparent;-webkit-background-clip: text;text-align:center;">
腹有诗书气自华,最是书香能致远。
</p>
[1]: https://blog.zxliu.cn/usr/uploads/2020/11/217422969.png
[2]: https://blog.zxliu.cn/usr/uploads/2020/11/1945224260.png
[3]: https://blog.zxliu.cn/usr/uploads/2020/11/1321091741.png
[4]: https://blog.zxliu.cn/usr/uploads/2020/11/2167948208.png