Workerman ThinkPHP5 宝塔 安装Event拓展_九城科技-CSDN博客

mikel阅读(810)

来源: Workerman ThinkPHP5 宝塔 安装Event拓展_九城科技-CSDN博客

Workerman 结合 TP5在宝塔环境下安装Event拓展

操作系统是CentOS7

先用workerman官方给的检查环境的脚本进行检查

curl -Ss http://www.workerman.net/check.php | php

全部显示OK就代表满足workman的运行环境

宝塔默认是关掉了几个PHP的函数的

需要在PHP的配置文件里面去把workman需要用到的函数给注释掉

 

然后后续操作基本一把梭

代码如下

wget https://pecl.php.net/get/event-3.0.2.tgz

tar -zxvf event-3.0.2.tgz

cd event-3.0.2

phpize

./configure –with-php-config=/www/server/php/73/bin/php-config

make && make install

echo “extension=event.so” >> /www/server/php/73/etc/php.ini

service php-fpm-73 reload
需要注意的是/www/server/php/73 为实际PHP所存在的目录

最后启动workerman

php start.php start

这样就启动起来了
————————————————
版权声明:本文为CSDN博主「九城科技」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_35422558/article/details/114822862

宝塔利用workerman实现websocket协议 用于微信小程序等应用的WSS通信 – 星空站长网

mikel阅读(1010)

来源: 宝塔利用workerman实现websocket协议 用于微信小程序等应用的WSS通信 – 星空站长网

网上下载的啦啦外卖小程序提示 WSS错误,所以研究起了这个WSS通信。

什么是Workerman

Workerman是一款开源高性能异步PHP socket框架。支持高并发,超高稳定性,被广泛的用于手机app、移动通讯,微信小程序,手游服务端、网络游戏、PHP聊天室、硬件通讯、智能家居、车联网、物联网等领域的开发。 支持TCP长连接,支持Websocket、HTTP等协议,支持自定义协议。拥有异步MySQL、异步Redis、异步Http、MQTT物联网客户端、异步消息队列等众多高性能组件。

相关阅读:Workerman简单开发实例WebSocket教程 、WebSocket在线测试

先根据相关阅读中的开发实例建立好PHP文件。Workerman的主程序和建立好的PHP文件都传到网站根目录。(注意,解压后的主程序删除掉目录后面的-master,不想删就修改建立的PHP文件指向)

运行PHP命令

curl -Ss http://www.workerman.net/check.php | php 检查是否适应 workerman 的环境。

这个时候,提示禁用了某个函数。

利用workerman实现websocket协议 用于微信小程序等应用的WSS通信

但是我在php5.6中已经删除了函数,这个时候唯一一个可能,就是你有多个PHP。

使用 php –ini 查看当前PHP调用的是哪个版本PHP的配置文件。

这里可以看到是调用的7.4的。

利用workerman实现websocket协议 用于微信小程序等应用的WSS通信

那我们进入7.4的配置文件(php.ini)文件将

搜索一下disable_functions

在这行前面加分号(;),或者把提示没有的函数拿掉即可

然后再次运行 curl -Ss http://www.workerman.net/check.php | php 检查是否合适 workerman 的环境。

全显示OK,并且没有报错。

进入PHP执行目录

cd /www/server/php/56/bin

php /home/wwwroot/domin.com/ws_test.php start

利用workerman实现websocket协议 用于微信小程序等应用的WSS通信

以上显示是成功开启服务

为了这个链接,研究了整整3天时间。

在网上看到的用代理方式,是没有用的。因为之前的一些程序没有写在教程中。

layer.js源码分析_极客神殿-CSDN博客_layer源码解析

mikel阅读(603)

来源: layer.js源码分析_极客神殿-CSDN博客_layer源码解析

最近在看layer.js源码,从中得到了一些启发,对于一个框架的设计也有了一定的看法,现在对于这个框架的设计以及其他的问题来说明一下。

layer.js是一个专注于弹出层的框架,这个框架本身可以实现5种弹出层类型,其他的就不多说了,可以去看看它的官网,下面说一下它的主要组织形式:

  1. 首先,这个框架本身就是一个IIFE(立即执行函数表达式),保证了局部环境,避免了全局变量污染的问题
  2. 框架内部主要是三个对象构成,分别是Class构造函数、layer对象、ready对象
  3. 通过window来暴露对外api

以前看过一点JQuery的源码,layer.js的框架结构和JQuery是相同形式,框架内部主要是这三个对象来组成,对于这三个对象上具体的方法以及属性我列举了下,如下图所示:

这里写图片描述

整个框架的结构组织以及脉络还是很清晰的,框架整体的代码量大概1300多行左右,我对这个框架运行的具体流程做了个较为详细的流程,具体流程如下:
这里写图片描述


/*!

 @Title: Layui
 @Description:经典模块化前端框架
 @Site: www.layui.com
 @Author: 贤心
 @License:MIT

 */

