作者: ethan

  • redis计算集合的交/并/差集

    require_once '../../CacheRedis.php';
    
    $server = '127.0.0.1:6379';
    $redis = CacheRedis::getInstance($server);
    
    $key1 = 'set:1';
    $key2 = 'set:2';
    $key3 = 'set:3';
    
    $res = $redis->sAdd($key1, 1, 2, 3, 4);
    $res = $redis->sAdd($key2, 2, 3, 4);
    $res = $redis->sAdd($key3, 3, 4, 5, 6);
    $redis->expire($key1, 5);
    $redis->expire($key2, 5);
    $redis->expire($key3, 5);
    
    // 集合间操作
    
    // sinter: 求多个集合的交集
    $res = $redis->sInter($key1, $key2, $key3);
    var_dump($res);
    
    // sunion: 求多个集合的并集
    $res = $redis->sUnion($key1, $key2, $key3);
    var_dump($res);
    
    // sdiff: 求多个集合的差集(第一个集合 - 其他集合, 也就说,结果是返回第一个集合中特有的元素)
    $res = $redis->sDiff($key1, $key2, $key3);
    var_dump($res);
    
    $key1_store = 'set:s:1';
    // sinterstore: 计算交集并将结果保存 ps: sUnionStore sDiffStore
    $res = $redis->sInterStore($key1_store, $key1, $key2, $key3);
    var_dump($res);
    $res = $redis->sMembers($key1_store);
    var_dump($res);
    
    // ps: 集合间的运算都比较耗时,所以,在数据量非常大的情况下,一般都是异步使用(脚本预热数据等方式), 使用store命令将结果缓存下来,使用的时候不需要实时计算,直接从缓存中取

    推荐: redis基础&实战教程

  • redis列表常用方法示例

    require_once '../../CacheRedis.php';
    
    $server = '127.0.0.1:6379';
    $redis = CacheRedis::getInstance($server);
    
    // rpush: 在列表右侧插入数据 lpush在列表左侧插入数据
    $key = 'list:test';
    $res = $redis->rPush($key, 1);
    $res = $redis->rPush($key, 2);
    $res = $redis->rPush($key, 3, 4, 5);
    /*$res = $redis->lPush($key, 6);
    $res = $redis->rPush($key, 7);*/
    $res = $redis->expire($key, 5);
    
    // lrange: 从左到右获取列表元素 key start end end=-1取全部元素
    $res = $redis->lRange($key, 0, -1);
    $res = $redis->lRange($key, 0, 3);
    var_dump($res);
    
    // lpop: 从列表左侧删除数据, rpop: 从列表右侧删除元素
    $res = $redis->lPop($key);
    $res = $redis->rPop($key);
    $res = $redis->lRange($key, 0, -1);
    var_dump($res);
    
    // lindex: 根据下标获取列表元素
    $res = $redis->lIndex($key, 1);
    var_dump($res);
    
    // llen: 获取列表长度
    $res = $redis->lLen($key);
    var_dump($res);

    推荐相关精品课程: redis基础&实战教程

  • sql任务重试方案

    定时执行相应的sql任务,是工作中常遇到的场景。通常用在统计相关的任务,这些sql语句中,通常都含有相对的时间函数,如now()等, 如:

    select count(*) from tb_cinfo where create_time>=UNIX_TIMESTAMP(DATE_FORMAT(date_sub(now(), interval 1 HOUR), '%Y-%m-%d %H')) and create_time<UNIX_TIMESTAMP(DATE_FORMAT(now(), '%Y-%m-%d %H')) 

    上面这个sql语句的的结果为 上一个小时内新增的数据量,但如该sql语句18点执行失败,19点进行手工重试时,就会有问题了,因为18点执行时统计的是17点的数据,而若19点进行重试,重新执行该语句,则统计的是18点的数据。

    方案

    自定义业务场景常用的时间函数,然后对伪sql解析为具体的可执行sql, 并将该可执行sql记录下来,之后重试则执行该记录下来的sql。

    举例说明:

    {@date}: 表示当前日期,e.g. 20200831, ,支持加减,如前一天:{@date-1} => 20200830,后一天:{@date+1} => 20200901

    {@date_timestamp}: 表示当前时间戳, e.g. 1598864224, 支持加减,如前10分钟:{@date_timestamp-600},后1个小时:{@date_timestamp+3600}

    {@date_minute_timestamp}: 表示当前分钟时间戳, 如2020-08-31 16:50:55按照2020-08-31 16:50:00,转换为1598863800, 同样支持加减,如前一分钟: {@date_minute_timestamp-1},后一分钟: {@date_minute_timestamp+1}

    {@date_hour_timestamp}: 表示当前小时时间戳,同样支持加减,如前一小时: {@date_hour_timestamp-1},后一小时: {@date_hour_timestamp+1}

    ps: 此处仅部分示例,可根据实际业务场景,自定义需要的时间函数

    则之前的sql, 就可以改写为:

    select count(*) from tb_cinfo where create_time>={@date_hour_timestamp-1} and create_time<{@date_hour_timestamp}

    对该sql进行解析, 得到解析后的sql:

    select count(*) from audit_cinfo where create_time>=1598860800 and create_time<1598864400

    此时,若该sql执行失败,无论什么时间,都可以直接重试即可,因为该sql语句并不包含时间函数,而是具体的时间数值。

    附sql解析语句示例(php版本):

    /**
         * sql解析
         *
         * @param $sql
         * @return mixed
         */
        public function parseQuery($sql) {
            $sql = str_replace("\n", ' ', trim($sql, ';'));
            $sql =  preg_replace("/\s(?=\s)/", "\\1", trim($sql));
            preg_match_all('/\{.*?\}/', $sql, $matches);
            $matches = $matches[0];
            $replace = [];
            foreach ($matches as $val) {
                $val = str_replace(' ', '', strtolower($val));
                preg_match('/@\w+[+|\-|\}]/', $val, $match1);
                preg_match('/[+|\-]\d+\}/', $val, $match2);
                $match1 = empty($match1[0]) ? '' : substr($match1[0], 1, -1);
                $func = empty($match2[0]) ? '' : substr($match2[0], 0, 1);
                $match2 = empty($match2[0]) ? '' : substr($match2[0], 1, -1);
    
                $currTime = time();
                $res = $match1;
                switch ($match1) {
                    case 'date':
                        $res = $this->calculation(strtotime(date("Y-m-d 00:00:00", $currTime)), $func, $match2, 86400);
                        $res = date("Ymd", $res);
                        break;
                    case 'date_timestamp':
                        $res = $this->calculation($currTime, $func, $match2, 1);
                        break;
                    case 'date_minute_timestamp':
                        $res = $this->calculation(strtotime(date("Y-m-d H:i:00", $currTime)), $func, $match2, 60);
                        $res = strtotime(date("Y-m-d H:i:00", $res));
                        break;
                    case 'date_hour_timestamp':
                        $res = $this->calculation(strtotime(date("Y-m-d H:00:00", $currTime)), $func, $match2, 3600);
                        $res = strtotime(date("Y-m-d H:00:00", $res));
                        break;
                    default:
                        break;
                }
                $replace[] = $res;
            }
    
            return str_replace($matches, $replace, $sql);
        }
    
        private function calculation($base, $func, $param, $step = 1) {
            $res = $base;
            if (empty($func) || empty($param) || empty($step)) {
                return $res;
            }
            switch ($func) {
                case '+':
                    $res = $base + $param * $step;
                    break;
                case '-':
                    $res = $base - $param * $step;
                    break;
                default:
                    break;
            }
            return $res;
        }
  • Eelasticsearch sort 注意事项

    Eelasticsearch中文档的默认_id是字符串类型的,即便你往里面写入的是整数值。

    这也就导致,如果要是根据_id进行排序的话,实际是依据的字符串的大小比较,而非整数的大小比较。

    举例说明:

    es创建索引, mapping如下:

    es中写入了部分测试数据:

    通过_id降序取出数据:

    可以看到,取到的第一条数据,并非预料中的100000001,而是99999999,这就是因为es对_id排序比较时,是将其当做字符串进行的比较,从而得到’99999999′ > ‘100000001’的结论。

    此时,我们更换排序字段,改为整数类型的 mysql_id2, 则可以得到符合预期的结果:

    综上,进行排序查询时,需要确保待排序的字段类型是否符合你的排序需求,从而避免以上误排序的情况出现。

  • golang 多维map赋值

    多维map赋值时,要注意,需要先声明,再赋值,如下:

    func TestMap(t *testing.T)  {
    	groupDict := make(map[int32]map[string]int32)
    
    	groupType := int32(1)
    
            // 声明
    	if _, ok := groupDict[groupType]; !ok {
    		groupDict[groupType] = make(map[string]int32)
    	}
    
    	groupDict[groupType]["test"] = 1
    	t.Log(groupDict)
    }

    如果没有中间的声明部分,则会就会执行报错:

    --- FAIL: TestMap (0.00s)
    panic: assignment to entry in nil map [recovered]
            panic: assignment to entry in nil map
    

  • ip地址校验-php

    校验ip地址的合法性,无需自己运用正则进行判断,php有专门的函数进行校验。

    判断是否是合法IP

    if( filter_var($ip, FILTER_VALIDATE_IP) ) {
        var_dump('ok');
    } else {
        var_dump('illegal');
    }

    判断是否是合法的IPv4 IP地址

    if( filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) ) {
        var_dump('ok');
    } else {
        var_dump('illegal');
    }

    断是否是合法的公共IPv4地址,192.168.1.1这类的私有IP地址将会排除在外

    if(filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE)) {
        var_dump('ok');
    } else {
        var_dump('illegal');
    }

    判断是否是合法的IPv6地址

    if( filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE) ) {
        var_dump('ok');
    } else {
        var_dump('illegal');
    }

    判断是否是public IPv4 IP或者是合法的Public IPv6 IP地址

    if( filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) ) {
        var_dump('ok');
    } else {
        var_dump('illegal');
    }

  • linux下载部分文件实现预览

    经常会遇到需要通过linux下载大文件,但下载前,需要先预览下文件内容的需求场景,这个时候,就可以通过curl的range参数来实现。

    -r, --range RANGE   Retrieve only the bytes within a range

    –range: 设置需要下载的字节范围

    举例:

    curl --range 0-1024 -o test.part1 http://xxxx/test.txt

    上述命令会下载test.txt文件前1024字节的数据到本地的test.part1文件, 读取test.part1内容即可实现预览目的。

    同时,–range命令也常用于大文件的分片下载,如:

    curl --range 0-1024 -o test.part1 http://xxxx/test.txt
    curl --range 1025-2048 -o test.part2 http://xxxx/test.txt
    curl --range 2049--o test.part3 http://xxxx/test.txt
    cat test.part1 test.part2 test.part3 > test.txt

    ps: 实现大文件分片下载,最好加一下MD5文件校验的逻辑 md5sum file

  • WordPress分类/标签目录404

    WordPress分类/标签目录访问404的问题,往往是因为固定链接设置的问题导致。

    如下:

    如当前页面的网址是 https://www.fushengwushi.com/archives/1131, 而如果当前目录“技术”的地址是 https://www.fushengwushi.com/archives/technology, 访问自然是404,因为Wordpress将其当成了一片文章去访问。

    解决办法:

    为分类和标签,单独设置前缀,不要与文章的前缀“archives”一样即可。

  • Emoji表情

    Emoji表情 – 是开发中常遇到的一个难点,无论是存储还是识别,都有一定的难度,下面来具体介绍一下:

    一、含义

    Emoji表情

    emoji 是可以插入文字的图形符号,是日语词“絵文字”(“えもじ”)的音译。

    Emoji 在上个世纪90年代,由日本电信商引入服务,最早用于在短消息之中插入表情。2007年,苹果公司的 iPhone 支持了 Emoji,导致它在全世界范围的流行。目前全球约有90%的在线用户频繁使用emoji,每天有60亿个emoji表情符号被传送。

    二、标准化

    2010年10月发布的Unicode 6.0版首次收录emoji编码,从此,Emoji表情就是一个文字,不过会被渲染成图形而已。

    Emoji 的国际标准在 2015 年出台,截止目前(2020-07),已经是v13.0版本了。

    三、渲染实现

    Unicode 只是规定了 Emoji 的码点和含义,但具体的实现与展示则依赖于其展示的系统,如果系统无法识别该Emoji表情,则无法正常显示。

    同时,不同的系统,展示的方式同样有区别,同一个Emoji表情,在不同的系统上的展示可能不同。

    四、表情存储

    Emoji表情的mysql存储一直是开发中常遇到的情景,一般而言,常规的方案是采用utf8mb4编码方式进行存储。

    如果业务上更改utf8mb4存储成本过大的话,也可以通过json_encode转为unicode编码,或者用varbinary类型进行储存。

    五、表情的识别

    emoji表情与其他字符的区分一直是个难点,如果想要知道一个字符串中有多少个Emoji表情,或者去除字符串中的Emoji表情等,都需要对Emoji表情进行识别。

    一般而言,Emoji表情的识别有两种方案。

    方案一:字节数

    可以通过字节数来进行,毕竟大部分Emoji表情的字节数为4,可以跟其他字符区分开,如下

    // 提取所含emoji表情字符数
    func GetEmojiNum(text string) int {
    	num := 0
    	for _, value := range text {
    		_, size := utf8.DecodeRuneInString(string(value))
    		if size > 3 {
    			num++
    		}
    	}
    	return num
    }

    如果要求不是非常精确的话,这种方法完全可以满足我们的需求,但肯定会有一部分Emoji表情会被遗漏,因为很多Emoji表情的字节数并不是4。

    方案二:编码范围匹配

    如果想要精确的匹配出Emoji表情,就需要拿到所有Emoji表情的编码范围,然后通过正则进行匹配。

    regex := "[\U0001f004]|[\U0001f0cf]|[\U0001f170-\U0001f171]|[\U0001f17e-\U0001f17f]|[\U0001f17f]|[\U0001f18e]|[\U0001f191-\U0001f19a]|[\U0001f201-\U0001f202]|[\U0001f21a]|[\U0001f22f]|[\U0001f232-\U0001f23a]|[\U0001f250-\U0001f251]|[\U0001f300-\U0001f321]|[\U0001f324-\U0001f393]|[\U0001f396-\U0001f397]|[\U0001f399]|[\U0001f39a-\U0001f39b]|[\U0001f39e-\U0001f3f0]|[\U0001f3f3-\U0001f3f5]|[\U0001f3f7-\U0001f4fd]|[\U0001f4ff-\U0001f53d]|[\U0001f549-\U0001f54e]|[\U0001f550-\U0001f567]|[\U0001f56f-\U0001f570]|[\U0001f573-\U0001f57a]|[\U0001f587]|[\U0001f58a-\U0001f58d]|[\U0001f590]|[\U0001f595-\U0001f596]|[\U0001f5a4-\U0001f5a5]|[\U0001f5a8]|[\U0001f5b1-\U0001f5b2]|[\U0001f5bc]|[\U0001f5c2-\U0001f5c4]|[\U0001f5d1-\U0001f5d3]|[\U0001f5dc-\U0001f5de]|[\U0001f5e1]|[\U0001f5e3]|[\U0001f5e8]|[\U0001f5ef]|[\U0001f5f3]|[\U0001f5fa-\U0001f64f]|[\U0001f680-\U0001f6c5]|[\U0001f6cb-\U0001f6d2]|[\U0001f6d5-\U0001f6d7]|[\U0001f6e0-\U0001f6e5]|[\U0001f6e9]|[\U0001f6eb-\U0001f6ec]|[\U0001f6f0]|[\U0001f6f3-\U0001f6fc]|[\U0001f7e0-\U0001f93a]|[\U0001f93c-\U0001f945]|[\U0001f947-\U0001f978]|[\U0001f97a-\U0001f9cb]|[\U0001f9cd-\U0001fa74]|[\U0001fa78-\U0001fa86]|[\U0001fa90-\U0001faa8]|[\U0001fab0-\U0001fab6]|[\U0001fac0-\U0001fac2]|[\U0001fad0-\U0001fad6]|[\u00a9]|[\u00ae]||[\u203c]|[\u2049]|[\u2122]|[\u2139]|[\u2194-\u2199]|[\u21a9-\u21aa]|[\u231a-\u231b]|[\u2328]|[\u23cf]|[\u23e9]|[\u23ea-\u23f3]|[\u23f8-\u23fa]|[\u24c2]|[\u25aa-\u25ab]|[\u25b6]|[\u25c0]|[\u25fb-\u25fe]|[\u2600-\u2604]|[\u260e]|[\u2611]|[\u2614-\u2615]|[\u2618]|[\u261d]|[\u2620]|[\u2622-\u2623]|[\u2626]|[\u262a]|[\u262e-\u262f]|[\u2638-\u263a]|[\u2640]|[\u2642]|[\u2648-\u2653]|[\u265f-\u2660]|[\u2663]|[\u2665-\u2666]|[\u2668]|[\u267b]|[\u267e-\u267f]|[\u2692-\u2697]|[\u2699]|[\u269b-\u269c]|[\u26a0-\u26a1]|[\u26a7]|[\u26aa-\u26ab]|[\u26b0-\u26b1]|[\u26bd-\u26be]|[\u26c4-\u26c5]|[\u26c8]|[\u26ce-\u26cf]|[\u26d1]|[\u26d3-\u26d4]|[\u26e9-\u26ea]|[\u26f0-\u26f5]|[\u26f7-\u26fa]|[\u26fd]|[\u2702]|[\u2705]|[\u2708-\u270d]|[\u270f]|[\u2712]|[\u2714]|[\u2716]|[\u271d]|[\u2721]|[\u2728]|[\u2733-\u2734]|[\u2744]|[\u2747]|[\u274c]|[\u274e]|[\u2753-\u2755]|[\u2757]|[\u2763-\u2764]|[\u2795-\u2797]|[\u27a1]|[\u27b0]|[\u27bf]|[\u2934-\u2935]|[\u2b05-\u2b07]|[\u2b1b-\u2b1c]|[\u2b50]|[\u2b55]|[\u3030]|[\u303d]|[\u3297]|[\u3299]"
    
    rCharacter := regexp.MustCompile(regex)
    num := len(rCharacter.FindAllStringSubmatch(text,-1))
    

    上面我们拿到了所有的Emoji表情,但实际的业务场景中,我们所认知的Emoji表情应该是所有的图形符号,而有些图形符号并不属于Emoji表情的范围,所以我们需要再加上这些图形符号的编码范围。

    杂项符号及图形768个字符中有637是emoji;增补符号及图形82个字符中有80个是emoji;所有80个表情符号都是emoji;交通及地图符号103个字符中有92个是emoji;杂项符号256个字符中有80个是emoji;装饰符号192个字符中有33个是emoji。

    下面是结合后的正则匹配:

    // 提取所含emoji表情字符数
    func GetEmojiNum(text string) int {
    	regex := "[\U0001f004]|[\U0001f0cf]|[\U0001f170-\U0001f171]|[\U0001f17e-\U0001f17f]|[\U0001f17f]|[\U0001f18e]|[\U0001f191-\U0001f19a]|[\U0001f1e6-\U0001f1ff]{1,2}|[\U0001f201-\U0001f202]|[\U0001f21a]|[\U0001f22f]|[\U0001f232-\U0001f23a]|[\U0001f250-\U0001f251]|[\U0001f300-\U0001f64f]|[\U0001f680-\U0001f6ff]|[\U0001f7e0-\U0001fa74]|[\U0001fa78-\U0001fa86]|[\U0001fa90-\U0001faa8]|[\U0001fab0-\U0001fab6]|[\U0001fac0-\U0001fac2]|[\U0001fad0-\U0001fad6]|[\u00a9]|[\u00ae]|[\u203c]|[\u2049]|[\u2122]|[\u2139]|[\u2194-\u2199]|[\u21a9-\u21aa]|[\u231a-\u231b]|[\u2328]|[\u23cf]|[\u23e9]|[\u23ea-\u23f3]|[\u23f8-\u23fa]|[\u24c2]|[\u25aa-\u25ab]|[\u25b6]|[\u25c0]|[\u25fb-\u25fe]|[\u2600-\u26ff]|[\u2700-\u27bf]|[\u2934-\u2935]|[\u2b05-\u2b07]|[\u2b1b-\u2b1c]|[\u2b50]|[\u2b55]|[\u3030]|[\u303d]|[\u3297]|[\u3299]"
    	rCharacter := regexp.MustCompile(regex)
    	return len(rCharacter.FindAllStringSubmatch(text,-1))
    }

    当然,这种做法还是存在一定误差的,因为一些Emoji表情实际是由多个编码组合而成的,所以会出现,一个Emoji表情,但是别出来的表情数>1,但相对而言,精确度比之字节比较不在一个等级。

    而且这种方法仅是在这种获取Emoji表情数,或者替换/提取Emoji表情等需要明确每一个Emoji表情的情景会出现上述不准确情况,如果你的业务需求仅仅是判断字符串中是否包含Emoji表情,上述方案则是完全没问题的。

    六、参考链接

    Full Emoji List

    WikiEmoji

  • emoji-无法json存储

    emoji – go语言的json,并不会如php一样,将中文,表情等,转换为unicode编码,故无法直接通过以往json的方式直接实现emoji表情的存储。

    下面看下具体的例子:

    php的json_encode

    <?php
    $access = [
        'uname' => "开心U0001f973"
    ];
    echo json_encode($access) . PHP_EOL;
    
    // 输出: {"uname":"\u5f00\u5fc3\ud83e\udd73"}

    go的jsoniter.Marshal

    Accuse := map[string]string{"uname":"开心U0001f973"}
    if acExtJson, err := jsoniter.Marshal(Accuse); err != nil {
        t.Log("failed")
    } else {
        t.Log(string(acExtJson))
    }
    
    // 输出: {"uname":"开心U0001f973"}

    故此,无法在go中通过json的格式,实现emoji表情的存储(当然也可以想办法手动将json后的字符串转为unicode,但这样就相对繁琐了,且容易出错)

    此时可以考虑其他的可以让mysql存储emoji表情的方法

    1. 字段类型设置为varbinary
    2. 将Mysql的编码从utf8转换成utf8mb4