最近对一些小功能比较感兴趣,时不时的脑海里会涌现出一两个比较新奇的点子。然后不由自主的会去思考,用哪种方式进行实现,做一个原型出来。秉承好记性不如烂笔头的传统,这里整理下,也为了今后来复习巩固。
列表的上移与下移
如图,这里以Redis配合PHP做了一个简单的版本,算是一个有个小心脏的麻雀吧。
设计思路:
排序的key为zset
: score(列表的位置), member(查看列表详细的hash后缀)
存储的key为:info: member
, 是一个hash结构。
下面看看大致的数据原料:
127.0.0.1:6379> keys *
1) "zset"
2) "info:eeeee"
3) "info:bbbbb"
4) "info:ddddd"
5) "info:ccccc"
6) "info:aaaaa"
127.0.0.1:6379> zrange zset 0 -1 withscores
1) "bbbbb"
2) "12345"
3) "ddddd"
4) "23456"
5) "eeeee"
6) "34567"
7) "ccccc"
8) "45678"
9) "aaaaa"
10) "56789"
127.0.0.1:6379> hgetall info:aaaaa
1) "name"
2) "biaobiao"
3) "age"
4) "23"
5) "address"
6) "liaoning_dalian"
127.0.0.1:6379>
然后看看PHP对列表操作的实现,因为只是演示,就不考虑性能了。代码规范不得不提,这段代码有点随意,以此为戒哈哈。
<?php $redis = new Redis(); $redis->connect("127.0.0.1", 6379); $orders = $redis->zrange("zset", 0, -1, true); foreach($orders as $member => $score) { if(intval($score)<=0 || empty($member)) { continue; } //$infoarray = $redis->hgetall("info:".$member); $info = $redis->hget("info:".$member, "name"); //foreach($infoarray as $field => $value) { // $info.=", {$field}={$value}"; //} $row = "order {$score}, info: {$info} | <a href='index.php?operation=up&cursortid={$member}'>上移</a> | <a href='index.php?operation=down&cursortid={$member}'>下移</a><br>"; echo $row; } /** * 上移下移实现 * */ $cursortid = $_GET['cursortid']; if($_GET['operation'] == "up") { //上移 $prevsortid = getPrevSortid($redis, $cursortid); //echo "<mark>cur:{$cursortid}, prev:{$prevsortid}</mark>"; swapSortid($redis, $cursortid, $prevsortid); }elseif($_GET['operation'] == "down") { // 下移 $nextsortid = getNextSortid($redis, $cursortid); //echo "<mark>cur:{$cursortid}, prev:{$nextsortid}</mark>"; swapSortid($redis, $cursortid, $nextsortid); } function swapSortid($redis, $oldid, $newid) { if($oldid == $newid) { return; } if(empty($oldid) || empty($newid)) { return; } $oldscore = $redis->zscore("zset", $oldid); $newscore = $redis->zscore("zset", $newid); $redis->zadd("zset", $newscore, $oldid); $redis->zadd("zset", $oldscore, $newid); } function getPrevSortid($redis, $sortid) { $sortids = $redis->zrange("zset", 0, -1); if(empty($sortids)) { return; } $ret = $sortids[0]; for($index =0; $index < count($sortids)-1; $index++) { if($sortids[$index+1] == $sortid) { $ret = $sortids[$index]; } } return $ret; } function getNextSortid($redis, $sortid) { $sortids = $redis->zrange("zset", 0, -1); if(empty($sortids)) { return; } $ret = $sortids[count($sortids)-1]; for($index = 0; $index < count($sortids)-1; $index++) { if($sortids[$index] == $sortid) { $ret = $sortids[$index+1]; } } return $ret; }
2018年6月13日23:19:02
签到服务设计
现在很多的APP都会有这么一个功能,用来提升日活,签到得积分,签到返礼物等模式也在一定程度上能刺激用户的消费。但是不同的APP适用的场景也不尽相同,最直观的就是“累积登录”,还是“累积连续登录”。
累计登录count-details.php
<?php /** * 签到场景:显示具体哪天签到,以及累计签到天数,无需统计连续天数。 * */ $redis = new Redis(); $redis->connect("127.0.0.1", 6379); $userid = 2614677; $key = "signup:countdetails:list:"; echo "<h3><a href='count-details.php?operation=signup'>点我签到</a></h3>"; if($_GET['operation'] == "signup") { $lastdate = $redis->lindex($key.$userid, -1); if($lastdate == date("Ymd")) { $ret = "今日已经签过到了~"; }else if(strtotime($lastdate) > strtotime(date("Ymd"))) { $ret = "签到日期有误,不能签之前的到的~"; }else{ ; $redis->rpush($key.$userid, date("Ymd")); $ret = "恭喜您签到成功啦~"; } echo "<mark>".$ret."</mark>"; } $daylist = $redis->lrange($key.$userid, 0, -1); $daycount = count($daylist); $html = "用户{$userid}累计签到{$daycount}天,详细清单:<br><ul>"; foreach($daylist as $day) { $html.= "<li>{$day}</li>"; } $html.="</ul>"; echo $html;
累积连续登录count-only.php
<?php /** * 借助Redis实现签到程序 * */ $redis = new Redis(); $redis->connect("127.0.0.1", 6379); $key = "signup:countonly:hash:"; $userid = 2614677; /** 数据结构设计 * hash: * count => N, // 累积连续签到天数 * lastdate => date("Ymd"), // 上次签到日期 **/ // 输出表单页面 $info = $redis->hgetall($key.$userid); $count = intval($info['count']); $lastdate = strval($info['lastdate']); $html = <<< EOF 用户{$userid} <a href='count-only.php?operation=signup'>点我签到</a> 吧~, 截止今天已累计签到{$count}天~ EOF; echo $html; if($_GET['operation'] == "signup") { // 检查今日是否签到 $ret = ""; if(strtotime(date("Ymd")) < strtotime($lastdate)) { // 签到日期不合法 $ret = "签到日期小于当前日期啦~"; }else if($lastdate == date("Ymd")) { // 今日已经签到过了 $ret = "您今天已经签过到啦~"; }else{ // 符合签到要求,正常处理 if(strtotime(date("Ymd")) - strtotime($lastdate) <= 86400) { $redis->hincrby($key.$userid, "count", 1); $redis->hset($key.$userid, "lastdate", date("Ymd")); $ret = "今日签到成功,快去领取签到奖励吧~"; }else{ $redis->hmset($key.$userid, array("count"=>1, "lastdate"=>date("Ymd"))); $ret = "因签到中断,so重置下累计登录天数~"; } } echo $ret; }
优化版本(count-bitway.php
)
上面两个例子,相对而言消耗的存储资源比较大,因此在用户量巨大的场景下,徐亚特别考虑好Redis的QPS以及系统的负载压力。因此比较适用于短期的业务场景。对于长期统计签到的服务就不适用了。而bit方式则对这种情况比较适用,用到的方法是setbit
, getbit
, bitoount
。
<?php /** * 签到对用户量很大的服务来说是一个很耗资源的功能。下面使用setbit, getbit, bitcount实现一个适用于“活动”场景的签到功能。 * */ $redis = new Redis(); $redis->connect("127.0.0.1", 6379); $userid = 2614677; $key = "signup:countbitway:"; /** * 重点是offset 的计算,即以当前天数减去活动上线当天的天数,作为offset。后续会用于计算哪天签到,累积签到日期数。 * */ $startday = "20180613"; echo "<h3><a href='count-bitway.php?operation=signup'>点我签到</a></h3>"; $offset = intval(strtotime(date("Ymd")) - strtotime($startday))/86400; $count = $redis->bitcount($key.$userid); $html = "用户{$userid}累积签到{$count}天,清单如下:<br><ul>"; for($index=0; $index <= $offset; $index++) { if($redis->getbit($key.$userid, $index)){ $tempdate = date("Y-m-d", strtotime($startday) + 86400*$index); $html.="<li>".$tempdate."</li>"; } } $html .="</ul>"; echo $html; if($_GET['operation'] == "signup") { $issignuped = intval($redis->getbit($key.$userid, $offset)); if($issignuped) { // 今日已签到 $ret = "今日已签到~"; }else{ $redis->setbit($key.$userid, $offset, 1); $ret ="恭喜您签到成功~"; } echo "<mark>{$ret}</mark>"; }
基本上这三个例子都适用于不同的场景,具体的业务具体分析吧,没有最好的,只有更合适的。
漂流瓶
记得上高一的时候特别喜欢玩QQ邮箱的漂流瓶,突然发现微信竟然也有了,然后就有了兴趣,试着自己做了一个简易的原型出来。
drifting-bottle.php
<?php /** * 简易版漂流瓶实现 * */ $redis = new Redis(); $redis->connect("127.0.0.1", 6379); $bottleidpool = "bottleidpool:set"; i?>
<div>
<div><textarea id="bottlecontent" cols=66 rows=7>漂流瓶的海洋~</textarea>
<input type="hidden" id="bottleid">
</div>
<hr>
<div>
<input type='button' value='扔一个' onclick='throwbottle()' />
<input type='button' value='捞一个' onclick='catchbottle()' />
<input type='button' value='回复' onclick='replybottle()' />
<input type="button" value="我的" onclick="minebottles()" />
</div>
</div>
<script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
<script> function throwbottle() { var content = $("#bottlecontent").val(); $.ajax({ url: "drifting-bottle-action.php", method: "POST", dataType: "JSON", data: {"userid": 2614677, "content": content, "action": "throw"}, success: function(data) { console.log(data); if(data.code == 200) { $("#bottlecontent").val(data.result); } }, error: function(err) { console.log(err); } }); } function catchbottle() { $.ajax({ url: "drifting-bottle-action.php", method: "POST", data: {"userid": 2614677, "action": "catch"}, dataType: "JSON", success: function(data) { console.log(data); if(data.code == 200) { var bottle = data.result.bottle; var content = "From:" + bottle.owner + ", content:" + bottle.content +"\n Replies: "; for(var index=0; index < data.result.replies.length; index++) { content += "\tReplier: " + data.result.replies[index].replyid + ", replycontent: " + data.result.replies[index].replycontent + ";\n" } $("#bottlecontent").val(content); $("#bottleid").val(bottle.bottleid); } }, error: function(err) { console.log(err); } }); } function replybottle() { var bottleid = $("#bottleid").val(); var reply = $("#bottlecontent").val(); if(bottleid == "") { alert("必须先捞一个,才能回复哦~"); return; } alert("请在文本域填写您的回复信息吧~"); $.ajax({ url: "drifting-bottle-action.php", method: "POST", data: {"userid": 2614677, "action": "reply", "bottleid": bottleid, "reply": reply}, dataType: "JSON", success: function(data) { console.log(data); if(data.code == 200) { $("#bottlecontent").val(data.content); } }, error: function(err) { console.log(err); } }); } function minebottles() { $.ajax({ url: "drifting-bottle-action.php", method: "POST", data: {"userid": 2614677, "action":"mine"}, dataType: "JSON", success: function(data) { console.log(data); if(data.code == 200) { var bottles = data.result; var str = ""; for(var index=0; index < bottles.length; index++) { str += "From: " + bottles[index].owner + ", content: " + bottles[index].content + "\n"; } $("#bottlecontent").val(str); } }, error: function(err) { console.log(err); } }); } </script>
drifting-bottle-action.php
<?php $action = $_REQUEST['action']; $userid = intval($_REQUEST['userid']); $redis = new Redis(); $redis->connect("127.0.0.1", 6379); $bottleidpool = "bottleidpool:set"; $bottlecontainer = "bottlecontainer:hash"; $mybottlekey = "bottleofmine:zset:"; $bottlereply = "bottlereply:"; $ret = array("code"=>-1, "result"=>"服务器异常啦,待会再试试吧~"); if($action == "throw") { $content = $_REQUEST['content']; // 生成UUID,记录到池子和我的两个模块中。 $uniqid = uniqid(); $bottle = array( "bottleid" => $uniqid, "content" => $content, "owner" => $userid, ); $redis->hset($bottlecontainer, $uniqid, json_encode($bottle)); $redis->sadd($bottleidpool, $uniqid); $redis->zadd($mybottlekey.$userid, time(), $uniqid); $ret = array("code"=>200, "result"=>"您的瓶子已经飘到了800里开外啦~"); }else if($action == "catch") { // srandmember $bottleid = $redis->srandmember($bottleidpool); $bottle = json_decode($redis->hget($bottlecontainer, $bottleid), true); $replies = array(); foreach($redis->lrange($bottlereply.$bottleid, 0, -1) as $reply) { array_push($replies, json_decode($reply, true)); } $ret = array("code"=>200, "result"=>array("bottle"=>$bottle, "replies"=>$replies)); }else if($action == "mine") { //返回我扔出去的所有瓶子 $bottleids = $redis->zrevrange($mybottlekey.$userid, 0, -1); $bottles = array(); foreach($bottleids as $bottleid) { array_push($bottles, json_decode($redis->hget($bottlecontainer, $bottleid), true)); } $ret = array("code"=>200, "result"=>$bottles); }else if($action == "reply") { $bottleid = $_REQUEST['bottleid']; $reply = $_REQUEST['reply']; $row = array( "replyid" => $userid, "replycontent" => $reply ); $redis->lpush($bottlereply.$bottleid, json_encode($row)); $ret = array("code"=>200, "result"=>"恭喜您回复瓶子成功啦~"); } echo json_encode($ret);
实现效果
next
不定期更新~