<?php
/**
*......................位图......................
* _oo0oo_
* o8888888o
* 88" . "88
* (| -_- |)
* 0\ = /0
* ___/`---'\___
* .' \\| |// '.
* / \\||| : |||// \
* / _||||| -卍-|||||- \
* | | \\\ - /// | |
* | \_| ''\---/'' |_/ |
* \ .-\__ '-' ___/-. /
* ___'. .' /--.--\ `. .'___
* ."" '< `.___\_<|>_/___.' >' "".
* | | : `- \`.;`\ _ /`;.`/ - ` : | |
* \ \ `_. \_ __\ /__ _/ .-` / /
* =====`-.____`.___ \_____/___.-`___.-'=====
* `=---='
*
*..................佛祖开光 ,永无BUG...................
*
* Description: 请简单描述
* Author: Shuxiaoyuan
* Email: sxy@shuxiaoyuan.com
* Website: https://www.shuxiaoyuan.com
* DateTime: 2023/2/28 15:52
*/
namespace App\Http\Controllers\Redis;
use App\Common\Tools;
use App\Constants\ErrorCode;
use App\Constants\RedisDemo\RedisDemo;
use App\Facades\ToolsFacade;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Validator;
/**
* Description: Redis 位图
* Author: Shuxiaoyuan
* Email: sxy@shuxiaoyuan.com
* DateTime: 2021/12/29 17:38
*
* 基础知识:
* 1TB = 1024GB
* 1GB = 1024MB
* 1MB = 1024KB
* 1KB = 1024B (byte)
* 1B = 8b (bit位)
*
* 适用场景:
* 签到1天送1积分,连续签到2天送2积分,3天送3积分,3天以上均送3积分等。
* 如果连续签到中断,则重置计数,每月初重置计数。
* 当月签到满3天领取奖励1,满5天领取奖励2,满7天领取奖励3……等等。
* 显示用户某个月的签到次数和首次签到时间。
* 在日历控件上展示用户每月签到情况,可以切换年月显示……等等。
*
* 最大位只能设置为 2^32 = (这会将位图限制为 512MB)
*
* Class RedisBitmapController
*
* @package App\Http\Controllers\Redis
*/
class RedisBitmapController
{
public string $key = RedisDemo::REDIS_BITMAP;
public string $tmp_key = RedisDemo::REDIS_BITMAP . ':tmp';
/**
* Description: 设置相应位为0或者1
* Author: Shuxiaoyuan
* Email: sxy@shuxiaoyuan.com
* Website: https://www.shuxiaoyuan.com
* DateTime: 2023/4/14 18:00
*
* @param Request $request
*
* @return array
*/
public function setBit(Request $request): array
{
$key = $request->input('redis_key');
if (!$key) {
$key = $this->tmp_key;
}
$bit = (int)$request->input('bit', 66);
// 返回值:0或1,存储在偏移量的原始位值
Redis::setbit($key, $bit, 1);
$status = Redis::getbit($key, $bit);
$data = [
'bit' => $bit,
'redis_key' => $key,
'status' => $status,
'bitcount' => Redis::bitcount($key), // 一共有多少位设置成 1
];
Redis::expire($key, 8 * 2400);
return Tools::outSuccessInfo($data);
}
/**
* Description: 获取相应位是0还是1
* Author: Shuxiaoyuan
* Email: sxy@shuxiaoyuan.com
* Website: https://www.shuxiaoyuan.com
* DateTime: 2023/4/14 18:07
*
* @param Request $request
*
* @return array
*/
public function getBit(Request $request): array
{
$key = $request->input('redis_key');
if (!$key) {
$key = $this->tmp_key;
}
$bit = (int)$request->input('bit', 66);
$status = Redis::getbit($key, $bit);
$data = [
'bit' => $bit,
'redis_key' => $key,
'status' => $status,
'bitcount' => Redis::bitcount($key), // 一共有多少位设置成 1
// 'value' => Redis::get($key), // 编码需要转换
];
return Tools::outSuccessInfo($data);
}
/**
* Description: 获取指定范围内1的个数
* Author: Shuxiaoyuan
* Email: sxy@shuxiaoyuan.com
* Website: https://www.shuxiaoyuan.com
* DateTime: 2023/4/14 18:12
*
* @param Request $request
*
* @return array
*/
public function getCountByStartToEnd(Request $request): array
{
$key = $request->input('redis_key');
if (!$key) {
$key = $this->tmp_key;
}
$start_bytes = $request->input('start_bytes', 0);
$end_bytes = $request->input('end_bytes', -1);
// 这里的开始和结束指 byte,从 0 开始算,0到2 查找的是 0到(2+1)*8=24位
// 0-0 就是 1byte 也指 0-7位
$bitcount = Redis::bitcount($key, $start_bytes, $end_bytes);
$data = [
'redis_key' => $key,
'start_bytes' => $start_bytes,
'end_bytes' => $end_bytes,
'bitcount' => $bitcount,
];
return Tools::outSuccessInfo($data);
}
/**
* Description: 获取指定范围内出现的第一个 0 或 1
* Author: Shuxiaoyuan
* Email: sxy@shuxiaoyuan.com
* Website: https://www.shuxiaoyuan.com
* DateTime: 2023/4/17 14:10
*
* @param Request $request
* @param ErrorCode $errorCode
*
* @return array|void
*/
public function getBitPosByStartToEnd(Request $request, ErrorCode $errorCode)
{
$validator = Validator::make($request->all(), [
'start_bytes' => 'required|integer|min:0',
'end_bytes' => 'required|integer',
'status' => 'required|in:0,1',
], $message = [
'start_bytes.required' => '开始位未设置',
'start_bytes.integer' => '开始位必须为整数',
'start_bytes.min' => '开始位最小为1',
'end_bytes.required' => '结束位未设置',
'end_bytes.integer' => '结束位必须为整数',
'status.required' => '状态未设置',
'status.in' => '状态只能为0或1',
]);
if ($validator->fails()) {
return $errorCode->getValidatorErrorMessage($validator);
}
$key = $request->input('redis_key');
if (!$key) {
$key = $this->tmp_key;
}
$start_bytes = $request->input('start_bytes', 0);
$end_bytes = $request->input('end_bytes', -1);
$status = $request->input('status', 1);
// 这里的开始和结束指 byte,从 0 开始算
$bitpos = Redis::bitpos($key, $status, $start_bytes, $end_bytes);
$data = [
'redis_key' => $key,
'start_bytes' => $start_bytes,
'end_bytes' => $end_bytes,
'bitcount' => Redis::bitcount($key),
'bitpos' => $bitpos,
];
return Tools::outSuccessInfo($data);
}
/**
* Description: 设置位,简单字符串演示
* Author: Shuxiaoyuan
* Email: sxy@shuxiaoyuan.com
* DateTime: 2022/1/7 16:07
*
* @param Request $request
*
* @return array
*/
public function setBitExample(Request $request): array
{
$key = $this->key;
$string = $request->input('string', 'shuxiao');
$ascii = [];
$bin = [];
$offset = [];
for ($i = 0; $i < strlen($string); $i++) {
// $o = $string[$i] . $o; // 反转字符串
// 将字符串转 ASCII 码: "s":115,"h":104,"u":117,"x":120,"i":105,"a":97,"o":111
$ascii[$string[$i]] = ord($string[$i]);
// 转成二进制值: "s":"1110011","h":"1101000","u":"1110101","x":"1111000","i":"1101001","a":"1100001","o":"1101111"
$bin[] = decbin(ord($string[$i]));
}
/**
* 字符 s=>1110011 和字符 h=>1101000 表示如下
* 高位 字符s 低位 高位 字符h 低位
* |0|1|1|1|0|0|1|1| |0|1|1 |0 |1 |0 |0 |0 | 用8位字节表示
* |7|6|5|4|3|2|1|0| |7|6|5 |4 |3 |2 |1 |0 | 8个字节中,左边为高位,右边为低位
* |0|1|2|3|4|5|6|7|……|8|9|10|11|12|13|14|15| 这个是位顺序
*
* 注意: 位数组的顺序和字符的位顺序是相反的
* 所以 s 需要设置的位是: 1、2、3、6、7
* Redis::setbit($key, 1, 1);
* Redis::setbit($key, 2, 1);
* Redis::setbit($key, 3, 1);
* Redis::setbit($key, 6, 1);
* Redis::setbit($key, 7, 1);
* // 应该输出一个 s
* dd(Redis::get($key));
*
* h 需要设置的位是: 9、10、12
* Redis::setbit($key, 9, 1);
* Redis::setbit($key, 10, 1);
* Redis::setbit($key, 12, 1);
* // 应该输出一个 sh
* dd(Redis::get($key));
*
*/
// 设置偏移量:主要是计算偏移量在多少位
foreach ($bin as $k => $v) {
for ($i = 0; $i < strlen($v); $i++) {
if ($v[$i]) {
// 计算偏移量
$bit = (int)bcmul($k, 8) + ($i + 1);
$offset[] = $bit;
Redis::setbit($key, $bit, 1);
}
}
}
$data = [
'输入的字符串' => $string,
'取出键值' => Redis::get($key),
'ascii码' => $ascii,
'二进制值' => $bin,
'需要设置的偏移量' => $offset,
];
Redis::del($key);
return Tools::outSuccessInfo($data);
}
/**
* Description: 用户签到
* Author: Shuxiaoyuan
* Email: sxy@shuxiaoyuan.com
* DateTime: 2022/1/10 13:21
*
* @param Request $request
* @param ErrorCode $errorCode
*
* @return array
*/
public function setUserTask(Request $request, ErrorCode $errorCode): array
{
$validator = Validator::make($request->all(), [
'member_id' => 'required|integer|min:1',
'date' => 'required|date_format:Y-m-d',
], $message = [
'member_id.required' => '用户ID未设置',
'member_id.integer' => '用户ID必须为整数',
'member_id.min' => '用户ID最小为1',
'date.required' => '签到日期未设置',
'date.date_format' => '签到日期格式不正确',
]);
if ($validator->fails()) {
return $errorCode->getValidatorErrorMessage($validator);
}
$member_id = $request->input('member_id');
$date = $request->input('date');
$year = date('Y', strtotime($date));
// 将每一个用户的签到按照年份进行划分,Redis键按照会员和年来划分
$key = $this->key . ':' . $year . ':' . $member_id;
// 测试一年的每一天都签到
// $days = ToolsFacade::getEverydayDate($year . '0101', $year . '1231', 'nd');
/**
* 将该年中的月日作为位来设置,这样每一个键最大的存储空间为 1231(bit) / 8 = 154(Bytes)
* 因为月日不连续,并且第一天是101,所以这样会浪费一部分空间,可以进一步缩减到 365/366 天
*/
// // 缩短到 366 天方案
// // 今年的开始天到现在的天数
// $formatted_dt1 = Carbon::parse(Carbon::make($date)->firstOfYear()->toDateString());
// $formatted_dt2 = Carbon::parse($date);
// $date_diff_days = $formatted_dt1->diffInDays($formatted_dt2); // 这个就当做相应位
// dd($date_diff_days);
$day = date('md', strtotime($date));
// 返回值:0或1,存储在偏移量的原始位值
$status = Redis::setbit($key, (int)$day, 1);
$bitcount = Redis::bitcount($key);
// 入库持久化过程略
$data = [
'redis_key' => $key,
'status' => $status,
'count_number' => $bitcount,
];
Redis::expire($key, 8 * 3600);
return Tools::outSuccessInfo($data);
}
/**
* Description: 统计用户某段时间内的签到次数
* Author: Shuxiaoyuan
* Email: sxy@shuxiaoyuan.com
* Website: https://www.shuxiaoyuan.com
* DateTime: 2023/4/14 14:51
*
* @param Request $request
*
* @return array
*/
public function getUserTaskByStartToEnd(Request $request): array
{
$member_id = $request->input('member_id');
// 开始时间和结束时间-这里假设不考虑跨年的情况
$start_date = $request->input('start_date', '2023-01-01');
$end_date = $request->input('end_date', '2023-01-31');
$year = date('Y', strtotime($start_date));
$key = $this->key . ':' . $year . ':' . $member_id;
/**
* 方案一:根据开始时间和结束时间循环去取相应位,然后统计
*/
$method1 = Redis::pipeline(function ($pipeline) use ($key, $start_date, $end_date) {
$start_bytes = (int)date('md', strtotime($start_date));
$end_bytes = (int)date('md', strtotime($end_date));
for ($i = $start_bytes; $i < $end_bytes; $i++) {
$pipeline->getbit($key, $i);
}
});
// 后续研究下效率问题
$count1 = array_sum($method1);
// 如果是采用的缩短到 366 天方案
// $year_first_day = Carbon::parse(Carbon::make($start_date)->firstOfYear()->toDateString()); // 今年的第一天
// $start_bytes = $year_first_day->diffInDays($start_date);
// $end_bytes = $year_first_day->diffInDays($end_date);
// dd($start_bytes, $end_bytes);
/**
* 方案二:使用 bitcount 命令,将包含开始时间和结束时间内的统计出来,然后过滤掉不合法数据
* 采用默认即: 12B - 17B
*/
$start_bytes = floor(bcdiv((int)date('md', strtotime($start_date)), 8, 2));
$end_bytes = ceil(bcdiv((int)date('md', strtotime($end_date)), 8, 2));
// 这个范围内不仅包含了指定的日期,还包含了未指定的日期,所以要处理
$method2 = Redis::bitcount($key, $start_bytes, $end_bytes);
$tmp_start_bytes = bcmul($start_bytes, 8); // 96b
$tmp_end_bytes = bcmul($end_bytes, 8); // 136b
// 需要剔除的位
$exclude = Redis::pipeline(function ($pipeline) use ($key, $start_date, $end_date, $tmp_start_bytes, $tmp_end_bytes) {
$start_bytes = (int)date('md', strtotime($start_date)); // 101b
$end_bytes = (int)date('md', strtotime($end_date)); // 131b
// 96b - 101b
for ($i = $tmp_start_bytes; $i < $start_bytes; $i++) {
$pipeline->getbit($key, $i);
}
// 131b - 136b
for ($i = $end_bytes; $i < $tmp_end_bytes; $i++) {
$pipeline->getbit($key, $i);
}
});
// 因为只有0和1两个值,所以可以直接求和
$exclude_count = array_sum($exclude);
$count2 = $method2 - $exclude_count;
$data = [
'redis_key' => $key,
'start_date' => $start_date,
'end_date' => $end_date,
'count1' => $count1,
'method2' => $method2,
'count2' => $count2,
];
return Tools::outSuccessInfo($data);
}
/**
* Description: 查看用户某一天是否签到
* Author: Shuxiaoyuan
* Email: sxy@shuxiaoyuan.com
* DateTime: 2022/1/7 14:52
*
* @param Request $request
*
* @return array
*/
public function getUserIsTaskByDay(Request $request): array
{
$member_id = $request->input('member_id');
$date = $request->input('date');
$year = date('Y', strtotime($date));
// 将每一个用户的签到按照年份进行划分,Redis键按照会员和年来划分
$key = $this->key . ':' . $year . ':' . $member_id;
$day = (int)date('md', strtotime($date));
// 查找某一位上面的值
$status = Redis::getbit($key, $day);
$data = [
'redis_key' => $key,
'day' => $day,
'status' => $status,
];
return Tools::outSuccessInfo($data);
}
}
本文档使用 SmartWiki 发布