;!function(win) {

    "use strict";

    var Lay = function() {
        this.v = '1.0.9_rls'; //版本号
    };

    Lay.fn = Lay.prototype;

    var doc = document,
        config = Lay.fn.cache = {},

        // 获取本js所在目录
        getPath = function() {
            var js = doc.scripts,
                jsPath = js[js.length - 1].src;
            return jsPath.substring(0, jsPath.lastIndexOf('/') + 1);
        }(),

        // 异常提示
        error = function(msg) {
            win.console && console.error && console.error('Layui hint: ' + msg);
        },

        // 检测opera环境
        isOpera = typeof opera !== 'undefined' && opera.toString() === '[object Opera]',

        // 内置模块
        modules = {
            layer : 'modules/layer', //弹层
            laydate : 'modules/laydate', //日期
            laypage : 'modules/laypage', //分页
            laytpl : 'modules/laytpl', //模板引擎
            layim : 'modules/layim', //web通讯
            layedit : 'modules/layedit', //富文本编辑器
            form : 'modules/form', //表单集
            upload : 'modules/upload', //上传
            tree : 'modules/tree', //树结构
            table : 'modules/table', //富表格
            element : 'modules/element', //常用元素操作
            util : 'modules/util', //工具块
            flow : 'modules/flow', //流加载
            carousel : 'modules/carousel', //轮播
            code : 'modules/code', //代码修饰器
            jquery : 'modules/jquery', //DOM库(第三方)  
            mobile : 'modules/mobile', //移动大模块 | 若当前为开发目录,则为移动模块入口,否则为移动模块集合
            'layui.all' : 'dest/layui.all' //PC模块合并版
        };

    config.modules = {}; //记录模块物理路径
    config.status = {}; // 记录已注册的模块集。
    config.timeout = 10; //符合规范的模块请求最长等待秒数
    config.event = {}; //记录模块自定义事件

    // 定义模块
    Lay.fn.define = function(deps, callback) {
        var that = this,
            type = typeof deps === 'function',
            mods = function() {
                // 参数callback,可选,用于回调。
                // 回调参数function,用于回调时,注册模块。
                typeof callback === 'function' && callback(function(app, exports) {
                    // 回调参数function的参数app,必要,代表模块名。
                    // 回调参数function的参数exports,必要,代表模块的接口方法。
                    layui[app] = exports;
                    // config.status,记录已注册的模块集。
                    config.status[app] = true;
                });
                return this;
            };

        // 参数deps,代表依赖的模块集,可选。
        type && (
            callback = deps,
            deps = []
        );

        // 相当于layui['layui.all'] || layui['layui.mobile']
        // 模块名layui.all,代表所有模块。
        // 模块名layui.mobile,代表手机版的所有模块。
        // 如果已经加载所有模块,则直接执行回调。
        if (layui['layui.all'] || (!layui['layui.all'] && layui['layui.mobile'])) {
            return mods.call(that);
        }

        // 方法layui.use,动态加载所依赖的模块集deps。
        that.use(deps, mods);
        return that;
    };

    // 动态加载模块集
    Lay.fn.use = function(apps, callback, exports) {
        var that = this,
        // config.dir,内置文件的基目录,默认值为layui.js的所在目录,需以斜杠结束。
            dir = config.dir = config.dir ? config.dir : getPath;
        var head = doc.getElementsByTagName('head')[0];

        // 参数apps,必要,可以是字符串或数组。
        apps = typeof apps === 'string' ? [ apps ] : apps;

        // 参数apps中存在jquery时,如果页面已加载jQuery1.7+库,则直接使用该库。
        if (window.jQuery && jQuery.fn.on) {
            that.each(apps, function(index, item) {
                if (item === 'jquery') {
                    apps.splice(index, 1);
                }
            });
            layui.jquery = jQuery;
        }

        var item = apps[0],
            timeout = 0;
        // 参数exports,可选。
        exports = exports || [];

        // config.host,格式为“//.../”,默认值为config.dir中的主机,或当前页面的主机。
        config.host = config.host || (dir.match(/\/\/([\s\S]+?)\//)/* 匹配“//.../” */ || [ '//' + location.host + '/' ])[0];

        // apps.length === 0 || (layui['layui.all'] || layui['layui.mobile']) && modules[item]
        // 参数apps,允许为空集。
        // 如果需要加载的模块集为空集,则执行回调。
        // 模块名layui.all,代表所有模块。
        // 模块名layui.mobile,代表手机版的所有模块。
        // modules,代表layui的内置模块集。
        // 如果已经加载所有模块,并且当前模块是layui的内置模块,则当前模块不需要加载。
        if (apps.length === 0
            || (layui['layui.all'] && modules[item])
            || (!layui['layui.all'] && layui['layui.mobile'] && modules[item])
        ) {
            return onCallback(), that;
        }

        // 用于监听文件加载完毕
        function onScriptLoad(e, url) {
            var readyRegExp = navigator.platform === 'PLaySTATION 3' ? /^complete$/ : /^(complete|loaded)$/
            if (e.type === 'load' || (readyRegExp.test((e.currentTarget || e.srcElement).readyState))) {
                config.modules[item] = url;
                head.removeChild(node);
                // 轮询查看当前模块是否已注册,每0.025秒轮询一次,共论询config.timeout秒。
                // config.timeout,文件加载超时,默认值为10秒。
                (function poll() {
                    if (++timeout > config.timeout * 1000 / 4) {
                        return error(item + ' is not a valid module');
                    };
                    config.status[item] ? onCallback() : setTimeout(poll, 4);
                }());
            }
        }

        var node = doc.createElement('script'),
        // config.base,代表扩展模块的JS文件目录,默认值为空串,需要以斜杠结束。
        // modules,代表layui的内置模块集。
        // layui.modules[name],代表模块name的相对路径(不包括后缀.js),默认值为name。
        //         如果当前模块是内置模块,则相对路径相对于config.dir + "lay/"。
        //        如果当前模块是扩展模块,则相对路径相对于config.base。
            url = (modules[item] ? (dir + 'lay/') : (config.base || '')) + (that.modules[item] || item) + '.js';
        node.async = true;
        node.charset = 'utf-8';
        node.src = url + function() {
            // config.version=true时,使用config.v作为版本号,否则自己作为版本号,默认值不启用版本号。
            // config.v,代表版本号,默认值为当前时间。
            // config.version=true,config.v不设置时,使流览器不会加载缓存文件,而是重新加载。
            var version = config.version === true ? (config.v || (new Date()).getTime()) : (config.version || '');
            return version ? ('?v=' + version) : '';
        }();

        // config.modules[name],代表已加载,或正在加载中的模块name的相对路径(不包括后缀.js)。
        if (!config.modules[item]) {
            head.appendChild(node);
            if (node.attachEvent && !(node.attachEvent.toString && node.attachEvent.toString().indexOf('[native code') < 0) && !isOpera) {
                node.attachEvent('onreadystatechange', function(e) {
                    onScriptLoad(e, url);
                });
            } else {
                node.addEventListener('load', function(e) {
                    onScriptLoad(e, url);
                }, false);
            }
        } else {
            // 轮询查看是否加载完毕,每0.025秒轮询一次,共论询config.timeout秒。
            // config.timeout,文件加载超时,默认值为10秒。
            (function poll() {
                if (++timeout > config.timeout * 1000 / 4) {
                    return error(item + ' is not a valid module');
                };
                // config.status,记录已注册的模块集。
                (typeof config.modules[item] === 'string' && config.status[item]) ? onCallback() : setTimeout(poll, 4);
            }());
        }
        config.modules[item] = url;

        //回调
        function onCallback() {
            // 参数exports,记录模块的接口。
            exports.push(layui[item]);
            // 加载下一个模块,如果没有下一个,则执行回调。
            apps.length > 1 ? that.use(apps.slice(1), callback, exports) : (typeof callback === 'function' && callback.apply(layui, exports));
        }

        return that;

    };

    // 获取节点的style属性值
    Lay.fn.getStyle = function(node, name) {
        var style = node.currentStyle ? node.currentStyle : win.getComputedStyle(node, null);
        return style[style.getPropertyValue ? 'getPropertyValue' : 'getAttribute'](name);
    };

    // 动态加载CSS
    Lay.fn.link = function(href, fn, cssname) {
        var that = this,
            link = doc.createElement('link');
        var head = doc.getElementsByTagName('head')[0];

        // 参数fn,可选。
        if (typeof fn === 'string')
            cssname = fn;

        // 参数cssname,用于标识CSS文件的ID,默认值为href。
        var app = (cssname || href).replace(/\.|\//g, '');
        var id = link.id = 'layuicss-' + app,
            timeout = 0;

        link.rel = 'stylesheet';
        // config.debug=true时,使流览器不会加载缓存文件。
        link.href = href + (config.debug ? '?v=' + new Date().getTime() : '');
        link.media = 'all';

        // 参数cssname,同一ID的CSS文件的只许加载一次。
        if (!doc.getElementById(id)) {
            head.appendChild(link);
        }

        // 参数fn,用于监听CSS加载完毕。
        if (typeof fn !== 'function') return;
        // 轮询查看是否加载完毕,每0.1秒轮询一次,共论询config.timeout秒。
        (function poll() {
            if (++timeout > config.timeout * 1000 / 100) {
                return error(href + ' timeout');
            };
            parseInt(that.getStyle(doc.getElementById(id), 'width')) === 1989 ? function() {
                fn();
            }() : setTimeout(poll, 100);
        }());
    };

    // css内部加载器
    Lay.fn.addcss = function(firename, fn, cssname) {
        // 全局配置dir,用于内置文件的基目录,默认值为layui.js所在的目录,需要以斜杠结束。
        layui.link(config.dir + 'css/' + firename, fn, cssname);
    };

    // 图片预加载
    Lay.fn.img = function(url, callback, error) {
        var img = new Image();
        img.src = url;
        if (img.complete) {
            return callback(img);
        }
        img.onload = function() {
            img.onload = null;
            callback(img);
        };
        img.onerror = function(e) {
            img.onerror = null;
            error(e);
        };
    };

    // 全局配置
    Lay.fn.config = function(options) {
        options = options || {};
        for (var key in options) {
            config[key] = options[key];
        }
        return this;
    };

    // layui.modules[name],代表模块name的相对路径(不包括后缀.js),默认值为name。
    Lay.fn.modules = function() {
        var clone = {};
        for (var o in modules) {
            clone[o] = modules[o];
        }
        return clone;
    }();

    // 设置模块的相对路径(不含后缀.js)
    Lay.fn.extend = function(options) {
        var that = this;

        options = options || {};
        for (var o in options) {
            // layui[name],如果存在,则表示模块name已注册。
            // layui.modules[name],代表模块name的相对路径(不包括后缀.js),默认值为name。
            // 已注册或已设置相对路径的模块集,不允许再设置相对路径。显然,内置模块的相对路径不允许更改。
            if (that[o] || that.modules[o]) {
                error('\u6A21\u5757\u540D ' + o + ' \u5DF2\u88AB\u5360\u7528');
            } else {
                that.modules[o] = options[o];
            }
        }
        return that;
    };

    // 路由
    Lay.fn.router = function(hash) {
        var hashs = (hash || location.hash).replace(/^#/, '').split('/') || [];
        var item,
            param = {
                dir : []
            };
        for (var i = 0; i < hashs.length; i++) {
            item = hashs[i].split('=');
            /^\w+=/.test(hashs[i]) ? function() {
                if (item[0] !== 'dir') {
                    param[item[0]] = item[1];
                }
            }() : param.dir.push(hashs[i]);
            item = null;
        }
        return param;
    };

    // 本地存储
    Lay.fn.data = function(table, settings) {
        table = table || 'layui';

        if (!win.JSON || !win.JSON.parse) return;

        //如果settings为null,则删除表
        if (settings === null) {
            return delete localStorage[table];
        }

        settings = typeof settings === 'object'
            ? settings
            : {
                key : settings
            };

        try {
            var data = JSON.parse(localStorage[table]);
        } catch (e) {
            var data = {};
        }

        if (settings.value)
            data[settings.key] = settings.value;
        if (settings.remove)
            delete data[settings.key];
        localStorage[table] = JSON.stringify(data);

        return settings.key ? data[settings.key] : data;
    };

    // 设备信息
    Lay.fn.device = function(key) {
        var agent = navigator.userAgent.toLowerCase();

        //获取版本号
        var getVersion = function(label) {
            var exp = new RegExp(label + '/([^\\s\\_\\-]+)');
            label = (agent.match(exp) || [])[1];
            return label || false;
        };

        var result = {
            os : function() { //底层操作系统
                if (/windows/.test(agent)) {
                    return 'windows';
                } else if (/linux/.test(agent)) {
                    return 'linux';
                } else if (/iphone|ipod|ipad|ios/.test(agent)) {
                    return 'ios';
                }
            }(),
            ie : function() { //ie版本
                return (!!win.ActiveXObject || "ActiveXObject" in win) ? (
                    (agent.match(/msie\s(\d+)/) || [])[1] || '11' //由于ie11并没有msie的标识
                    ) : false;
            }(),
            weixin : getVersion('micromessenger') //是否微信
        };

        //任意的key
        if (key && !result[key]) {
            result[key] = getVersion(key);
        }

        //移动设备
        result.android = /android/.test(agent);
        result.ios = result.os === 'ios';

        return result;
    };

    // 提示
    Lay.fn.hint = function() {
        return {
            error : error
        }
    };

    // 遍历
    Lay.fn.each = function(obj, fn) {
        var that = this,
            key;
        if (typeof fn !== 'function') return that;
        obj = obj || [];
        if (obj.constructor === Object) {
            for (key in obj) {
                if (fn.call(obj[key], key, obj[key])) break;
            }
        } else {
            for (key = 0; key < obj.length; key++) {
                if (fn.call(obj[key], key, obj[key])) break;
            }
        }
        return that;
    };

    // 阻止事件冒泡
    Lay.fn.stope = function(e) {
        e = e || win.event;
        e.stopPropagation ? e.stopPropagation() : e.cancelBubble = true;
    };

    // 自定义模块事件
    Lay.fn.onevent = function(modName, events, callback) {
        if (typeof modName !== 'string'
            || typeof callback !== 'function') return this;
        config.event[modName + '.' + events] = [ callback ];

        //不再对多次事件监听做支持
        /*
        config.event[modName + '.' + events] 
          ? config.event[modName + '.' + events].push(callback) 
        : config.event[modName + '.' + events] = [callback];
        */

        return this;
    };

    // 执行自定义模块事件
    Lay.fn.event = function(modName, events, params) {
        var that = this,
            result = null,
            filter = events.match(/\(.*\)$/) || []; //提取事件过滤器
        var set = (events = modName + '.' + events).replace(filter, ''); //获取事件本体名
        var callback = function(_, item) {
            var res = item && item.call(that, params);
            res === false && result === null && (result = false);
        };
        layui.each(config.event[set], callback);
        filter[0] && layui.each(config.event[events], callback); //执行过滤器中的事件
        return result;
    };

    win.layui = new Lay();

}(window);

使用 Redis 实现一个轻量级的搜索引擎,牛逼啊! - 入她程序员 - 博客园

mikel阅读(721)

来源: 使用 Redis 实现一个轻量级的搜索引擎,牛逼啊! – 入她程序员 – 博客园

场景

大家如果是做后端开发的,想必都实现过列表查询的接口,当然有的查询条件很简单,一条 SQL 就搞定了,但有的查询条件极其复杂,再加上库表中设计的各种不合理,导致查询接口特别难写,然后加班什么的就不用说了(不知各位有没有这种感受呢~)。
下面以一个例子开始,这是某购物网站的搜索条件,如果让你实现这样的一个搜索接口,你会如何实现?(当然你说借助搜索引擎,像 Elasticsearch 之类的,你完全可以实现。但我这里想说的是,如果要你自己实现呢?)

从上图中可以看出,搜索总共分为6大类,每大类中又分了各个子类。这中间,各大类条件之间是取的交集,各子类中有单选、多选、以及自定义的情况,最终输出符合条件的结果集。
好了,既然需求很明确了,我们就开始来实现。关注公众号Java技术栈回复面试,可以获取整理的 Redis 系列面试题及全部答案。
实现1

率先登场是小A同学,他是写 SQL 方面的“专家”。小A信心满满的说:“不就是一个查询接口吗?看着条件很多,但凭着我丰富的 SQL 经验,这点还是难不倒我的。”
于是乎就写出了下面这段代码(这里以 MYSQL 为例):
select … from table_1
left join table_2
left join table_3
left join (select … from table_x where …) tmp_1

where …
order by …
limit m,n
代码在测试环境跑了一把,结果好像都匹配上了,于是准备上预发。这一上预发,问题就开始暴露出来。
预发为了尽可能的逼真线上环境,所以数据量自然而然要比测试大的多。所以这么一个复杂的 SQL,它的执行效率可想而知。测试同学果断把小A的代码给打了回来。
实现2

总结了小A失败的教训,小B开始对SQL进行了优化,先是通过了explain关键字进行SQL性能分析,对该加索引的地方都加上了索引。同时将一条复杂SQL拆分成了多条SQL,计算结果在程序内存中进行计算。这篇Explain 最完整总结,推荐看下。
伪代码如下:
$result_1 = query(‘select … from table_1 where …’);
$result_2 = query(‘select … from table_2 where …’);
$result_3 = query(‘select … from table_3 where …’);

$result = array_intersect($result_1, $result_2, $result_3, …);
这种方案从性能上明显比第一种要好很多,可是在功能验收的时候,产品经理还是觉得查询速度不够快。MySQL 实现一个简单版搜索引擎,这篇推荐看下。
小B自己也知道,每次查询都会向数据库查询多次,而且有些历史原因,部分条件是做不到单表查询的,所以查询等待的时间是避免不了的。
实现3

小C从上面的方案中看到了优化的空间。他发现小B在思路上是没问题的,将复杂条件拆分,计算各个子维度的结果集,最后将所有的子结果集进行一个汇总合并,得到最终想要的结果。
于是他突发奇想,能否事先将各个子维度的结果集给缓存起来,这要查询的时候直接去取想要的子集,而不用每次去查库计算。
这里小C采用 Redis 来存储缓存数据,用它的主要原因是,它提供了多种数据结构,并且在 Redis 中进行集合的交并集操作是一件很容易的事情。
具体方案,如图所示:

这里每个条件都事先将计算好的结果集ID存入对应的key中,选用的数据结构是集合(Set)。查询操作包括:
子类单选:直接根据条件 key,获取对应结果集;
子类多选:根据多个条件 Key,进行并集操作,获取对应结果集;
最终结果:将获取的所有子类结果集进行交集操作,得到最终结果;
这其实就是所谓的反向索引。
这里会发现,漏了一个价格的条件。从需求中可知,价格条件是个区间,并且是无穷举的。所以上述的这种穷举条件的 Key-Value 方式是做不到的。这里我们采用 Redis 的另一种数据结构进行实现,有序集合(Sorted Set):

将所有商品加入 Key 为价格的有序集合中,值为商品ID,每个值对应的分数为商品价格的数值。这样在 Redis 的有序集合中就可以通过ZRANGEBYSCORE命令,根据分数(价格)区间,获取相应结果集。
至此,方案三的优化已全部结束,将数据的查询与计算通过缓存的手段,进行了分离。在每次查找时,只需要简单的查找 Redis 几次就能得出结果。查询速度上符合了验收的要求。
扩展

分页
这里你或许发现了一个严重的功能缺陷,列表查询怎么能没有分页。是的,我们马上来看 Redis 是如何实现分页的。关注公众号Java技术栈回复面试,可以获取整理的 Redis 系列面试题及全部答案。
分页主要涉及排序,这里简单起见,就以创建时间为例。
如图所示:

图中蓝色部分是以创建时间为分值的商品有序集合,蓝色下方的结果集即为条件计算而得的结果,通过ZINTERSTORE命令,赋结果集权重为0,商品时间结果为1,取交集而得的结果集赋予创建时间分值的新有序集合。对新结果集的操作即能得到分页所需的各个数据:
页面总数为:ZCOUNT命令
当前页内容:ZRANGE命令
若以倒序排列:ZREVRANGE命令
数据更新
关于索引数据更新的问题,有两种方式来进行。一种是通过商品数据的修改,来即时触发更新操作,一种是通过定时脚本来进行批量更新。
这里要注意的是,关于索引内容的更新,如果暴力的删除 Key,再重新设置 Key。因为 Redis 中两个操作不会是原子性进行的,所以中间可能存在空白间隙,建议采用仅移除集合中失效元素,添加新元素的方式进行。
性能优化
Redis 是内存级操作,所以单次的查询会很快。但是如果我们的实现中会进行多次的 Redis 操作,Redis 的多次连接时间可能是不必要时间消耗。通过使用MULTI命令,开启一个事务,将 Redis 的多次操作放在一个事务中,最后通过EXEC来进行原子性执行(注意:这里所谓的事务,只是将多个操作在一次连接中执行,如果执行过程中遇到失败,是不会回滚的)。
总结

这里只是一个采用 Redis 优化查询搜索的一个简单 Demo,和现有的开源搜索引擎相比,它更轻量,学习成本页相应低些。其次,它的一些思想与开源搜索引擎是类似的,如果再加上词语解析,也可以实现类似全文检索的功能。
总结了一些2020年的面试题,这份面试题的包含的模块分为19个模块,分别是: Java 基础、容器、多线程、反射、对象拷贝、Java Web 、异常、网络、设计模式、Spring/Spring MVC、Spring Boot/Spring Cloud、Hibernate、MyBatis、RabbitMQ、Kafka、Zookeeper、MySQL、Redis、JVM 。

获取资料以上资料:关注公众号:有故事的程序员,获取学习资料。
记得点个关注+评论哦~

利用redis实现多属性快速查询 - SegmentFault 思否

mikel阅读(666)

来源: 利用redis实现多属性快速查询 – SegmentFault 思否

前言

拿京东举例,如下图

我们要找一款电子琴,牌子有:雅马哈、卡西欧,价格有各种区间,各种颜色、不同的音色数。

现如今动不动就得整点高并发啥的,直接用mySQL我们是不是真的扛不住?在前面加一层cache?怎么加?各种属性的组合存到一个属性组合成的key中?如何相对实时的更新属性?

之前的文章我有介绍过redissetbitbitop的使用方法,就是将某一位标记为1或者0代表存在不存在,然后利用bitop进行AND或者OR计算,得到我们想要的结果,今天我们就从零开始打造一个“高性能”的属性筛选器!

按属性储存数据

假设现在我们有三款电子琴,一款雅马哈、两款卡西欧,具体的属性表格为:

ID 品牌 颜色 价格 音色
1 雅马哈 红色 1000 100
2 卡西欧 黑色 2000 150
3 卡西欧 白色 2000 200

我们将属性+属性值组合为key,ID为对应的某位偏移量,这样使用下面的语句初始化数据到redis

//初始化品牌
$redis->setBit('brand-雅马哈', 1, 1);
$redis->setBit('brand-卡西欧', 2, 1);
$redis->setBit('brand-卡西欧', 3, 1);

//初始化颜色
$redis->setBit('color-红色', 1, 1);
$redis->setBit('color-黑色', 2, 1);
$redis->setBit('color-白色', 3, 1);

//初始化价格
$redis->setBit('price-1000', 1, 1);
$redis->setBit('price-2000', 2, 1);
$redis->setBit('price-2000', 3, 1);

......

随意组合属性筛选

我想要搜一下,2000元的白色卡西欧,只需要这样

$redis->bitop('AND', 'cacheKey', 'brand-卡西欧', 'color-白色');
$redis->bitop('AND', 'cacheKey1', 'cacheKey', 'price-2000');

结果cacheKey1的二进制形式为001,这样我们就知道搜索的结果是ID为3的商品。

然而redis并没有提供查询哪些位位1的方法,我们只能通过get方法将内容获取出来,自己处理。提供一段参考代码:

$bit = $redis->get($cacheKey);

$bitLength = strlen($bit);
//redis返回的数据长度可能不是8的倍数,为了方便解包,我们将它补齐
while($bitLength % 8 != 0) {
    $bitLength++;
}

$bit = str_pad($bit, $bitLength, pack('N', 0));
$bit = unpack('N*', $bit);
$bit = array_filter($bit);
$ids = [];
foreach($bit as $k => $b) {
    $bitPos = [];
    while($b) {
        $bin = sprintf('%032s', decbin($b));
        $bitPos[] = strrpos($bin, '1');
        $b &= ($b - 1);
    }

    foreach($bitPos as $pos) {
        $ids[] = ($k - 1) * 32 + $pos;
    }

}

我在本地试了一下,20W的数据(单个属性-属性值redis占用大概24k),同时搜索4个属性只需要不到10ms,当然现实中肯定没这么理想,但效果一定不会太差。

优化setbit

如果商品和属性过多,对redis的写入压力是相当大的(商品数属性数属性值数的写入数),我们可以先自行组合成字符串,然后单个属性-属性值对写入,具体实现细节就不写了,就是利用pack函数打包。

Redis for .NET 系列之实现分页需求 - b̶i̶n̶g̶.̶ - 博客园

mikel阅读(752)

来源: Redis for .NET 系列之实现分页需求 – b̶i̶n̶g̶.̶ – 博客园

代码笔记:

                var tableName = "Table1";
                redisClient.AddItemToSortedSet(tableName, "1", 1);
                redisClient.AddItemToSortedSet(tableName, "2", 2);
                redisClient.AddItemToSortedSet(tableName, "3", 3);
                var pageIndex = 1;
                var pageSize = 5 ;
                //分页查询
                var value = redisClient.GetRangeFromSortedSetDesc(tableName, (pageIndex - 1) * pageSize, (pageSize * pageIndex) - 1);

 

ServiceStack.Redis高效封装和简易破解 - 东汉 - 博客园

mikel阅读(915)

来源: ServiceStack.Redis高效封装和简易破解 – 东汉 – 博客园

 1.ServiceStack.Redis封装

封装的Redis操作类名为RedisHandle,如下代码块(只展示部分代码),它的特点:

1)使用连接池管理连接,见代码中的PooledClientManager属性。如果不用连接池,而是代码直接RedisClient client = new RedisClient(“localhost”, 6379, “password”);去获取一个连接实例操作,那么当Redis操作频繁时,代价很大,不可行。

2)支持读写分离的Redis服务端(如果你只用一个Redis服务端,那么读写服务端连接字符串一样即可)。

3)操作Redis时,自动切换读写Redis连接实例,见代码中的GetRedisClient函数,所有写操作取“写连接实例”PooledClientManager.GetClient(),所有读操作取“读连接实例”PooledClientManager.GetReadOnlyClient()。

注意:如果你读写是两个做了主从复制的Redis服务端,那么要考虑主从复制是否有延迟,是否有一些读操作要求实时数据,如果是,那么需要在GetXX读数据时用写连接实例。这时候,可以改写此GetXX函数,可在函数参数末尾增加 bool? isReadOnly = null 带默认值的参数,即支持外部调用指定用哪种连接实例操作。这种情况一般是系统把Redis当作一个NoSQL数据库;而更多时候我们系统是把Redis当作一个缓存,不需要做主从复制,读写连接实例指向的是同一个Redis服务端,当系统比较大时可能会用到缓存集群(比如一致性哈希缓存等)。

4)后继如果Redis需要做一致性哈希等集群,那么可以实例化多个RedisHandle实例,然后撰写算法来取相应的RedisHandle实例。

