实战一:最简单的新闻缓存 设置 查询 获取过期时间
一台数据库服务器 一台redis缓存服务器 redis缓存服务器为了缓解用户对数据库服务器请求造成的较大压力 根据业务关系来决定 哪些数据需要进入缓存 让用户访问的时候让缓存的命中率更高
最常见的模式:
a.判断cache是否存在
b.有从redis获取直接返回
c.没有则从数据库获取 取出来后插入缓存 返回数据读取出来的数据
用到的命令:
set key value 设置键的值
set key value EX 100 设置键的值 并设置了过期为100s
set key value EX 100 NX 设置键的值 并设置了过期时间为100s 并且一旦设置在设置的话就会返回false
get key 获取键的值
ttl key 获取缓存的过期时间
keys * 查看所有key
flushall 干掉所有的key
incr 命令可以对指定key的值+1,如果key不存在,首先设置为0,然后+1 y
incrBy 命令类似,可以增加指定的数字
新闻类别的redis里面的数据要设置合理的过期时间
因为如果不设置过期时间 缓存只会越来越大
我们通过上边讲过的“最常见的模式” 当用户访问的时候 还没过期则返回 如果过期了那么从数据库直接读取插入达到redis 返回数据库里面的数据给用户
这样就避免了用户一直不访问但缓存一直存在的情况 也解决了缓存空间
实战二:缓存穿透技术实现--设置空键
其实上边的流程是正确的 通畅的 去查redis缓存 有则返回 没有去查数据库 数据库有则插入到缓存返回数据库里面的值 但是不做细致的处理容易产生“缓存穿透” 比如用户一直访问不存在的数据库id 那么如果不做特殊处理 则程序会一直去访问数据库 对数据库也是会照成很多的压力 所以现在我们来了解一下“
如何防止缓存穿透
”
通常的比较low的其他防护手段
a.在程序路由阶段 就限定ID的范围(譬如使用正则表达式)
b.程序硬编码判断ID值的合法性
c.数据库查询判断
接下来我们讲解一下redis层面的做法:
比如新闻表的id范围是100到2000 这时如果查询2004或者3000等id都是没有的 现在没有不代表以后没有 因此这里其中的一种做法就是:
譬如key=news20000 如果数据库返回空 那么我们依然往redis当中存储一个值 我们约定好的数据结构 比如new20000 = -1 并且设置过期时间 譬如过期时间为200秒 如果下次继续查询的是news20000 那么我们增加这个key的过期时间 譬如增加5s
有图有真相 看流程图:
再来一张:
代码请参考“缓存穿透技术实现–封杀单IP”的代码 一样的
实战三:缓存穿透技术实现--封杀单IP
如果有脑残的人写程序反复在玩弄你 那么你的redis里面很快就会塞满各种无用新闻key 这些key的值都是-1 数量过多
如果仅仅是像“缓存穿透技术实现–设置空键” 里面讲到的 多数据库也会有压力(毕竟第一次还是需要访问数据库的) 今天的策略是:对于一个IP 我们单独开辟一个key/value key=前缀+IP地址
比如如果DB没有查询命中 则+2分
如果命中我们设置的redis里面的“默认值” 则+1分
一旦有以上操作 对应key的数据重新expire指定的时间 比如是3600s
当该IP的分数值达到我们设定的阙值,
则该IP无法访问我们的新闻页面(或者给一个静态页面看看拉倒)
上代码吧 不画图了:
<?php
require("Entities/functions.php");
require("Entities/NewsInfo.php");
//判断是否被禁
if(isForbidden())
{
header("location:wait.html"); //直接跳转到 设定好的页面
exit();
}
$newsid=getParameter("id");
if(!$newsid) die("错误的参数");
$news=new NewsInfo();
$getNews=$redis->StringOperator->getFromReids("news".$newsid); //从redis取
$setIncr=false;
if(!$getNews) //如果木有取到
{
$getNews=getFromDB($newsid);//就从数据库取
if(!$getNews) //数据库里也没取到 //这里要加2分
{
$getNews=defaultCache; //设置为默认缓存
$redis->StringOperator->incrForbidden('p'.getIP(),2); //给指定的ip增加2分 incrForbidden()方法当中并且会重新设置key的过期时间
$setIncr=true;//标识设置为true,防止下面再一次被加分
}
else
$getNews=json_encode($getNews);//DB取到了 就把它变成JSON格式字符串
$redis->StringOperator->setToRedis("news".$newsid,$getNews,200);//塞入缓存 ,过期时间为200秒。 测试时间,莫纠结
}
else
{
echo "from cache";
}
if(isDefaultCache($getNews))
{
!$setIncr && $redis->StringOperator->incrForbidden('p'.getIP(),1); //命中 默认缓存 给指定的ip增加1分
$redis->StringOperator->expireCache("news".$newsid,5);// 给原来的缓存 增加5秒时间
exit("别黑我了!!!");
}
echo $getNews;
其实“缓存穿透技术实现–封杀单IP”是在“缓存穿透技术实现–设置空键”的基础上
多了“前缀+IP地址”这个键 根据访问的次数不同> 那么“前缀+IP地址”这个键的对应的值会变大 大到设置的阙值就让用户无法再访问
所以不再画图讲述
实战四:缓存穿透技术实现--封杀IP段
这里我们需要使用到list列表类型 常用的命令如下
lpush key value 从键为key的列表的头部开始插入值value
rpush key value 从键为key的列表的尾部开始插入值value
lpush/rpush users zhangsan lisi hujun 可以批量插入
llen key 获取键为key的列表的长度
lrange key start end 从键为key的list中根据起始和结束位置获取数据 0代表第一个 -1代表最后一个 -2倒数第二个
为什么要使用到封杀IP段呢?是什么情况下会用到封杀IP段呢?
上边实战三我们只是讲的封杀单个IP
那么会有些脑残的人写一个小小功能不断的变换IP来访问 比如一会是192.168.10.1 一会是192。168.10.2
过一会就有切换为了192.168.10.3 来回的变化
这样假设192.168.10.1设置了单个ip的key的过期时间是5s 接着切换了其他的ip 又是5s 时间一长 同样次数多了redis很快被占满并且对数据库请求也会造成一定的压力 所以严格意义上讲 我们需要搞这么一个封杀IP段的功能
直接上代码:
<?php
require("Entities/functions.php");
require("Entities/NewsInfo.php");
//判断自己的IP 所在的IP段是否被禁止
$myip=getIP();//获取自己的IP
if(isInForbiddenIPList('l'.getIPLevel($myip)))
{
header("location:waitall.html"); //直接跳转到 设定好的页面
exit();
}
//判断是否单IP被禁
if(isForbidden('p'.$myip))
{
header("location:wait.html"); //直接跳转到 设定好的页面
exit();
}
$newsid=getParameter("id");
if(!$newsid) die("错误的参数");
$news=new NewsInfo();
$getNews=$redis->StringOperator->getFromReids("news".$newsid); //从redis取
$setIncr=false;
$incrNum=0;//禁止IP加分后 的返回值
if(!$getNews) //如果木有取到
{
$getNews=getFromDB($newsid);//就从数据库取
if(!$getNews) //数据库里也没取到 //这里要加2分
{
$getNews=defaultCache; //设置为默认缓存
$incrNum=$redis->StringOperator->incrForbidden('p'.getIP(),2); //给指定的ip增加2分
$setIncr=true;//标识设置为true,防止下面再一次被加分
}
else
$getNews=json_encode($getNews);//DB取到了 就把它变成JSON格式字符串
$redis->StringOperator->setToRedis("news".$newsid,$getNews,200);//塞入缓存 ,过期时间为200秒。 测试时间,莫纠结
}
else
{
echo "from cache";
}
if(isDefaultCache($getNews))
{
!$setIncr && $incrNum=$redis->StringOperator->incrForbidden('p'.getIP(),1); //命中 默认缓存 给指定的ip增加1分
$redis->StringOperator->expireCache("news".$newsid,5);// 给原来的缓存 增加5秒时间
$getNews="别黑我了";//这里不使用exit,防止下面的程序不执行
}
//这里 做处理,如果$incrNum 达到了指定的阀值,则插入 禁止列表
if($incrNum>=forbiddenLimit)
{
//getIPLevel()函数是根据IP获取 前N段内容,默认3段 ,如192.168.10.3 只取192.168.10 目前只支持IPV4
//这样相同段的IP地址就被扔到了同一个队列里面去了
$redis->ListOperator->push('l'.getIPLevel($myip),$myip);
}
echo $getNews;
其实也是在“设置空键”“封杀单IP”的基础上来搞的 只是多了一个判断if($incrNum>=forbiddenLimit){} 这一步
权重达到我们设置的阙值 就会被扔到禁止列表里面去 然后开始的时候就会判断自己的ip地址所在的段是否已经是被禁止了的IP段
实战五:缓存预热--一次性批量插入缓存数据 老牌管道技术 pipe mode
首先你得要明白什么是“预热” 准备加热吗?
预热可以理解为在系统启动前或者启动时将数据库大量数据导入到redis当中去! 利用到redis当中的老牌功能 管道技术!
首先你得要明白为什么要玩“预热”?
假设不预热 通过上边的实战一到四我们明白 假设我们的系统很牛逼 刚开始就会有很多的人来访问 大量的有效的key会进入到数据库查询 你就想象淘宝重新开发了一个淘宝app软件 很多人去买东西 超级大的访问量 每个id起初都会去请求一次数据库再插入到redis当中 这么大的访问量对数据库的访问也是有很大压力的 为了缓解这种压力 所以就要进行“数据预热”处理!明白了吗?
可以不预热吗?
如果数据量不是很大 几万几十万条 在系统启动前 直接灌入即可 譬如写个java程序 或者 php脚本 来完成 读取db数据库 直接插入到redis当中去 但是数据量大了也会有一定的时间和性能的消耗 这里说的是数据量不大的情况下哦!
我们今天讲的是redis如何批量插入数据(pipe mode),而不是读取数据库再循环去插入到redis 因为那样真的是太low逼了!
如何实现呢?
可以根据redis协议的格式生成出一个文件 然后批量导入
请看具体实现步骤:
第一步了解协议规则:
譬如这句话:set news101 newscontent EX 200
拆分成redis格式就是 (注意换行是\r\n)
*5 //按空格拆分有几段
$3 //代表"set"的字符长度
set
$7 //代表"news101" 的字符长度
news101
$11
newscontent //代表下面的user_main有几个字符长度
$2
EX
$3
200
第二步查数据库生成复合上述协议规则的sql
第三步创建shell脚本 将sql放到里面去 并在终端执行命令
sql如下:
终端命令如下
mysql -uroot -pHuliang1991128 -D test --skip-column-names --raw < /usr/soft-hujun/bin-hujun/news.sql | redis-cli -h 127.0.0.1 -a 111111 -p 6379 --pipe解释一下子哈:
-D表示选取 test库
–skip-column-names 表示不显示列名
–raw 原生输出 < /usr/soft-hujun/bin-hujun/news.sql 表示读取该文件内容 | redis-cli -h 127.0.0.1 -a 111111 -p 6379 这个简单 就是连接redis
–pipe 表示 管道模式 该模式是为了执行大量插入而设计的 redis的管道技术看我这篇博客https://blog.csdn.net/weixin_39166924/article/details/96000843
执行完成之后
然后去看redis里面就已经有数据了哦!
小小提醒 存进去的我没弄成json格式 你用的时候最好存json格式进去 汉字自动转成16进制 你要查看中文的你得这么进入
redis-cli --raw -h 127.0.0.1 -a 111111 -p 6379
就比之前多了个–raw而已
还有另外一种方式
直接生成文件
mysql -h 192.168.222.1 -ushenyi -p123123 -D test --skip-column-names --raw < news.sql > allnews
然后执行该命令:
cat allnews | redis-cli --pipe
实战六:HASH类型保存新闻缓存--克服STRING类型弊端
前述:上边我们讲过将新闻缓存放入redis的string类型当中 键值对的形式 值可以采用json字符串的形式
比如:{news_id:1,news_name:wahaha,views:12} 这种形式的值
第一点:数据读取么问题 还tmd的特别方便
第二点:当发生新闻数据的改动时 更新 重新更新缓存 set key xxxx 也没问题 也应该这么做
第三点:但是这value里面有个点击量字段 假设用户刷新页面 其他内容不变 就要点击量+1 怎么办?
难不成你从数据库重新取一下字段 然后重新更新缓存?
或者说把缓存值取出来+1再放进去?那万一放进去的过程中其他并发进来了也改变了点击量呢?
所以行不通啊!
对于不经常改变的我们可以采用string类型来做缓存类型 对于缓存拥有多个独立字段且会发生独立变化的时候 怎么办呢?
当然是HASH类型啦 哈哈哈!
Hash类型常用命令总结:
hset key field value 设置值 你可以把key理解成一张表
hget key field 取值 从key这个表里面去field字段的值
hgetall key 获取key这个表里面所有字段的值
hkeys keys 获取所有的字段名称
hkeys values 获取所有的字段的值
一条一条执行觉得麻烦 可以写成一行来实现
hmset key field1 value1 field2 value2 ...... 一次性设置key表里面多个字段的值
hmget key filed1 filed2...... 一次性获取key表里面的多个字段的值
hincrby hnews101 views 1 直接对指定的key和字段 +1 hnews101表里面的veiws字段值加1
直接上代码:
require("Entities/functions.php");
require("Entities/NewsInfo.php");
$newsid=getParameter("id");
if(!$newsid) die("错误的参数");
$newsKey_prefix="hnews";//新闻key的前缀
$newsKey=$newsKey_prefix.$newsid; //拼凑成 一个完整的新闻key
$news=new NewsInfo();
$getNews=$redis->HashOperator->get($newsKey); //从redis取 ,注意此时 是hash类型
if(!$getNews) //如果木有取到
{
$getNews=getFromDB($newsid);//就从数据库取
$getNews=$getNews[0]; //注意这里。 各个框架或自己写的代码取值 格式不同, 要以['key'=>'value'] 这种形式的数组
if(!$getNews) //数据库里也没取到 //这里要加2分
{
//这里加入 防穿透 代码,为了代码 演示 清晰,这里暂时不加了,假设 都能取到数据
//防止穿透 去看上边的代码
}
$redis->HashOperator->set($newsKey,$getNews);//塞入缓存 hash数据类型
$redis->setExpireTime($newsKey,200);//过期时间为200秒。 测试时间,莫纠结
}
else
{
echo "from cache";
}
//假设上面OK, 则我们要增加新闻点击量
$redis->HashOperator->incr($newsKey,"views");
echo "-------------------------------------以下是内容-------- ";
echo json_encode($getNews);
插入之后就是一条条的hash数据类型到redis里面去:
hnews1001 news_id 1001 news_name 新闻1 views 11
hnews1002 news_id 1002 news_name 新闻2 views 12
点击一次 那么我们就会在程序里面通过hIncrBy(key,field,1)对key表里面的views点击量字段增加1
这样就避免了上边我们描述的问题的发生
实战七:缓存预热2--根据点击量提前插入缓存思路
我们在实战五当中讲到的缓存预热的方式 是通过命令的形式 利用mysql以及redis的管道技术 来完成的缓存预热功能
适合数据量较小的情况下 一次性全部插入到缓存当中去 那假设真的有上千万甚至上亿条记录怎么办呢
你还真把这上亿条记录都放到缓存里面去吗?
实际的做法
如果数据量非常庞大的情况下 网站启动前 先把近3个月或者几个月点击量最高的x篇新闻插入到缓存当中 当然我们也同样可以使用实战五当中redis协议的形式来插入到缓存 但是今天我们用php脚本来实现
上菜:
<?php
require("../Entities/functions.php");
require("../Entities/NewsInfo.php");
$cmd='init';
if($argc==2)
{
$cmd=$argv[1];
}
if($cmd=="init")
{
pre_init();
}
function pre_init()
{
global $redis;
$redis->pipeExec(function(Redis $redis_client){
//先从db取出 点击量数据,为了演示就简单点了 select * from news order by views desc limit 0,3 假设就取 3条,
//仅仅是为了演示 简单,不要纠结、不要纠结、不要纠结
$newsPreData=getDataBySQL("select * from news order by views desc limit 0,3 ");
foreach($newsPreData as $row)
{
$key="hnews".$row["news_id"]; //拼接key
$redis_client->hMset($key,[
"news_id"=>$row["news_id"],
"news_title"=>$row["news_title"],
"views"=>$row["views"]
]);
$redis_client->expire($key,200);
}
$redis_client->exec();
echo "done~~~";
});
}
代码里面用到了argc argv 别说你不知道干哈滴哈
真不懂点击这条链接地址看看吧https://blog.csdn.net/weixin_39166924/article/details/104121277
其实也是利用了redis里面的管道技术 当然要用管道了 因为管道就是为插入大量数据而产生的!但这不仅仅是循环插入到redis 不是哈
不是的! 自己去百度redis管道的作用和原理!
实战八:sorted set 有序集合 分离点击量
sorted set 有序集合 它里面自带了一个score 特别适合完成排行榜功能 比如要统计新闻的点击量 就非常适合使用有序集合来完成 有序集合 会自动进行排序 进行的是正排序
用到的命令如下:
zadd news 19 news101
zadd news 20 news102 //表示向news这个集合加入一个元素叫做news102 其score是20
//要知道 我们一个系统不可能只有新闻模块也许会有评论模块订单模块用户模块 这些都可以用单独的集合命名
zadd news 21 news103 22 news104 23 news105 24 news106 //一条条太麻烦 还可以一次性全干进到集合里面去
zrange news 0 -1 //取出news集合里面第一个到最后一个元素 那就是news103 news104 news105
zrange news 0 -1 withscores //取出news集合里面第一个到最后一个元素 那就是news103 news104 news105 并且带有scores排序值
zrank news news101 //news集合当中news101的排序名次 默认news集合当中是根据score进行正序排序的 得到的值如果是第一则返回0 第二个返回1 名字就是返回值+1即可
zrevrange news 0 -1 withscores //表示对news集合进行倒叙排序 本身news集合是正序的 使用zrevrange可以倒叙排序
zincrby news 1 news101 //news集合里面的news101的scroe值加1 如果元素不存在则添加
zscore news news101 //取出news集合里面的news101在news集合里面的scroe的值
zrangebyscore news 21 28 withscores //取出score范围在21到28之间的news集合里面的元素
zrevrangebyscore news 28 21 withscores //倒叙取出score范围在28到21之间的news集合里面的元素
我们讲这个有序集合叨叨了这么多命令到底有个屁用啊?
讲讲场景吧 现在领导让你把库里面的记录预热到redis缓存当中 但是还要考虑新闻点击量排名的问题你该怎么做?预热简单 就按照实战五或者实战七的思路进行预热即可 但是预热的时候不应该将点击量也预热到redis的hash数据类型里面去 这样的话你要是对点击量进行排序会很麻烦 难不成你还要将所有的新闻从redis里面取出来 然后再遍历出点击量 然后再排名吗?这样很low逼的!所以最好的办法就是 新闻的id 标题 内容预热到hash里面去 点击量预热到有序集合里面去 因为考虑到有序集合的特性
zadd news 21 news103 22 news104 23 news105 24 news106
看看吧 news 就是个集合 news103的点击量21 news104的点击量22
再通过我们上边叨叨的那些命令 即可很轻松容易的获取随时第一名谁是倒数第一
各种命令玩转so easy
上菜:
<?php
require("Entities/functions.php");
require("Entities/NewsInfo.php");
$newsid=getParameter("id");
if(!$newsid) die("错误的参数");
$newsKey_prefix="hnews";//新闻key的前缀
$newsClickSet="newsclick" ;//保存新闻 点击量集合的集合名称
$newsClickKey="news".$newsid;//保存新闻点击量 的成员名
$newsKey=$newsKey_prefix.$newsid; //拼凑成 一个完整的新闻key
$news=new NewsInfo();
$getNews=$redis->HashOperator->get($newsKey); //从redis取 ,注意此时 是hash类型
$redis->zSetOperator->setName=$newsClickSet; //设置本次取值 集合名称
if(!$getNews) //如果木有取到
{
$getNews=getFromDB($newsid);//就从数据库取 ,由于为了演示简单,统一用的是select * .所以下面要做处理
$getNews=$getNews[0]; //注意这里。 各个框架或自己写的代码取值 格式不同, 要以['key'=>'value'] 这种形式的数组
if(!$getNews) //数据库里也没取到 //这里要加2分
{
//这里加入 防穿透 代码,为了代码 演示 清晰,这里暂时不加了,假设 都能取到数据
}
$getViews=$getNews["views"];//保存数据库中的点击量
unset($getNews["views"]) ;//去除点击量 字段,因为它不存入hash类型
$redis->HashOperator->set($newsKey,$getNews);//塞入缓存,塞的是新闻数据
$redis->zSetOperator->set($getViews,$newsClickKey); //塞入缓存 ,塞得是 点击量
$redis->setExpireTime($newsKey,200);//过期时间为200秒。 测试时间,莫纠结
}
else
{
echo "from cache";
}
//假设上面OK, 则我们要增加新闻点击量
//$redis->HashOperator->incr($newsKey,"views");
//这里要使用 zSet来增加了 而不是Hash
$getNews["views"]=$redis->zSetOperator->incr($newsClickKey); //由于增加后 会返回 加完后的值,因此 赋值给$getNews作为结果
echo "-------------------------------------以下是内容-------- ";
echo json_encode($getNews);
在代码当中你会发现 我们对hash里面的新闻缓存设置了过期时间 因为hash里面的news101表里面就是针对的一条新闻记录
但是我们并没有给有序集合里面的缓存设置过期时间 因为 news集合里面的 21 news101 22 news102等着写元素
是针对多条新闻的哦!不能对news 集合设置过期时间 不然所有新闻的点击量就全部消失啦!
虽然分体式缓存时间了 新闻内容存hash 新闻经常变化的点击量存有序集合 但是一旦新闻内容hash过期 有序集合里面的点击量还是存在 虽然实现了分体式缓存 但是不同步啊! 这 就是问题所在! 那该怎么办呢?
实战九:手动清除缓存-- 订阅/发布功能实战以及事务运用
在上边的实战当中我们都设置了缓存的过期时间 到期之后redis会自动帮我们清除掉到期的缓存数据 这种可以称之为无脑缓存 但是在实际的开发过程当中并不能满足我们的需求 很多时候需要我们手动清空缓存!
发布/订阅功能常用命令:
1.A客户端
subscribe news (news代表的是频道 任意写) 此时命令行执行会卡在那不动
2.B客户端
publish news xxxx (代表向news频道发送内容)
现在我们来看php代码当中是如何订阅频道的?
//当然我这里代码不全 你可以理解成伪代码哈 只提供思路 具体代码最后我会放到个人码云上
<?php
ini_set('default_socket_timeout', -1); //不超时 ,否则监听一段时间(默认60秒) 自动退出了
require("../Entities/functions.php");
function callback($redis_cient, $chan, $msg){
echo $msg;
}
$redis->subscribe($channel,"callback");
?>
这里有需要特别注意的地方
$redis实例化的类里面我们需要重写subscribe()方法 在自己重新定义的subscribe()函数里面我们需要重新连接redis 就是开一个新的redis连接呗 因为上边我们讲过 一旦订阅了频道 那么这个链接就会卡在那里不动 什么事情都不干 就光等着其他客户端发布消息 从而获取到消息 所以要重新开一个redis链接
function subscribe(String $channel,String $callback)
{
//开一个新连接
$sub_client=new Redis();
$sub_client->connect("192.168.222.137",6379);
$sub_client->subscribe([$channel],$callback);
}
还有一个特别需要注意的地方
我们上边写的订阅频道的代码 必须要在客户端脚本的形式去运行 如果是在网站当中运行 比如在apache+php的环境下运行 是不能够的哦! 找个命令行 php aa.php 就完事 就可以订阅频道了 就这么简单
另外哈 脚本要设置永不超时 不然超时了就没用了 所以开头就是
ini_set('default_socket_timeout', -1); //不超时 ,否则监听一段时间(默认60秒) 自动退出了
以上讲的是订阅发布功能 我们接着实战八里面的问题继续来讲
新闻内容存hash 新闻经常变化的点击量存有序集合 但是一旦新闻内容hash过期 有序集合里面的点击量还是存在 虽然实现了分体式缓存 但是不同步啊! 这 就是问题所在! 那该怎么办呢?
解决这个问题的步骤如下
1.不管是hash里面的新闻内容还是有序集合里面的新闻点击量 一律不作exprie(也就是过期时间)
2.插入缓存时,除了新闻内容入hash 新闻点击量入有序集合外 专门做一个有序集合比如newscachetime 集合来存放时间戳
zadd newscachetime xxxx 101
zadd newscachetime xxxx 102
101 102直接存放的新闻id
xxxx表示的是当前时间戳加上过期时间比如200s
3.判断如果之前设置的时间戳+200s <= 当前时间戳 则代表过期了
接下来要干的就是
删除hash里面的对应的news值
删除有序集合里面点击量的值
删除有序集合里面时间戳的值
在做以上三个删除的时候 我们需要使用到事务
redis里面的事务你知道不? 来 简单的了解一下吧兄嘚!
redis里面的事务和mysql里面的事务不一样 因为redis里面的事务不具有原子性 啥意思呢?
redis里面的事务就是把多条命令放入到队列当中 最后一次性执行 但是如果第一条命令执行成功 第二条也成功 但是第三条命令没有执行成功 那么前边的不会回滚 这要是在mysql里面是会发生回滚的,然后后边的第四条命令不管你第三条成功失败还是会继续执行滴!
redis当中的事务讲的很详细很详细了
->https://www.cnblogs.com/DeepInThought/p/10720132.html
常用的几个事务相关的命令:
muti 开启事务
exec 执行事务
discard 取消事务
watch 监控key
上代码:
现在我们先来看一下读取缓存以及缓存不存在从数据库查询再入缓存这步的操作:
其实和上边的实战当中一样 除了将新闻缓存入hash以及点击量入有序集合之外 我们只不过比之前多了一步将时间戳插入到了有序集合当中 用来判断插入的新闻缓存是否过期 如果过期了咋删除hash里面的以及有序集合里面的点击量和有序集合里面的时间戳 当然下边的代码只是取和往redis里面插入数据
<?php
require("Entities/functions.php");
require("Entities/NewsInfo.php");
$newsid=getParameter("id");
if(!$newsid) die("错误的参数");
$newsKey_prefix="hnews";//新闻key的前缀
$newsClickSet="newsclick" ;//保存新闻 点击量集合的集合名称
$newsClickKey="news".$newsid;//保存新闻点击量 的成员名
$newsKey=$newsKey_prefix.$newsid; //拼凑成 一个完整的新闻key
$newsCacheKey="newscache";//用来保存 新闻缓存时间戳的
$news=new NewsInfo();
$getNews=$redis->HashOperator->get($newsKey); //从redis取 ,注意此时 是hash类型
$redis->zSetOperator->setName=$newsClickSet; //设置本次取值 集合名称
if(!$getNews) //如果木有取到
{
$getNews=getFromDB($newsid);//就从数据库取 ,由于为了演示简单,统一用的是select * .所以下面要做处理
$getNews=$getNews[0]; //注意这里。 各个框架或自己写的代码取值 格式不同, 要以['key'=>'value'] 这种形式的数组
if(!$getNews) //数据库里也没取到 //这里要加2分
{
//这里加入 防穿透 代码,为了代码 演示 清晰,这里暂时不加了,假设 都能取到数据
}
$getViews=$getNews["views"];//保存数据库中的点击量
unset($getNews["views"]) ;//去除点击量 字段,因为它不存入hash类型
//注意这个 部分 要用事务来完成。现在先简单些
$redis->HashOperator->set($newsKey,$getNews);//塞入缓存,塞的是新闻数据
$redis->zSetOperator->set($getViews,$newsClickKey); //塞入缓存 ,塞得是 点击量
$redis->zSetOperator->set(time(),$newsid,$newsCacheKey); //也是插入时间戳
}
else
{
echo "from cache";
}
//假设上面OK, 则我们要增加新闻点击量
//$redis->HashOperator->incr($newsKey,"views");
//这里要使用 zSet来增加了 而不是Hash
$getNews["views"]=$redis->zSetOperator->incr($newsClickKey); //由于增加后 会返回 加完后的值,因此 赋值给$getNews作为结果
echo "-------------------------------------以下是内容-------- ";
echo json_encode($getNews);
手动清除过期的缓存 依据是有序集合里面的时间戳是否过期 代码入下:
$redis->subscribe($channel,"callback");
做了订阅 我们需要在另一个客户单进入redis来发布消息 比如发布的是cc 那么就会执行clearNewsCache()的操作 进入另外一个客户端打开redis 直接执行publish news xxxx
(代表向news频道发送内容) 即可向news频道发布cc这个消息 另外客户端接收到该消息就会执行代码里面的clearNewsCache()函数 完成清空过期缓存的操作
<?php
ini_set('default_socket_timeout', -1); //不超时 ,否则监听一段时间(默认60秒) 自动退出了
require("../Entities/functions.php");
$channel="news";
$newCache_Time=20;//假设新闻缓存是20秒
$newsCache_key="newscache";//保存新闻时间戳的key
function callback($redis_cient, $chan, $msg){
if($msg=="cc")//清除缓存
{
clearNewsCache();
}
}
$redis->subscribe($channel,"callback");
function clearNewsCache() //获取过期的 id们
{
global $newsCache_key,$newCache_Time;
global $redis ;// 这个是我们自己封装的Redis Object
$ids=$redis->client()->zRangeByScore($newsCache_key,0,time()-$newCache_Time); //找到过期的key
//[101,102,103]
if($ids && count($ids)>0)
{
$ids_str=implode(" ",$ids);
echo "del ids:".$ids_str.PHP_EOL; //显示即将删除哪些数据
$redis->multiExec(function(Redis $redis_client) use($ids){
foreach($ids as $id)
{
$redis_client->del("hnews".$id);//删除 hash 类型的新闻数据
$redis_client->zRem("newsclick","news".$id);
$redis_client->zRem("newscache",$id);
}
});
echo "clear news cache done~~~".PHP_EOL;
}
else
echo "no expired news".PHP_EOL;
}
注意里面的函数multiExec()里面用到了redis当中的事务
//事务执行,临时写下。后面再完善
function multiExec(callable $callbak)
{
//管道执行
$this->redis_client->multi(Redis::MULTI);
$callbak($this->redis_client);
$this->redis_client->exec();
}
当然这个地方呢 你也可以不用使用订阅发布功能 针对单机而言呢 直接做个定时任务比如linux的crontab swoole的定时器 第三方的elastic-job等 定时执行脚本 删除过期缓存 多节点角度 该借点开个订阅端 定时任务定时向各个频道发送消息
当你可以理解成笨方法来实现新闻缓存过期功能 redis2.8之前我们采用这种形式 过程比较复杂 数据量大了也不是很方便 但是对于几十万几百万的数据是没啥问题的 就是麻烦点呗 一样能实现功能 不过还有更好的 就是自动的删除过期缓存机制 看下一个实战吧
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200203110818275.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl8zOTE2NjkyNA==,size_16,color_FFFFFF,t_70
实战十:自动清除缓存触发事件--keyspace基础扫盲
上边我们讲过实战九里面的做法是比较传统的麻烦的做法 也是redis2.8之前的一些做法 那么redis2.8之后就有了新的做法了哦
redis2.8之后有一个新的功能 叫做keyspace通知 触发某些事件后可以向指定的频道发送通知
如何启动该功能呢?
默认redis是禁用该功能的 因为全部开启会对cpu造成一定的损耗
找到redis的配置文件 修改如下为Ex E表示键事件通知 x表示过期事件 每当有过期键被删除的时候向订阅了的频道发送消息
修改完成之后几得去重启redis服务为了分区数据,redis里面支持数据库(db)的切换。默认连接的是0 。
配置文件中 有databases 属性可以设置数量,默认是16
1、当我们需要切换时候 可以通过 select x来进行切换(注意 是从0开始的)
2、flushall 则清空所有DB的数据。flushdb是清空当前DB
具体操作:
找个客户端 订阅 :subscribe __keyevent@0__:expired
另外开个客户端setex name 10 shenyi
当库0里面的name过期被删除的时候 订阅subscribe keyevent@0:expired的客户端就会接收到过期的键name
我们为了获取16个库里面所有过期的key都能在过期之后出发事件 所以在订阅的时候我们需要使用:
psubscribe __keyevent@*__:expired
区别在于这厮 支持通配符。仅此而已
这样不管是库0 还是库1的key发生过期 都会被监控到从而向订阅了的客户端发送消息
现在我们来看一下实战代码讲解:
第一步:预热 将数据库数据预热到redis缓存当中 采用了管道技术哈 redis里面的事务
<?php
require("../Entities/functions.php");
require("../Entities/NewsInfo.php");
$cmd='init';
if($argc==2)
{
$cmd=$argv[1];
}
if($cmd=="init")
{
pre_init();
}
function pre_init()
{
global $redis;
$redis->pipeExec(function(Redis $redis_client){
//先从db取出 点击量数据,为了演示就简单点了 select * from news order by views desc limit 0,3 假设就取 3条,
//仅仅是为了演示 简单,不要纠结、不要纠结、不要纠结
$newsPreData=getDataBySQL("select * from news order by views desc limit 0,3 ");
foreach($newsPreData as $row)
{
$key="hnews".$row["news_id"]; //拼接key
// $newsCacheKey="newscache";//用来保存 新闻缓存时间戳的
$redis_client->set("news".$row["news_id"],$row["news_id"],200); //注意这里,用了 keyspace notifications后 需要设置换过期时间 这里用的是string类型 因为我们可以获取到它的过期事件从而触发订阅发布
//这里设置的hash类型 存放新闻内容等
$redis_client->hMset($key,[
"news_id"=>$row["news_id"],
"news_title"=>$row["news_title"],
// "views"=>$row["views"]
]);
//这好比 zadd newsclick xx(点击量) news101
//集合名 我们定位 newsclick 。代表新闻点击量集合。 因为万一后面 还有 新闻评论数、点赞数呢?
$redis_client->zAdd("newsclick",$row["views"],"news".$row["news_id"]);
// $redis_client->expire($key,200); 这里不要加缓存、不要加缓存、不要加缓存
// $redis_client->zAdd($newsCacheKey,time(),$row["news_id"]); //重点部分。插入时间戳 /用了 keyspace notifications后这句话不需要了
}
$redis_client->exec();
echo "done~~~";
});
}
第二步:另起一个客户端 执行php脚本 用来订阅频道 当有过期的键的时候自动触发订阅发布消息 这个是跑起来不再关闭的一个进程哈
需要注意的几个细节上边见过 a.一下代码中的$redis->psubscribe($channel,"callback");
psubscribe()函数中重新起一个redis链接 因为订阅了一个频道之后这个链接地址会卡死在那不动 什么都干不了 一直等着消息过来才会有结果 b.下边的脚本代码要在cli模式下运行 不能在网站中运行哈!
<?php
ini_set('default_socket_timeout', -1); //不超时 ,否则监听一段时间(默认60秒) 自动退出了
require("../Entities/functions.php");
$channel="__keyevent@*__:expired"; //注意这里,频道不是news
$newCache_Time=20;//假设新闻缓存是20秒
$newsCache_key="newscache";//保存新闻时间戳的key
/*function callback($redis_cient, $chan, $msg){
if($msg=="cc")//清除缓存
{
clearNewsCache();
}
}*/
function callback($redis, $pattern, $chan, $msg){
// $chan 的形式如__keyevent@0__:expired 、__keyevent@1__:expired
//所以这里要用正则来进行匹配
if(preg_match('/^__keyevent@(\d+)__:expired$/i',$chan,$matchs))
{
$db=$matchs[1];// 取出DB 不同的DB 需要进行select
//$msg的形式 是 news101
if(preg_match('/^news(\d+)$/i',$msg,$matchID))
{
$newsID=$matchID[1];
clearNewsCacheByID($db,$newsID);
}
}
}
$redis->psubscribe($channel,"callback");
function clearNewsCacheByID($db=0,$newsID) //获取过期的 id们 ,这是前面课程的清楚缓存函数
{
global $redis ;// 这个是我们自己封装的Redis Object
$redis->selectDB($db);//选择数据库 ,重要
$redis->multiExec(function(Redis $redis_client) use($newsID){
$redis_client->del("hnews".$newsID);//删除 hash 类型的新闻数据
$redis_client->zRem("newsclick","news".$newsID);
});
echo "clear news cache,newsID=$newsID done~~~".PHP_EOL;
}
function clearNewsCache() //获取过期的 id们 ,这是前面课程的清楚缓存函数
{
global $newsCache_key,$newCache_Time;
global $redis ;// 这个是我们自己封装的Redis Object
$ids=$redis->client()->zRangeByScore($newsCache_key,0,time()-$newCache_Time); //找到过期的key
//[101,102,103]
if($ids && count($ids)>0)
{
$ids_str=implode(" ",$ids);
echo "del ids:".$ids_str.PHP_EOL; //显示即将删除哪些数据
$redis->multiExec(function(Redis $redis_client) use($ids){
foreach($ids as $id)
{
$redis_client->del("hnews".$id);//删除 hash 类型的新闻数据
$redis_client->zRem("newsclick","news".$id);
$redis_client->zRem("newscache",$id);// 这句不需要了。由于使用了keyspace notifications
}
});
echo "clear news cache done~~~".PHP_EOL;
}
else
echo "no expired news".PHP_EOL;
}
第三步:用户请求时候走的代码 就是我们刚开始实战一当中讲的过程 有就从缓存redis当中去 没有从数据库去再入redis 这里用到了string类型存news101 设置过期时间 以便订阅发布功能能检测到是否过期 从而删除hash和有序集合里面的值
另外就是存新闻的hash 以及 点击量的有序集合了
<?php
require("Entities/functions.php");
require("Entities/NewsInfo.php");
$newsid=getParameter("id");
if(!$newsid) die("错误的参数");
$newsKey_prefix="hnews";//新闻key的前缀
$newsClickSet="newsclick" ;//保存新闻 点击量集合的集合名称
$newsClickKey="news".$newsid;//保存新闻点击量 的成员名
$newsKey=$newsKey_prefix.$newsid; //拼凑成 一个完整的新闻key
$newsCacheKey="newscache";//用来保存 新闻缓存时间戳的
$news=new NewsInfo();
//$getNews=$redis->HashOperator->get($newsKey); //从redis取 ,注意此时 是hash类型
$getNews=$redis->StringOperator->getFromReids("news".$newsid);//使用 keyspace notifications 后。存入的是String 类型,key=news101
$redis->zSetOperator->setName=$newsClickSet; //设置本次取值 集合名称
if(!$getNews) //如果木有取到
{
$getNews=getFromDB($newsid);//就从数据库取 ,由于为了演示简单,统一用的是select * .所以下面要做处理
$getNews=$getNews[0]; //注意这里。 各个框架或自己写的代码取值 格式不同, 要以['key'=>'value'] 这种形式的数组
if(!$getNews) //数据库里也没取到
{
//这里加入 防穿透 代码,为了代码 演示 清晰,这里暂时不加了,假设 都能取到数据
}
$getViews=$getNews["views"];//保存数据库中的点击量
unset($getNews["views"]) ;//去除点击量 字段,因为它不存入hash类型
//注意这个 部分 要用事务来完成。现在先简单些
$redis->StringOperator->setToRedis("news".$newsid,$newsid,5);//假设存200秒
$redis->HashOperator->set($newsKey,$getNews);//塞入缓存,塞的是新闻数据
$redis->zSetOperator->set($getViews,$newsClickKey); //塞入缓存 ,塞得是 点击量
// $redis->zSetOperator->set(time(),$newsid,$newsCacheKey); //也是插入时间戳 /这句话不需要了
}
else
{
echo "from cache";
$getNews=$redis->HashOperator->get($newsKey);
}
//假设上面OK, 则我们要增加新闻点击量
//$redis->HashOperator->incr($newsKey,"views");
//这里要使用 zSet来增加了 而不是Hash
$getNews["views"]=$redis->zSetOperator->incr($newsClickKey); //由于增加后 会返回 加完后的值,因此 赋值给$getNews作为结果
echo "-------------------------------------以下是内容-------- ";
echo json_encode($getNews);
实战十一:基于自动清除缓存机制的其他联想到的案例
1、订单自动关闭。 订单创建后设置订单号key,过期后自动修改数据库 订单状态
2、用户注册送虚拟币 用户注册成功后,设置用户IDKey,过期时间可以根据用户分类。这样可以分时间段执行赠送虚拟币。减轻并发压力
3、注销账户 redis里面存一个用户ID的key。每当用户登录或发评论,更新过期时间。 超过这个时间段后(譬如半年),自动注销账户
4、缓存失效后,自动“预热” 结合消息队列。当缓存失效后,发送消息给消息队列。x分钟后自动预热
5、其他各种 可以结合 “定时”完成的任务
实战十二:缓存中的锁来 实现防止库存变负数 -- 简单版本 上锁
缓存可以保存要延时显示或者被处理的数据 也可以保存我们的一些业务数据比如点击量比如库存等
库存 在并发比较大的系统当中 我们是将他放到redis当中去的 因为并发大 不可能每个请求都去数据库查 这样对数据库的压力太大了
假设在商城里面 用户下单了我们就去减库存 比如现在库存还有一件 a用户和b用户同时去抢购这一件商品 目前我们没对程序做任何的处理
万一a用户的网络延迟 卡顿了两秒 这个时候b用户下单了并且成功的减去了库存比如1 那么现在是0 然后当a用户延迟结束之后也会去减库存
代码实现上如下所示:
获取商品id 去redis当中的有序集合查询库存
判断库存是否小于等于0
如果是 则 提示库存不足
如果不是 则执行减库存操作
正如上边所描述的 那么这时候的库存就变成了负数-1 针对这种问题该如何解决呢?
上菜:
$redis->client()->setnx(“lock”,1)
redis当中设置一个字符串类型的值 当第一个并发进来之后锁定该字符串的值 对了 首先你得明白setnx()的特性哈 setnx(key,value)只有key的值没有被设置过才会成功返回1 如果已经被设置了再去设置就会返回false
第一个并发进来 上锁 然后处理完之后解锁 然后第二个并发进来再上锁 处理完之后再解锁 这样就避免了库存出现负数的情况 因为在网络延时的时候也是上了锁的 别人是无法操作的哦!
require("../Entities/functions.php");
$resObj=new stdClass();
header("Content-type:application/json");
if(!isset($_POST["id"])) exit("no");
$prod_id=intval($_POST["id"]);
$prodKey="prod".$prod_id; // 拼凑成 news101 这样的key
$redis->zSetOperator->setName="stock";//设置sorted list的key 名称
while(!$redis->client()->setnx("lock",1)){
usleep(100000); // 1s=1000毫秒 1毫秒=1000微秒
}
$current_stock=$redis->zSetOperator->get($prodKey);
if($current_stock<=0)
{
$resObj->msg="no stock";//代表没库存
$resObj->result=$current_stock;
}
else{
if(isset($_GET["delay"]) && intval($_GET['delay'])==1) //这里开始模拟卡顿了。 假设你判断后,正好进来了很多并发 或者你卡顿了
{
sleep(2); //模拟卡顿2秒
}
$ret=$redis->zSetOperator->incr($prodKey,-1);
$resObj->msg="OK";//代表没库存
$resObj->result=$ret; //这里返回减去1后的库存
}
//这里不要忘记释放锁 不然就会出现死锁的现象 不释放 那么第二个并发永远进不到程序里面来 都是在while循环当中转悠
$redis->client()->del("lock");//释放锁
exit(json_encode($resObj));
在上边几个实战的基础之上呢 只是多了一个string类型的锁 为了防止商品库存变负数 仅此而已! 多少几句
如果要采用自动清除分体式缓存的方法 hash里面存商品信息 有序集合里面存库存 string类型里面存放商品id并设置过期时间
然后通过订阅发布的自动获取过期事件来自动清除过期的缓存 在以上基础之上为了防止商品库存变负数 然后再设置一个string类型的锁即可!
为了让读者更加清晰 这里弄了几行伪代码:
while(!$redis->client()->setnx('lock',1)){
//不抢到锁 打死不退出循环
usleep(100000);//休眠100毫秒 继续抢锁
}
//循环下边是各种 实际业务的执行
xxx
xxx
xxx
//执行完成了 可以删除lock锁了 让下一个人来抢
$redis->client()->del('lock');
这是一种比较简单的不能再简单的处理库存变负数的方法 但是有弊端 假设服务器宕机或者其他问题 永远无法释放锁 那么就出现了死锁 其他用户永远都无法下单 这就蛋疼了 如何解决呢?
实战十三:缓存中的锁 如何防止出现死锁
上边讲述了如何在redis当中使用锁的机制来防止库存变负数 但是也存在弊端
问题:看代码里面的注释
while(!$redis->client()->setnx('lock',1)){
//不抢到锁 打死不退出循环
usleep(100000);//休眠100毫秒 继续抢锁
}
//循环下边是各种 实际业务的执行
xxx
xxx
xxx
//假设在这里 某些业务代码导致卡顿了很久 或者由于某些原因 当前请求线程或者进程崩溃了那么下边删除锁的代码就不会被执行 那么后边的并发就永远在while循环里面晃悠 这就是出现了死锁!
//执行完成了 可以删除lock锁了 让下一个人来抢
$redis->client()->del('lock');
假设真的像上述代码当中提到的 在处理业务的时候进程崩溃了 无法删除锁 其他并发都会在while循环里面等待 一直循环 就真的成为了死锁了 该如何解决呢? 很多人会想到给string类型的锁加上过期时间 那么该如何加呢? 你既要保证setnx()的特性 又要加上过期时间!
set key value EX 100 NX 设置键的值 并设置了过期时间为100s 并且一旦设置在设置的话就会返回false
很多人没用过 只是用了set key value EX 100
那么php当中该如何调用呢?
php操作redis的手册:https://github.com/phpredis/phpredis#set
// Will set the key, if it doesn’t exist, with a ttl of 10 seconds
$redis->set(‘key’, ‘value’, [‘nx’, ‘ex’=>10]);
解决啦!
php操作redis更可以直接操作原生命令 方式如下:
$redis->rwaCommand(“set”,“name”,“huxiaobai”)
https://github.com/phpredis/phpredis 这里面都有的哈
上菜:
require("../Entities/functions.php");
$resObj=new stdClass();
header("Content-type:application/json");
if(!isset($_POST["id"])) exit("no");
$prod_id=intval($_POST["id"]);
$prodKey="prod".$prod_id; // 拼凑成 news101 这样的key
$redis->zSetOperator->setName="stock";//设置sorted list的key 名称
//防止死锁出现重点在这里 就算是处理业务逻辑的进程崩溃了 因为我们设置了锁的过期时间 也不会出现死锁 导致任何人都无法下单无法减去库存的现象发生 当然你设置的这个过期时间一定得是保障正常业务逻辑走完的时间才行
while(!$redis->client()->set("lock",1,["NX","EX"=>2])){
usleep(100000); // 1s=1000毫秒 1毫秒=1000微秒
}
$current_stock=$redis->zSetOperator->get($prodKey);
if($current_stock<=0)
{
$resObj->msg="no stock";//代表没库存
$resObj->result=$current_stock;
}
else{
if(isset($_GET["delay"]) && intval($_GET['delay'])==1) //这里开始模拟卡顿了。 假设你判断后,正好进来了很多并发 或者你卡顿了
{
sleep(2); //模拟卡顿2秒
}
$ret=$redis->zSetOperator->incr($prodKey,-1);
$resObj->msg="OK";//代表没库存
$resObj->result=$ret; //这里返回减去1后的库存
}
//这里不要忘记释放锁 不然就会出现死锁的现象 不释放 那么第二个并发永远进不到程序里面来 都是在while循环当中转悠
$redis->client()->del("lock");//释放锁
exit(json_encode($resObj));
当然你设置的这个锁的过期时间一定得是保障正常业务逻辑走完的时间才行
别以为这样就可以解决防止出现死锁 并且 防止库存变负数 这样是不行滴!
会出现以下几个问题:
a.卡顿时间如果超过锁本身的过期时间 那么锁就会被别人拿走了 因为锁过期了 别的并发进来就不会继续走while循环
b.别人操作完成后 你这时候不卡顿了 继续执行下方的,又会出现上边讲过的负数的问题
c.同样,锁被别人拿走了 你很有可能删除掉别人创建的锁
这都是会遇到的问题 并且几率还非常的高!
防止库存变负数 就会造成死锁现象发生 防止了死锁发生吧又会出现库存变负数 真他吗矛盾 那该如何最好的解决呢?
实战十四:利用事物防止误锁 防止库存变负数
实战十三已经把问题提出来了,该如何解决呢? 我们先来复习一下事务 上边也提到过事务 也共享过事务的博客文章 用到的常用命令 multi exec discard 和watch 今天我们重点来看一下watch的功能
因为redis当中的事务不支持回滚 所以就多了一个watch
监控一个或者多个键,一旦其中有一个键被删除 修改 覆盖 过期了排除在外哈 接下来如果你执行了multi…exec
就会失败。监控一直持续到exec命令执行就取消监听(所有被watch的可以) 断开当前链接就会取消watch对键的监控 以及 执行完exec也会取消watch对键的监控!
watch 和事务配合来执行的 单独执行没有任何意义!
上菜:
整体思路:还是要上锁的 并且给锁设置过期时间(string类型) 这一点和上边实战当中是一样的 抢着锁了才能减少库存也就是代码当中while()循环里面的!
一旦抢着锁 那么利用watch对它进行监控 监控住锁并不是说别人不能去修改他 而是当他被修改之后 那么接下来的事务就会被取消不执行! 因为在接下来执行业务逻辑的时候可能会出现卡顿 或者其他的并发也进来了恰好锁也到期了 锁被其他并发抢走的情况发生 对于a用户来说抢找了锁 但是在业务处理上卡顿了造成锁过期 那么锁就会被并发进来的b用户抢走 b用户正常执行完业务逻辑是要删除锁的 那么当a卡顿完继续往下走的时候发现watch监控的锁被修改了 那么这个时候它的事务里面的代码就不会执行了 multi exec 都会放弃执行!那就代表这a用户没有下单成功没有秒杀到呗!
require("../Entities/functions.php");
$resObj=new stdClass();
header("Content-type:application/json");
if(!isset($_POST["id"])) exit("no");
$prod_id=intval($_POST["id"]);
$prodKey="prod".$prod_id; // 拼凑成 news101 这样的key
$redis->zSetOperator->setName="stock";//设置sorted list的key 名称
$myid=session_create_id();
//此处上锁并且设置过期时间 a用户进来了lock没有被设置 那么抢走锁继续走下边的逻辑代码 在a用户执行过程中b用户并发进来了 走到这一看锁在a用户手里 就会while里面循环几趟 a用户完事了会释放锁 b在继续接下来的正常的业务逻辑 发证都他妈得一个一个得来!
while(!$redis->client()->set("lock",$myid,["NX","EX"=>2]))
{
usleep(100000) ;//1 秒==1000毫秒 1毫秒=1000微妙
}
//这里对用户获取到的锁进行watch监控 一旦被修改 那么接下来的业务里面的事务就都放弃执行 啥时候会被修改呢?
//a用户卡顿啦或者进来了很多并发并且锁到期了 锁自动释放 b用户获取到锁 那么a用户卡顿完成之后的业务逻辑里面的事务操作将放弃执行
$redis->client()->watch("lock");
$current_stock=$redis->zSetOperator->get($prodKey);
if($current_stock<=0)
{
$resObj->msg="no stock";//代表没库存
$resObj->result=$current_stock;
//如果库存为0 那么也是要执行事务 删除掉锁
$redis->client()->multi(Redis::MULTI)
->del("lock")
->exec();
}
else{
if(isset($_GET["delay"]) && intval($_GET['delay'])==1) //这里开始模拟卡顿了。 假设你判断后,正好进来了很多并发 或者你卡顿了
{
sleep(5); //模拟卡顿5秒
}
//你可以在这里利用redis的队列 将秒杀成功的人放到队列里面去 然后用定时任务也好swoole里面的自定义用户进程也好 去跑去生成订单 资金流水等操作
//正常逻辑 减掉库存 事务操作
$ret=$redis->client()->multi(Redis::MULTI)
->zIncrBy("stock",-1,$prodKey) //member 是prod101
->del("lock")
->exec();
if($ret)
{
$resObj->msg="OK";//代表OK
$resObj->result=$ret[0]; //这里返回减去1后的库存
}
else{
$resObj->msg="canceled";//代表没有执行
$resObj->result=$redis->zSetOperator->get($prodKey); //这里返回减去1后的库存
}
}
exit(json_encode($resObj));
实战十五:HyperLogLog类型使用来统计每日注册人数
首先了解一下HyperLogLog类型 HLL是一个基数统计算法 譬如统计的ip访问量/每天 每周或者每月注册用户数
这列功能你会使用mysql来统计 但是如果让你统计一张千万级别的数据库里面的数据 效率会是很慢的哦!我试过!
先看命令吧:
pfadd 20200101 lishi
pfadd 20200101 lishi
pfadd 20200101 zhangshan //成功返回1 pfadd 向HyperLogLog类型的元素里面添加元素
pfcount 20180101 // 结果会是2 pfcount 统计一个HyperLogLog类型的元素的值的个数 统计的时候会自动对重复数据去重操作
//比如 我们要统计今天网站的注册用户数量 就可以将新注册的用户放到一个HyperLogLog的数据类型里面去 然后通过pfcount的命令来获取这个元素里面的个数 完成基数统计的功能
//现在来研究一下pfmerge
pfadd h1 1 2 3
pfadd h2 3 4 5
pfmerge h3 h1 h2 // 表示将h1和h2合并到h3当中 最后的结果是1 2 3 4 5 会对重复的去重处理
pfcount h3 //结果会是5 3有一个重复的 会自动去重
利用HyperLogLog我们能实现什么功能? 比如要统计一个月以来的用户的注册的数量 我们可以将每天注册的用户的数量pfadd
到hyperloglog类型的元素里面去 最后通过pfmerge整合到一个hyperloglog里面去
通过pfcount统计出最终的结果即可! php操作hyperloglog的实例就不写了
自己去https://github.com/phpredis/phpredis#hyperloglogs
看吧
pfadd 20200101 zhangsan
keys * 会出现20200101的键
type 20200101 会显示string类型
上边的案例也就是说hyperloglog里面的元素都会被转成string类型存储
所以我们当统计完数据之后需要删除的时候 直接执行 del 20200101 即可清楚缓存数据