zoukankan      html  css  js  c++  java
  • Redis到底该如何利用(二)?

    上一篇文章里我简述了使用Keys作为Redis搜索的方式,确实感受到了社区的力量,写文章好处多。首先谢谢各位前辈的指导,我知道了拿Redis作为搜索是个错误的方向。本来这篇文章我觉得确实没必要发了,但是想想既然错了,那就将错就错,写出来给初学者一些思考吧。

    本篇我将会讲讲,分词建立key索引和redis scan命令两种方式。

    注意:这两种方式的搜索也不一定可行,具体场景要具体测试衡量,拿Redis做搜索要深思熟虑并且测试,甚至是要直接回避的。

    另外,上篇评论也建议大家看一看,前辈们给了很多经验总结,有一些同学可能没明白。这些点我先整理下:

    1. 我采用了StactkExchange.Redis,而不是ServiceStack.Redis。对于后者我觉得是个好工具,但是4.0开始收费了,3.9功能不是特别全,一些地方存在不足。

    2. 有同学建议GetAll之类的方式,我觉得对于缓存应该还是不要StringSet(list)StringGet(list)的方式吧,毕竟数据量大了,序列化反序列化就费时。这点不知道大家怎么看?我个人觉得每条记录应该是一个key-value,这个value应该是避免存成整个集合的,否则效率何在?

    3. 上一篇中的Keys模糊匹配,请大家在实际运用的时候忽略掉。因为Keys会引发Redis锁,并且增加Redis的CPU占用,情况是很恶劣的。

    分词索引法

    这种方式是我实践过后,结合上篇的前辈给的观点觉得唯一比较可行且符合redis特性的方式,不过最终效率上还是比不过内存。

    详细的实现思路清看Redis作者博客(参考资料1),这里的例子还是基于UserName,英文,并且只针对词组做了长度为3的分词,其他场景请自行扩展。

     首先基于AutoComplete的字母搜索,那么我们需要对所有的Name做一个分词,即:

    abc => (a, ab, abc)

    形成一个Set的集合形式:

    那么输入a,我们就直接取set a里的内容,输入ab就直接取ab集合的内容。那么我们开始转换,首先我们需要对User表的姓名进行分词:

    var redis = ConnectionMultiplexer.Connect("localhost");
    var db = redis.GetDatabase();
    
    for (var i = 1; i < 4; i++)
    {
        var data = dbCon.Lookup<string, int>(string.Format(@"select words, id from (
                                        select Row_number() over (partition by words order by name) as rn,id,words from (
                                            select  id, SUBSTRING(name, 1, {0}) as words, name from User 
                                        ) as t
                                        ) t2 where rn <= {1} and words != '' and words is not null", i, 20));
    
        data.ForEach((key, item) =>
          {
             db.SetAdd("capqueen:Cache:user:" + key.ToLower(), item.Select<int, RedisValue>(j => j).ToArray());
          });
    }

    第一步:采用SQL,分组排序筛选出每个分词的前20条数据,这里使用的是OrmLite的语法。

    第二部:存入RedisSet,注意这里其实只是做了一个索引,并不保存具体的User内容,效果如下:

    接着搜索的时候我们可以实现如下:

    public List<User> SearchWords(string keywords)
    {
                var redis = ConnectionMultiplexer.Connect("localhost");
                var db = redis.GetDatabase();
                var result = db.SetMembers("capqueen:Cache:user:" + keywords.ToLower());
                var users = new List<User>();
    
                if (result.Any())
                {
                    //转换成ids                
                    var ids = result.ToList().Select<RedisValue, RedisKey>(i => i.ToString());
                    //按照keys获取value ,事先已经存好了Users
                    var values = db.StringGet(ids.ToArray());
    
                    //构造List Json以加速解析
                    var portsJson = new StringBuilder("[");
    
                    values.ToList().ForEach(item =>
                    {
                        if (!string.IsNullOrWhiteSpace(item))
                        {
                            portsJson.Append(item).Append(",");
                        }
                    });
    
                    portsJson.Append("]");
    
                    users = JsonConvert.DeserializeObject<List<User>>(portsJson.ToString());
                }
    }

    经过实际的测试,这样的写法比前面的Keys确实好了不少,但是性能还是差强人意的。

    Scan搜索法

    这种方法是我在查阅了Redis的文档之后,发现的,但是也就是试验一下,估计也不能用做生产环境大规模查询。

    Scan根据数据结构的不同分为了SCANHSCANSSCANSCAN,具体的信息请看文档。我们这里采用了ZSCAN:

    ZSCAN key cursor [MATCH pattern] [COUNT count]

    这里cursor是搜索的迭代的一个游标,具体还没弄明白,pattern就是匹配规则 count就是记录条数

     

    由于我使用的是StackExchange.Redis,它提供的zscan方法是:

    IEnumerable SortedSetScan(RedisKey key, RedisValue pattern = null, int pageSize = 10, long cursor = 0, int pageOffset = 0, CommandFlags flags = CommandFlags.None);

    用过之后,我发现了这里的pageSize/pageOffset貌似没有效果,为此我还特地上github为作者留了言,他给我一些解释:

     https://github.com/StackExchang, 我的英语比较差,请凑合看。

    public void CreateTerminalCache(List<User> users)
    {
                if (users == null) return;
    
                var db = ConnectionMultiplexer.GetDatabase();
                
                var sourceData = new List<KeyValuePair<RedisKey, RedisValue>>();
                //构造集合数据
                var list = users.Select(item =>
                {
                    var value = JsonConvert.SerializeObject(item);
                    //构造原始数据
                    sourceData.Add(new KeyValuePair<RedisKey, RedisValue>("capqueen:users:" + item.Id, value));
    
                    //构造数据    
                    return new SortedSetEntry(item.Name, item.Id);
                });
    
                //添加进有序集合,采用name - id 
                db.SortedSetAdd("capqueen:users:index", list.ToArray());
    
    
                //添加港口数据key-value
                db.StringSet(sourceData.ToArray(), When.Always, CommandFlags.None);
    }

    然后搜索的时候如下:

    public List<User> GetUserByWord(string words)
    {
    
    
                var db = ConnectionMultiplexer.GetDatabase();
                
                //搜索
                var result = db.SortedSetScan("capqueen:users:index", words + "*", 10, 1, 30, CommandFlags.None).Take(30).ToList();
               
                var users = new List<User>();
    
                if (result.Any())
                {
                    //转换成ids                
                    var ids = result.ToList().Select<SortedSetEntry, RedisKey>(i => i.ToString());
    
                    //按照keys获取value
                    var values = db.StringGet(ids.ToArray());
    
                    //构造List Json以加速解析
                    var portsJson = new StringBuilder("[");
    
                    values.ToList().ForEach(item =>
                    {
                        if (!string.IsNullOrWhiteSpace(item))
                        {
                            portsJson.Append(item).Append(",");
                        }
                    });
    
                    portsJson.Append("]");
    
                    users = JsonConvert.DeserializeObject<List<User>>(portsJson.ToString());
                }
    
                return users;
    }        

    总结

    总的来说,通过这么一些列的研究和前辈们的指导,我对Redis有了一些了解。AutoComplete的场景是真的不适合使用Redis,可以说目前Redis用来做一些搜索可能还早,期待以后会有相关功能吧。上一篇文章里,有些前辈给的 意见很好,希望大家也可以学习一下。

    1. 分级缓存,该到内存的还是应该保存到appServer的内存,redis只是集中式缓存的一步。
    2. 多增加一个数据服务器,几种提供数据服务,这样可以把一些缓存直接统一到这个机器来做。链接
    3. 感谢前辈们的留言,尤其感谢@雷兽 前辈等

    参考资料

    1. Redis作者博客,这是其中一篇讲如何基于Redis实现AutoComplete的文章:http://oldblog.antirez.com/post/autocomplete-with-redis.html
    2. Redis 第三方管理工具 For Windows:http://redisdesktop.com/
    3. Redis .NET链接工具的Top20:http://nugetmusthaves.com/Tag/Redis
    4. Redis命令中文文档:http://redisdoc.com/
    5. 知乎上的一个讨论:http://www.zhihu.com/question/19764056
  • 相关阅读:
    为什么要对url进行encode
    活在当下
    Linux Shell 文本处理工具
    Servlet、Servlet容器等内容讲解
    Java编程中的一些常见问题汇总
    创建文件目录
    ubuntu
    iptables
    mysqldump导入导出
    pt-table-sync
  • 原文地址:https://www.cnblogs.com/capqueen/p/HowToUseRedis2.html
Copyright © 2011-2022 走看看