复制代码
  1 namespace NetDh.RedisUtility
  2 {
  3     /*
  4      * 一个RedisHandle实例对应一个Redis服务端或者一组主从复制Redis服务端。
  5      * 如果Redis需要做一致性哈希等集群,则要自己撰写算法来取相应的RedisHandle实例。
  6      */
  7 
  8     /// <summary>
  9     /// Redis操作类
 10     /// </summary>
 11     public class RedisHandle
 12     {
 13         /// <summary>
 14         /// Redis连接池管理实例
 15         /// </summary>
 16         public PooledRedisClientManager PooledClientManager { get; set; }
 17 
 18         /* 如果你的需求需要经常切换Redis数据库,则可把Db当属性,这样每一个RedisHandle实例可以对应操作某Redis的某个数据库。此时,可在构造函数中增加int db参数。*/
 19         ///// <summary>
 20         ///// 一个Redis服务端默认有16个数据库,默认都是用第0个数据库。如果需要切换数据库,则传入db值(0~15)
 21         ///// </summary>
 22         //public int Db { get; set; }
 23 
 24         /// <summary>
 25         /// 构造函数
 26         /// </summary>
 27         public RedisHandle()
 28         {
 29             #region 此代码为创建“连接池示例”,配置信息直接用静态类RedisClientConfig1承载,你也可以选择用配置文件承载
 30             var config = new RedisClientManagerConfig
 31             {
 32                 AutoStart = true,
 33                 MaxWritePoolSize = RedisClientConfig1.MaxWritePoolSize,
 34                 MaxReadPoolSize = RedisClientConfig1.MaxReadPoolSize,
 35                 DefaultDb = RedisClientConfig1.DefaultDb,
 36             };
 37             //如果你只用到一个Redis服务端,那么配置读写时就指定一样的连接字符串即可。
 38             PooledClientManager = new PooledRedisClientManager(RedisClientConfig1.ReadWriteServers
 39                 , RedisClientConfig1.ReadOnlyServers, config)
 40             {
 41                 ConnectTimeout = RedisClientConfig1.ConnectTimeout,
 42                 SocketSendTimeout = RedisClientConfig1.SendTimeout,
 43                 SocketReceiveTimeout = RedisClientConfig1.ReceiveTimeout,
 44                 IdleTimeOutSecs = RedisClientConfig1.IdleTimeOutSecs,
 45                 PoolTimeout = RedisClientConfig1.PoolTimeout
 46             };
 47             #endregion
 48         }
 49         /// <summary>
 50         /// 构造函数
 51         /// </summary>
 52         /// <param name="poolManager">连接池,外部传入自己创建的PooledRedisClientManager连接池对象,
 53         /// 可以把其它RedisHandle实例的PooledClientManager传入,共用连接池</param>
 54         public RedisHandle(PooledRedisClientManager poolManager)
 55         {
 56             PooledClientManager = poolManager;
 57 
 58         }
 59         /// <summary>
 60         /// 获取Redis客户端连接对象,有连接池管理。
 61         /// </summary>
 62         /// <param name="isReadOnly">是否取只读连接。Get操作一般是读,Set操作一般是写</param>
 63         /// <returns></returns>
 64         public RedisClient GetRedisClient(bool isReadOnly = false)
 65         {
 66             RedisClient result;
 67             if (!isReadOnly)
 68             {
 69                 //RedisClientManager.GetCacheClient()会返回一个新实例,而且只提供一小部分方法,它的作用是帮你判断是否用写实例还是读实例
 70                 result = PooledClientManager.GetClient() as RedisClient;
 71             }
 72             else
 73             {
 74                 //如果你读写是两个做了主从复制的Redis服务端,那么要考虑主从复制是否有延迟。有一些读操作是否是即时的,需要在写实例中获取。
 75                 result = PooledClientManager.GetReadOnlyClient() as RedisClient;
 76             }
 77             //如果你的需求需要经常切换Redis数据库,则下一句可以用。否则一般都只用默认0数据库,集群是没有数据库的概念。
 78             //result.ChangeDb(Db);
 79             return result;
 80         }
 81 
 82         #region 存储单值 key-value,其中value是string,使用时如果value是int,可以把比如int转成string存储
 83         public void SetValue(string key, string value, int expirySeconds = -1)
 84         {
 85             using (RedisClient redisClient = GetRedisClient())
 86             {
 87                 //redisClient.SetEntry(key, value, expireIn);
 88                 if (expirySeconds == -1)
 89                 {
 90                     redisClient.SetValue(key, value);
 91                 }
 92                 else
 93                 {
 94                     redisClient.SetValue(key, value, new TimeSpan(0, 0, 0, expirySeconds));
 95                 }
 96             }
 97         }
 98 
 99         public string GetValue(string key)
100         {
101             using (RedisClient redisClient = GetRedisClient(true))
102             {
103                 var val = redisClient.GetValue(key);
104 
105                 return val;
106             }
107         }
108 
109         public bool Remove(string key)
110         {
        ...
复制代码

5)在GetRedisClient函数中有句注释的代码//result.ChangeDb(Db);。其中,ChangeDb是切换Redis数据库(Redis默认有16个数据库,见redis-server.exe目录下的redis.conf配置文件中的“databases 16”)。我们一般默认都是用第0个数据库,如果需要切换数据库,则传入Db值(0~15)。我这边一般不会用到切换数据库的需求,如果你的需求需要经常切换Redis数据库,此句可用。否则一般都只用默认0数据库,集群是没有数据库的概念。

为了说明一个Redis服务端有多个数据库以及数据库之间的切换,做个小示例,如下图,我在Redis的第0个数据库存放了键值对数据”test2:1″,当我切到第1个数据库ChangeDb(1)时,GetValue(“test2”)返回的是null,当切回第0个数据库时,就取到1的值。

现在用命令登录Redis再演示一遍这个过程,如下图:

6)RedisHandle操作类包含的操作,大致如下图,Redis支持的数据类型比Memcache多,而且很实用,如果你的系统存取缓存会涉及比较复杂的逻辑,推荐使用Redis,Memcache能的Redis都能。

完整的源码请参考:https://gitee.com/donghan/NetDh-Framework/tree/master/Data/NetDh.RedisUtility

此工具类已经并到我的NetDh框架项目中,NetDh框架码云地址:https://gitee.com/donghan/NetDh-Framework

 

2.ServiceStack.Redis破解

我这边封装的是ServiceStack.Redis最新版本5.7.0,它在4.0版本之后就商业化,有做限制:每小时只能有6000次的Redis访问。网上有对ServiceStack.Redis和StackExchange.Reids进行比较,结果是前者性能比较好,不管真假,我是ServiceStack.Redis 3.x就开始用它了,一如既往继续用呗,有限制就破解呗。

步骤:

1)限制6000次是在ServiceStack.Text.dll中,而且在两个地方,用ILSpy打开ServiceStack.Text.dll,在搜索栏输入“RedisRequestPerHour”,可以看到RedisRequestPerHour=6000的限制,如下图(第1步你可不做,看看就好):

再搜索“AssertValidUsage”,发现另一个地方的6000次限制,如下图:

2)下载一个十六进制编辑器,我网上找的是wxMEdit工具(下载页面:http://wxmedit.github.io/downloads.html)。

3)先备份ServiceStack.Text.dll,用十六进制编辑器打开ServiceStack.Text.dll。

分析:6000转换成字节形式是 70 17 00 00(虽然6000的16进制是00001770),int的最大值2147483647转换成字节形式是 FF FF FF 7F,所以只要把70 17 00 00替换成FF FF FF 7F即可。

如下图,替换之前点了“查找下一个”发现全局就两个地方,那就确定是要修改的值,然后点击“替换”两次,ctrl+s保存文件,dll修改完成。

4)再用ILSpy看这两个值,已经修改了,如下图(第4步你也可不做,看看就好):

5)把修改的dll覆盖原来dll,最好在IDE中把原来的引用移除,重新添加引用一次,以防有缓存执行的还是旧的dll。编写如下代码测试:

覆盖dll之前会报6000限制,覆盖之后输出ok正常:

完美,点赞!

使用redis的zset实现高效分页查询(附完整代码) - 东汉 - 博客园

mikel阅读(812)

来源: 使用redis的zset实现高效分页查询(附完整代码) – 东汉 – 博客园

一、需求

移动端系统里有用户和文章,文章可设置权限对部分用户开放。现要实现的功能是,用户浏览自己能看的最新文章,并可以上滑分页查看。

 

二、数据库表设计

涉及到的数据库表有:用户表TbUser、文章表TbArticle、用户可见文章表TbUserArticle。其中,TbUserArticle的结构和数据如下图,字段有:自增长主键id、用户编号uid、文章编号aid。

 

自增长主键和分布式增长主键如何选(题外讨论):

TbUserArticle的主键是自增id,它有个缺陷是,当你的数据库有主从复制时,主从库的自增可能因死锁等原因导致不同步。不过,我们可以知道,这里的TbUserArticle的主键id不会用在其它表里,所以可以是自增id。不像用户表的主键,它就不能用自增id,因为用户表主键(uid)会经常出现在其它表中,当主从库自增不一致时,很多有uid字段的表数据在从库中就不正确了。用户表主键最好是用分布式增长主键算法生成的id(比如Snowflake雪花算法)。

那么你可能就要说了,TbUserArticle的主键为什么不直接用雪花算法产生,不管有没有用,先让主从库主键值一致总是有恃无恐。要知道,雪花算法产生的id一般是18位,而redis的zset的score是double类型,只能表达到16位”整数”部分(精确的说是9007199254740992=2的53次方)。因此,TbUserArticle的主键选择自增id。那么能不能产生一个16位(具体是53bit)的分布式增长id用于支持zset的score呢,当然也是可以的,因为目前的雪花算法是可以根据实际系统环境压缩bit位的,怎么压缩bit位呢,有许多方案,以后有需要我可以把它写出来。

建议:主键一般都要选自增id或分布式增长id,这种主键好处多多,它符合自增长(物理存储时都是在末尾追加数据,减少数据移动)、唯一性、长度小、查询快的特性,是聚集索引的很好选择。

 

三、redis缓存设计-zset

zset的作法及其优点说明:

1.zset的score倒序取数可以很好的满足取最新数据的需求。

2.用TbUserArticle的文章编号当value,用自增长id当score。自增id的唯一性可很方便的取下一页数据,直接取小于上次最后一笔的score即可(用lastScore表示)。而如果用文章的时间做score,则要考虑两笔文章的时间是同分同秒问题,当lastScore落在同分同秒的两篇文章之间时,就尴尬了,虽然有解,但麻烦了一点。有时的场景你用不了自增id当score,只能用文章时间,那怎么解决呢,方案就是当是同分同秒时,再根据文章编号做比较就好了,zset的score相同时,也是再根据value排序的,这块的代码实现请看下文第五点,只需稍微改点代码即可。

3.当新增或重新添加一项时,zset也会保持score排序。而如果用的是redis的list,一般就得从db重载缓存,新增进来的数据项就算是最新的,也不敢直接添加到list第一笔,因为并发情况下,保证不了最新就是在第一笔;至于重新添加进非最新项,那更是要从db取数重新装载缓存(一般是直接删除缓存,要用的时候才装载)。

4.第一次从db加载数据到zset时,可只取前N笔到zset。因为我们移动端的数据浏览,一般是只看最新N笔,当看到昨天浏览过的数据一般就不会再往下浏览。

5.控制zset为固定长度,防止一直增长,一是减少缓存开销,二是队列长度越短操作性能越高。而且redis服务端有两个参数:zset-max-ziplist-entries(zset队列长度,默认值128)和 zset-max-ziplist-value(zset每项大小,默认值64字节),它们的作用是,当zset长度小于128,且每个元素的大小小于64字节时,会启用ziplist(压缩双向链表),它的内存空间可以减少8倍左右,而且操作性能也更快。如果不满足这两个条件则是普通的skiplist(跳跃表)。另,数据结构hash和list默认长度是512。如果系统有100万个用户,每个用户都有自己的队列缓存,那么使用ziplist将节省非常大的内存空间,并提升很大的性能。

注意,当从zset移除一项数据,则看场景是否需要清空队列。否则有可能添加进来了一项很旧的数据,它会跑到缓存队列最底部,如果此旧数据比db中未进队列的数据还旧,那么队列中的数据就不正确了。(此时,用户滑到缓存最后一页时,就有可能浏览到这项不正确的数据,为什么是“有可能”,因为当取到zset最后一笔,很可能不够一页(一页10笔计算的话,90%会取不够一页),而不够一页就会从db直接取一页,从db直接取就不会有这项不正确的数据。而当zset又添加进一项新数据,末端那笔旧数据就会被T出队列(因为队列保持固定长度),zset数据又恢复正确了。不管怎样,这种问题几率虽不高,也是有解决方案,可搞个临界点处理此问题,不细说,否则又是长篇大论,最好的方案就是根据实际场景设计,比如从zset队列移除数据的情况多不多)。而如果添加到zset的数据都是最新数据,则不会有此问题。

当用唯一主键id做score时,这可是非常有用,你可以直接根据id定位到项了,至于如何大用它,我会再出篇博客。

 

四、代码实现

从redis缓存按页取数一般要考虑的点:

1.当根据cacheKey未取到数据时(可能是缓存过期了导致redis无此cacheKey数据),则触发重载数据(reload):从db取limit N笔数据,装载到redis zset队列中,并直接取N笔的第一页数据返回;
2.如果db本身也无对应数据,则添加”no_db_mark”标识到cacheKey队列中,下次请求则不会再触发db重载数据;
3.当取到缓存末尾时,从db取一页数据直接返回。这种情况是很少的,要根据业务场景合理规划缓存长度。

上代码:

代码注释比较详细和有用,请直接看代码。

其中,批量添加数据到zset的函数AddItemsToZset很有用,它使用lua一次性添加多笔数据到zset(注意,使用lua时,要保证lua执行快,否则它会阻塞其它命令的执行),经测试:AddItemsToZset添加1w笔数据,只需要39ms;10w笔需要448ms。因为我们只取前N笔数据到缓存,因此一般不会添加超过1w笔。

另一个通用有用的函数是GetPageDataByLastScoreFromRedis,它支持从指定的score开始取pageSize笔数据,即支持了zset分页。它是第二页(及之后)的取数,而如果取第一页取数,则直接用redis原生函数即可redis.GetRangeWithScoresFromSortedSetDesc(cacheKey, 0, pageSize – 1);。

复制代码
    /// <summary>
    /// 分页取数帮助类
    /// </summary>
    public class PageDataHelper
    {
        public readonly static string NoDbDataMark = "no_db_data";//在zset中标识db也无数据
        public static RedisHandle RedisClient = new RedisHandle();//redis操作对象示例
        public static DbHandleBase DbHandle = new SqlServerHandle("Data Source=.;Initial Catalog=Test;User Id=sa;Password=123ewq;");//db操作对象示例
        /// <summary>
        /// 按页取数。返回文章编号列表。
        /// </summary>
        /// <param name="lastInfo">上一页最后一笔的score,如果为空,则说明是取第一页。</param>
        /// <param name="getPast">true,用户上滑浏览下一页数据;false,用户上滑浏览最新一页数据</param>
        /// <returns>返回key-value列表,key就是文章编号,value就是自增id(可用于lastScore)</returns>
        public static IDictionary<string, double> GetUserPageData(string uid, int pageSize, string lastInfo, bool getPast)
        {
            long lastScore = 0;
            //1.解析lastInfo信息。->getPast为false,则固定取最新第一页数据,不用解析。lastInfo为空,则也不用解析,默认第一页
            if (getPast && !string.IsNullOrWhiteSpace(lastInfo))
            {
                lastScore = long.Parse(lastInfo);//外层有try..catch..
            }
            string cacheKey = $"usr:art:{uid}";
            bool isFirstPage = lastScore <= 0;
            using (IRedisClient redis = RedisClient.GetRedisClient())
            {
                if (isFirstPage)
                {
                    //2.第一页取数
                    var items = redis.GetRangeWithScoresFromSortedSetDesc(cacheKey, 0, pageSize - 1);
                    if (items.Count == 0)
                    {
                        //2.1 无数据时,则从db reload数据
                        items = ReloadDataToRedis(redis, cacheKey, uid, pageSize);
                        if (items.Count == 0 && pageSize > 0)
                        {
                            //如果db中也无数据,则向zset中添加一笔NoDbDataMark标识
                            redis.AddItemToSortedSet(cacheKey, NoDbDataMark, double.MaxValue);
                        }
                    }
                    else if (items.Count == 1 && items.ContainsKey(NoDbDataMark))
                    {
                        //2.2如果取到的是NoDbDataMark标识,则说明是空数据,则要Clear,返回空列表
                        items.Clear();
                    }
                    //设置缓存有效期,要根据业务场景合理设置缓存有效期,这边以7天为例。
                    redis.ExpireEntryIn(cacheKey, new TimeSpan(7, 0, 0, 0));
                    //2.3 第一页,有多少就返回多少数据。数据如果不够一页,说明本身数据不够。
                    return items;
                }
                else
                {
                    //3.第二页(及之后)取数
                    var items = GetPageDataByLastScoreFromRedis(redis, cacheKey, pageSize, lastScore);
                    if (items.Count < pageSize)
                    {
                        //3.1 如果取不够数据时,就到db取。如果db也不能取到一页数据,前端会显示无更多数据,不会一直db取。
                        return GetPageDataByLastScoreFromDb(uid, pageSize, lastScore);
                    }
                    //3.2 如果缓存数据足够,则返回缓存的数据。
                    return items;
                }
            }
        }
        public static Dictionary<string, double> ReloadDataToRedis(IRedisClient redis, string cacheKey, string uid, int pageSize, string bizId = "")
        {
            //1.db取数 取top 1000笔数据。不需要全取到缓存。
            IEnumerable<dynamic> models;
            using (var conn = DbHandle.CreateConnectionAndOpen())
            {
                var sql = $"select top 1000 id,aid from TbUserArticle where uid=@uid order by id desc;";// limit 1000;";
                models = conn.Query<dynamic>(sql, new { uid = uid });
            }
            if (models.Count() <= 0) return new Dictionary<string, double>();
            //2.数据加载到redis缓存。
            var itemsParam = new Dictionary<string, double>();
            foreach (dynamic model in models)
            {
                itemsParam.Add((string)model.aid, (double)model.id);
            }
            //使用lua一次性添加数据到缓存。lua语句要执行快,经测试添加1w笔数据,只需要39ms;10w笔需要448ms。因为sql中有limit,因此一般不会添加超过1w笔。
            //因为是原子性操作、并且是zset结构,这边不需要加锁。db取到数据应第一时间加载到redis。
            AddItemsToZset(redis, cacheKey, itemsParam, true, true);
            if (pageSize <= 0) return null;
            //3.直接由models返回第一页数据。
            return models.Take(pageSize).ToDictionary(x => (string)x.aid, y => (double)y.id);
        }

        public static Dictionary<string, double> GetPageDataByLastScoreFromDb(string uid, int pageSize, double lastScore)
        {
            //db取一页数据。
            var sql = $"select top {pageSize} id,aid from TbUserArticle where uid=@uid and id<{lastScore}order by id desc;";// limit {pageSize};";
            using (var conn = DbHandle.CreateConnectionAndOpen())
            {
                return conn.Query<dynamic>(sql, new { uid = uid }).ToDictionary(x => (string)x.aid, y => (double)y.id);
            }
        }
        #region 通用函数
        /// <summary>
        /// ZSet第一页之后的取数,从lastScore开始取pageSize笔数据(第一页之后才有lastScore)。
        /// 使用lua,保证原子性操作。
        /// </summary>
        public static Dictionary<string, double> GetPageDataByLastScoreFromRedis(IRedisClient redis, string zsetKey, int pageSize, double lastScore)
        {
            //ZREVRANGEBYSCORE: from lastScore to '-inf'.
            var luaBody = @"local sets = redis.call('ZREVRANGEBYSCORE', KEYS[1], ARGV[1], '-inf', 'WITHSCORES');
            local result = {};
            local index=0;
            local pageSize=ARGV[2]*1;
            local lastScore=ARGV[1]*1;
            for i = 1, #sets, 2 do 
                if index>=pageSize then
                    break;
                end
                if (lastScore>sets[i+1]*1) then
                    table.insert(result, sets[i]);
                    table.insert(result, sets[i+1]);
                    index=index+1;
                end
            end
            return result";
            //ARGV[1]:lastScore ARGV[2]:pageSize
            var list = redis.ExecLuaAsList(luaBody, new string[] { zsetKey }, new string[] { lastScore.ToString(), pageSize.ToString() });
            var result = new Dictionary<string, double>();
            for (var i = 0; i < list.Count; i += 2)
            {
                result.Add(list[i], Convert.ToDouble(list[i + 1]));
            }
            return result;
        }
        /// <summary>
        /// 添加一项到zset缓存中。
        /// </summary>
        /// <param name="item">要添加到zset的数据项</param>
        /// <param name="maxCount">控制zset最大长度,如果为0,则不控制。</param>
        /// <returns></returns>
        public static string AddItemToZset(IRedisClient redis, string zsetKey, KeyValuePair<string, double> item, int maxCount = 0)
        {
            var items = new Dictionary<string, double>() { { item.Key, item.Value } };
            return AddItemsToZset(redis, zsetKey, items);
        }
        /// <summary>
        /// 添加多项到zset缓存中。
        /// </summary>
        /// <param name="items">要添加到zset的数据列表</param>
        /// <param name="hasCacheExpire">缓存zsetKey是否有设置缓存有效期。如果有设置缓存有效期,则当缓存中无数据时,可能是缓存过期;而如果缓存无有效期,缓存中无数据,就是db和缓存都无数据</param>
        /// <param name="isReload">是否是reload情况,true重载情况;false追加</param>
        /// <param name="maxCount">控制zset最大长度,如果为0,则不控制。</param>
        /// <returns></returns>
        public static string AddItemsToZset(IRedisClient redis, string zsetKey, Dictionary<string, double> items, bool hasCacheExpire = true
            , bool isReload = false, int maxCount = 0)
        {
            //!isReload,是因为如果isReload=true情况无数据,则也要进来重载队列为无数据(即,如果之前有数据要重载为无数据)
            if (!isReload && items.Count <= 0) return null;
            var argArr = new List<string>(items.Count * 2 + 2);//lua参数数组
            //var hasCacheExpire = cacheValidTime != null;
            //第一个lua参数是hasCacheExpire
            argArr.Add(hasCacheExpire ? "1" : "0");
            //第二个lua参数是maxCount
            argArr.Add(maxCount.ToString());
            //组合lua其它参数列表:ZADD的参数
            foreach (var item in items)
            {
                //Add score。 //ZADD KEY_NAME SCORE1 VALUE1
                argArr.Add(item.Value.ToString());
                argArr.Add(item.Key);
            }
            #region lua
            /*
            * 以下lua命令说明。
            * 1.ZREVRANGE从大到小取第一笔数据firstMark;
            * 2.缓存有设置有效期时(hasCacheExpire=1),如果第一笔数据firstMark为nil,则说明列表是空(失效key、未生成key),则不做任何处理,直接返回字符串not_exist_key。因为可能是用户失效数据,用户长期未访问,则不添加,后继来访问时重载数据。
            * 3.如果firstMark标识为no_db_data,则是被api标识为db没数据,而此时因要ZADD数据进来,因此要把此标识删除。其中,ZREMRANGEBYRANK从小到大删除,-1是倒数第一笔。
            * 4.ZADD数据进来
            * 5.KeepLength保持队列长度操作。如果队列长度(由ZCARD获取)超过指定的maxCount,则从队列第一笔开始删除多余元素,即score最小开始删除。
            * 6.maxCount为>0才KeepLength。返回数值:curCount - maxCount。(可以用返回值简单算出队列当前长度curCount)。如果返回值小于等于0则说明没有触发删除操作。
            * 7.maxCount为<=0时,直接返回'no_remove'。
            */
            //清空原来,重新加载数据的情况
            const string reloadLua = "redis.call('DEL', KEYS[1]) ";
            //追加数据到zset的情况
            const string addToLua =
            @"local firstMark = redis.call('ZREVRANGE',KEYS[1],0,0);
            local hasCacheExpire=ARGV[1]*1;
            if hasCacheExpire==1 and firstMark and firstMark[1]==nil then
                return 'not_exist_key';
            end
            if firstMark and firstMark[1]=='{0}' then
                redis.call('ZREMRANGEBYRANK', KEYS[1], -1,-1);
            end";
            const string constAllLua =
            @"{0}
            for i=3, #ARGV, 2 
                do redis.call('ZADD', KEYS[1], ARGV[i], ARGV[i+1]);  
            end
            local maxCount=ARGV[2]*1;
            if maxCount>0 then
              local curCount= redis.call('ZCARD', KEYS[1]);
              local removeCount=curCount - maxCount;
              if removeCount>0 then
                redis.call('ZREMRANGEBYRANK', KEYS[1], 0,removeCount-1);    
              end  
              return removeCount;
            end
            return 'no_remove';";
            #endregion
            var luaBody = string.Format(constAllLua, isReload ? reloadLua : string.Format(addToLua, NoDbDataMark));
            var luaResult = redis.ExecLuaAsString(luaBody, new string[] { zsetKey }, argArr.ToArray());
            return luaResult;
        }
        #endregion
    }
复制代码

 

五、用时间做score,同分同秒问题解决

如果是用时间做score,会有同分同秒问题,比如在TbUserArticle里增加了“时间”栏位。解决方法代码只需稍作微改,参数除了lastScore(此时是“时间”),还需要传lastAid(文章编号)。

1. 缓存处理修改,只动了以下红色粗体字。(注:当zset的两笔数据score相同时,是再根据value排序的):

复制代码
   public static Dictionary<string, double> GetPageDataByLastScoreFromRedis(IRedisClient redis, string zsetKey, int pageSize, double lastScore,string lastAid)
        {
            //ZREVRANGEBYSCORE: from lastScore to '-inf'.
            var luaBody = @"local sets = redis.call('ZREVRANGEBYSCORE', KEYS[1], ARGV[1], '-inf', 'WITHSCORES');
            local result = {};
            local index=0;
            local pageSize=ARGV[2]*1;
            local lastScore=ARGV[1]*1;
            local lastAid=ARGV[3];
            for i = 1, #sets, 2 do 
                if index>=pageSize then
                    break;
                end
                if (lastScore>sets[i+1]*1) or (lastScore==sets[i+1]*1 and lastAid>sets[i]) then
                    table.insert(result, sets[i]);
                    table.insert(result, sets[i+1]);
                    index=index+1;
                end
            end
            return result";
            //ARGV[1]:lastScore ARGV[2]:pageSize
            var list = redis.ExecLuaAsList(luaBody, new string[] { zsetKey }, new string[] { lastScore.ToString(), pageSize.ToString(), lastAid });
            var result = new Dictionary<string, double>();
            for (var i = 0; i < list.Count; i += 2)
            {
                result.Add(list[i], Convert.ToDouble(list[i + 1]));
            }
            return result;
        }
复制代码

2.db取数修改

reload SQL

$”select top 1000 时间,aid from TbUserArticle where uid=@uid order by 时间 desc,aid desc;”;

db中取一页的SQL

$”select top {pageSize} 时间,aid from TbUserArticle where uid=@uid and (时间<{lastScore} or (时间={lastScore} and aid<‘{lastAid}’)) order by 时间 desc,aid desc;”;

这样就可以了,中心思想就是:当“时间={lastScore} ”,那么就增加文章编号比较条件。

Redis 分页排序查询_Hello World .-CSDN博客_redis 分页

mikel阅读(552)

来源: Redis 分页排序查询_Hello World .-CSDN博客_redis 分页

edis是一个高效的内存数据库,它支持包括String、List、Set、SortedSet和Hash等数据类型的存储,在Redis中通常根据数据的key查询其value值,Redis没有条件查询,在面对一些需要分页或排序的场景时(如评论,时间线),Redis就不太好不处理了。
前段时间在项目中需要将每个主题下的用户的评论组装好写入Redis中,每个主题会有一个topicId,每一条评论会和topicId关联起来,得到大致的数据模型如下:

{
topicId: ‘xxxxxxxx’,
comments: [
{
username: ‘niuniu’,
createDate: 1447747334791,
content: ‘在Redis中分页’,
commentId: ‘xxxxxxx’,
reply: [
{
content: ‘yyyyyy’
username: ‘niuniu’
},

]
},

]
}
将评论数据从MySQL查询出来组装好存到Redis后,以后每次就可以从Redis获取组装好的评论数据,从上面的数据模型可以看出数据都是key-value型数据,无疑要采用hash进行存储,但是每次拿取评论数据时需要分页而且还要按createDate字段进行排序,hash肯定是不能做到分页和排序的。

那么,就挨个看一下Redis所支持的数据类型:

1、String: 主要用于存储字符串,显然不支持分页和排序。
2、Hash: 主要用于存储key-value型数据,评论模型中全是key-value型数据,所以在这里Hash无疑会用到。
3、List: 主要用于存储一个列表,列表中的每一个元素按元素的插入时的顺序进行保存,如果我们将评论模型按createDate排好序后再插入List中,似乎就能做到排序了,而且再利用List中的LRANGE key start stop指令还能做到分页。嗯,到这里List似乎满足了我们分页和排序的要求,但是评论还会被删除,就需要更新Redis中的数据,如果每次删除评论后都将Redis中的数据全部重新写入一次,显然不够优雅,效率也会大打折扣,如果能删除指定的数据无疑会更好,而List中涉及到删除数据的就只有LPOP和RPOP这两条指令,但LPOP和RPOP只能删除列表头和列表尾的数据,不能删除指定位置的数据,所以List也不太适合(转载的时候看了下,是有 LREM命令可以做到删除,但是LRANGE 似乎是一个耗时命令 O(N) )。
4、Set: 主要存储无序集合,无序!排除。
5、SortedSet: 主要存储有序集合,SortedSet的添加元素指令ZADD key score member [[score,member]…]会给每个添加的元素member绑定一个用于排序的值score,SortedSet就会根据score值的大小对元素进行排序,在这里就可以将createDate当作score用于排序,SortedSet中的指令ZREVRANGE key start stop又可以返回指定区间内的成员,可以用来做分页,SortedSet的指令ZREM key member可以根据key移除指定的成员,能满足删评论的要求,所以,SortedSet在这里是最适合的(时间复杂度O(log(N)))。

所以,我需要用到的数据类型有SortSet和Hash,SortSet用于做分页排序,Hash用于存储具体的键值对数据,我画出了如下的结构图:

 

在上图的SortSet结构中将每个主题的topicId作为set的key,将与该主题关联的评论的createDate和commentId分别作为set的score和member,commentId的顺序就根据createDate的大小进行排列。
当需要查询某个主题某一页的评论时,就可主题的topicId通过指令zrevrange topicId (page-1)×10 (page-1)×10+perPage这样就能找出某个主题下某一页的按时间排好顺序的所有评论的commintId。page为查询第几页的页码,perPage为每页显示的条数。
当找到所有评论的commentId后,就可以把这些commentId作为key去Hash结构中去查询该条评论对应的内容。
这样就利用SortSet和Hash两种结构在Redis中达到了分页和排序的目的。

博主额外添加的实现算法:

@Test
public void sortedSetPagenation(){
for ( int i = 1 ; i <= 100 ; i+=10) {
// 初始化CommentId索引 SortSet
RedisClient.zadd(“topicId”, i, “commentId”+i);
// 初始化Comment数据 Hash
RedisClient.hset(“Comment_Key”,”commentId”+i, “comment content …….”);
}
// 倒序取 从0条开始取 5条 Id 数据
LinkedHashSet<String> sets = RedisClient.zrevrangebyscore(“topicId”, “80”, “1”, 0, 5);
String[] items = new String[]{};
System.out.println(sets.toString());
// 根据id取comment数据
List<String> list = RedisClient.hmget(“Comment_Key”, sets.toArray(items));
for(String str : list){
System.out.println(str);
}
}
工具类:
package com.util;

import java.util.LinkedHashSet;
import java.util.List;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

/**
* Redis 客户端集群版
*
* @author babylon
* 2016-5-10
*/
public class RedisClient{

private static JedisPool jedisPool;

static {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(Global.MAX_ACTIVE);
config.setMaxIdle(Global.MAX_IDLE);
config.setMaxWaitMillis(-1);
config.setTestOnBorrow(Global.TEST_ON_BORROW);
config.setTestOnReturn(Global.TEST_ON_RETURN);
jedisPool = new JedisPool(“redis://:”+Global.REDIS_SERVER_PASSWORD+”@”+Global.REDIS_SERVER_URL+”:”+Global.REDIS_SERVER_PORT);
// jedisPool = new JedisPool(config, Global.REDIS_SERVER_URL, Integer.parseInt(Global.REDIS_SERVER_PORT), “zjp_Redis_224”);
}

public static String set(String key, String value) {
Jedis jedis = jedisPool.getResource();
String result = jedis.set(key, value);
jedis.close();
return result;
}

public static String get(String key) {
Jedis jedis = jedisPool.getResource();
String result = jedis.get(key);
jedis.close();
return result;
}

public static Long hset(String key, String item, String value) {
Jedis jedis = jedisPool.getResource();
Long result = jedis.hset(key, item, value);
jedis.close();
return result;
}

public static String hget(String key, String item) {
Jedis jedis = jedisPool.getResource();
String result = jedis.hget(key, item);
jedis.close();
return result;
}

/**
* Redis Hmget 命令用于返回哈希表中,一个或多个给定字段的值。
如果指定的字段不存在于哈希表,那么返回一个 nil 值。
* @param key
* @param item
* @return 一个包含多个给定字段关联值的表,表值的排列顺序和指定字段的请求顺序一样。
*/
public static List<String> hmget(String key, String… item) {
Jedis jedis = jedisPool.getResource();
List<String> result = jedis.hmget(key, item);
jedis.close();
return result;
}

public static Long incr(String key) {
Jedis jedis = jedisPool.getResource();
Long result = jedis.incr(key);
jedis.close();
return result;
}

public static Long decr(String key) {
Jedis jedis = jedisPool.getResource();
Long result = jedis.decr(key);
jedis.close();
return result;
}

public static Long expire(String key, int second) {
Jedis jedis = jedisPool.getResource();
Long result = jedis.expire(key, second);
jedis.close();
return result;
}

public static Long ttl(String key) {
Jedis jedis = jedisPool.getResource();
Long result = jedis.ttl(key);
jedis.close();
return result;
}

public static Long hdel(String key, String item) {
Jedis jedis = jedisPool.getResource();
Long result = jedis.hdel(key, item);
jedis.close();
return result;
}

public static Long del(String key) {
Jedis jedis = jedisPool.getResource();
Long result = jedis.del(key);
jedis.close();
return result;
}

public static Long rpush(String key, String… strings) {
Jedis jedis = jedisPool.getResource();
Long result = jedis.rpush(key, strings);
jedis.close();
return result;
}

/**
* Redis Lrange 返回列表中指定区间内的元素,区间以偏移量 START 和 END 指定。 其中 0 表示列表的第一个元素, 1 表示列表的第二个元素,以此类推。
* 你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。
* @param string
* @param start
* @param end
* @return
*/
public static List<String> lrange(String key, int start, int end) {
Jedis jedis = jedisPool.getResource();
List<String> result = jedis.lrange(key, start, end);
jedis.close();
return result;
}

/**
* 从列表中从头部开始移除count个匹配的值。如果count为零,所有匹配的元素都被删除。如果count是负数,内容从尾部开始删除。
* @param string
* @param string2
* @param i
*/
public static Long lrem(String key, Long count, String value) {
Jedis jedis = jedisPool.getResource();
Long result = jedis.lrem(key, count, value);
jedis.close();
return result;
}

/**
* Redis Zadd 命令用于将一个或多个成员元素及其分数值加入到有序集当中。
如果某个成员已经是有序集的成员,那么更新这个成员的分数值,并通过重新插入这个成员元素,来保证该成员在正确的位置上。
分数值可以是整数值或双精度浮点数。
如果有序集合 key 不存在,则创建一个空的有序集并执行 ZADD 操作。
当 key 存在但不是有序集类型时,返回一个错误。
* @param string
* @param i
* @param string2
* @return 被成功添加的新成员的数量,不包括那些被更新的、已经存在的成员。
*/
public static Long zadd(String key, double score, String member) {
Jedis jedis = jedisPool.getResource();
Long result = jedis.zadd(key, score, member);
jedis.close();
return result;
}

/**
* Redis Zrevrangebyscore 返回有序集中指定分数区间内的所有的成员。有序集成员按分数值递减(从大到小)的次序排列。
具有相同分数值的成员按字典序的逆序(reverse lexicographical order )排列。
除了成员按分数值递减的次序排列这一点外, ZREVRANGEBYSCORE 命令的其他方面和 ZRANGEBYSCORE 命令一样。
* @param key
* @param max
* @param min
* @param offset
* @param count
* @return 指定区间内,带有分数值(可选)的有序集成员的列表。
*/
public static LinkedHashSet<String> zrevrangebyscore(String key, String max, String min, int offset, int count){
Jedis jedis = jedisPool.getResource();
LinkedHashSet<String> result = (LinkedHashSet<String>) jedis.zrevrangeByScore(key, max, min, offset, count);
jedis.close();
return result;
}

}

————————————————
版权声明:本文为CSDN博主「LogansCodingLife」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/jack85986370/article/details/51483872

1秒钟复制百度文库中所有内容-小刀娱乐网 - 专注活动,软件,教程分享!总之就是网络那些事。

mikel阅读(755)

来源: 1秒钟复制百度文库中所有内容-小刀娱乐网 – 专注活动,软件,教程分享!总之就是网络那些事。

很多人经常会上百度搜索资料,结果发现在百度文库那边可以找到,兴奋了半天却发现下载时要币的,或者登陆上去麻烦,又或者限制VIP才能复制下载。针对这种情况,今天给大家带来一个破解百度文库下载的方法,其实非常简单,而且不用下载任何软件。

1.jpg

打开要复制的文库内容,在浏览器极速模式下点击F12或右键打开审查元素,点击Console,粘贴以下代码然后回车。

var box = document.getElementsByClassName("ie-fix");for(var i=0;i<box.length;i++){        console.log(box[i].innerText);}

HT_20210815160358.jpg

整篇文档就出现下面粘贴的代码里随便复制了,此方法仅限文字类的文档。

HT_20210815160356.jpg

8月15日置顶:已更新最新可用代码