微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!
Redis简单案例(二) 网站最近的访问用户
我们有时会在网站中看到最后的访问用户、最近的活跃用户等等诸如此类的一些信息。本文就以最后的访问用户为例,用Redis来实现这个小功能。在这之前,我们可以先简单了解一下在oracle、sqlserver等关系型数据库中是怎么实现的。不可否认至少会有一张表来记录,根据时间desc排序,再取出前几条数据。下面来看看怎么用Redis来实现这个小功能:案例用到的一些相关技术和说明: 技术说明Redis存储数据,用了主从的模式,主写从读artTemplate主要是用于显示最后登陆的5位用户的名称简单的思考:要用Redis的那种数据结构来存储这些数据呢?我们只要显示最后的5个访问用户(游客不在统计之内),结合一些数据的操作,个人认为,List是个比较好的选择。 要记录下是那个用户的访问,必须要有一个登陆的操作控制。1 /// <summary>2 /// simulating user login3 /// </summary>4 /// <param name="name"></param>5 /// <returns></returns>6 [HttpPost("/login")]7 public IActionResult Login(string name)8 {9 if (!string.IsNullOrWhiteSpace(name))10 {11 //Distinct12 var tran = _redis.GetTransaction();13 tran.ListRemoveAsync(_key, name, 1);14 tran.ListLeftPushAsync(_key, name);15 tran.Execute();1617 var json = new { code="000",msg= string.Format("{0} login successfully",name) };18 return Json(json);19 }20 else21 {22 var json = new { code = "001", msg = "name can't be empty" };23 return Json(json);24 }25 }在处理登陆时,难免会出现这样的情况,在一段时间内只有1个用户登陆,而且这个用户还由于一些原因登陆了多次,所以我们需要简单的处理一下,让我们的List只记录下最新的那个记录就好,所以要先把已经存在的先remove掉,然后才把新的记录push进去。接下来就是处理要显示的信息了。我们需要先知道我们的key中已经有多少个元素(用户)了,然后根据这个数量来进行不同的处理:当不足5个的时候,就不用进行ltrim操作,直接取全部数据就好了,超过5个时,就先用ltrim处理一下,再取List中的数据。1 /// <summary>2 /// get the last 5 login user3 /// </summary>4 /// <returns></returns>5 [HttpGet("/login/last")]6 public IActionResult GetLastFiveLoginUser()7 {8 var len = _redis.LLen(_key);9 if (len > _loginUserAmount)10 {11 //limit the count12 _redis.LTrim(_key, 0, _loginUserAmount-1);13 }14 var list = (from i in _redis.LRange(_key, 0, -1) select i.ToString());1516 var json = new { code="000",msg="ok",data = list };17 return Json(json);18 }到这里,我们的后台逻辑已经实现了,下面就是前台的展示了。要模拟多个用户登陆,所以就用了几个按钮来模拟,触发点击事件就是登陆成功。登陆成功之后自然在更新最近的访问用户信息,所以要在登陆成功的回调函数中去刷新一下访问用户的信息。登陆的function如下:1 function login(name) {2 $.ajax({3 url: "/login",4 data: { "name": name },5 dataType: "json",6 method: "POST",7 success: function (res) {8 if (res.code == "000") {9 getLastFiveLoginUser();10 } else {11 console.log(res.msg);12 }13 }14 });15 }下面就是登陆成功的回调函数,取到数据后便向模板中灌数据,然后把根据模板得到的html放到id为lastLoginUser的div中。具体代码如下:1 function getLastFiveLoginUser() {2 $.ajax({3 url: "/login/last",4 data: {},5 dataType: "json",6 success: function (res) {7 if (res.code == "000") {8 var html = template('lastLoginUserTpl', res);9 $("#lastLoginUser").html(html);10 }11 }12 });13 }上面说到的模板,定义是十分简单的,更多有关于这个模板引擎的信息可以参考这个地址:https://github.com/aui/artTemplate下面是模板的具体代码:1 <script id="lastLoginUserTpl" type="text/html">2 <ul>3 {{each data as item}}4 <li>5 {{item}}6 </li>7 {{/each}}8 </ul>9 </script> 好了,到这里是前后台都处理好了,下面来看看效果:可以看到,正如我们的预期,只显示最后登陆的5个用户的名称。再来看看redis里面的数据:正好应验了前面说的只保留了最后的5个。记录最新的一些日记信息、交易信息等等都是属于一个大类的,其实对于这一类问题,都是可以用List来处理的,可以来看看官网的这段话,这段话包含了许多的应用场景。This pair of commands will push a new element on the list, while making sure that the list will not grow largerthan 100 elements. This is very useful when using Redis to store logs for example. It is important to note thatwhen used in this way LTRIM is an O(1) operation because in the average case just one element is removed fromthe tail of the list.  
Redis简单案例(三) 连续登陆活动的简单实现
连续登陆活动,或许大家都不会陌生,简单理解就是用户连续登陆了多少天之后,系统就会送一些礼品给相应的用户。最常见的莫过于游戏和商城这些。游戏就送游戏币之类的东西,商城就送一些礼券。正值国庆,应该也有不少类似的活动。下面就对这个的实现提供两个思路,并提供解决方案。思路1(以用户为维度):连续登陆活动,必然是要求连续登陆,不能有间隔。用1表示登陆,0表示没有登陆,这样我们可以为每个用户创建一个key去存储他的登陆情况,就可以得到类似这样的一个二进制序列:1110111,如果是7个1,就表示连续7天,如果不是7个1就表示没有连续登陆7天。所以就能实现这个登陆活动的要求了。思路2(以天数为维度):一天之内,用户要么是登陆过,要么是没有登陆过。同样的用1来表示登陆过,用0表示没有登陆过。假设我们连续登陆的活动是2天,同时有3个用户,那么就要有2个key去存储这3个用户的登陆信息,这样就会得到类似这样的两个二进制序列:101(key1),111(key2)。此时,对这两个key的每一位都进行逻辑与运算,就会得到101,就表明,用户1和用户3连续登陆了两天。从而达到活动的要求。之前在string的基础教程中曾经说过关于二进制的相关操作会用一个简单的案例来给大家讲解,现在是兑现这个诺言的时候了。 下面就简单模拟一下国庆7天假期连续登陆七天的活动。方案1 :以用户为维度先为每个用户创建一个key(holiday:用户标识),对于我们的例子来说,每个key就会有7位二进制位。这时key会有这样的结构这时我们就会得到每个用户对应的二进制序列,然后就可以用bitcount命令去得到key含有的1的个数。如果等于7,就是连续登陆了七天。这样就可以在第七天用户登陆的时间去处理了是否发送礼品了。处理的逻辑是十分简单的。控制器简单逻辑如下:1 [HttpPost]2 public IActionResult LoginForEveryone()3 {4 Random rd = new Random();5 var tran = _redis.GetTransaction();6 for (int i = 0; i < 7; i++)7 {8 for (int j = 0; j < 1000; j++)9 {10 string activity_key = string.Format("holiday:{0}", j.ToString());11 // login 1(true) other 0(false)12 if (rd.Next(0,10) > 6)13 {14 tran.StringSetBitAsync(activity_key, i, true);15 }16 }17 }18 tran.ExecuteAsync();1920 List<int> res = new List<int>();21 for (int i = 0; i < 1000; i++)22 {23 string activity_key = string.Format("holiday:{0}", i.ToString());24 //7 days25 if (_redis.BitCount(activity_key) == 7)26 {27 res.Add(i);28 }29 }30 return Json(new { code = "000", data = res, count = res.Count });31 } 在这里还是用随机数的方法来模拟数据。主要操作有两个,一个是模拟登陆后把当天对应的偏移设置为1(true),另一个是取出用户登陆的天数。这是一次性模拟操作,与正常情况的登陆操作还是有些许不同的。大致如下:1 [HttpPost]2 public IActionResult LoginForEveryone()3 {4 //1.login and get the identify of user5 //2.get the Current day and write to redis6 string activity_key = string.Format("holiday:{0}", "identify of user");7 _redis.SetBit(activity_key, currend day, true);8 //3.send gift9 if(currend day==7&& _redis.BitCount(activity_key)==7)10 {11 send gift12 }13 return ...;14 }回到我们模拟的情况,在界面展示时,模拟登陆后会显示累计登陆用户的id。1 <script id="everyoneTpl" type="text/html">2 <span>total:{{count}}</span>3 <ul>4 {{each data as item}}5 <li>6 {{item}}7 </li>8 {{/each}}9 </ul>10 </script>11 <script>12 $(function () {13 $("#btn_everyone").click(function () {14 $.ajax({15 url: "/Holiday/LoginForEveryone",16 dataType: "json",17 method:"post",18 success: function (res) {19 if (res.code == "000") {20 var html = template('everyoneTpl', res);21 $("#div_everyone").html(html);22 }23 }24 })25 });26 })27 </script>下面来看看效果: 演示中:38、103、234、264、412、529这6位用户将得到连续登陆7天的礼品。  方案2 :以天数为维度 既然是以天数为维度,那么就要定义7个redis的key用来当作每天的登陆记录,类似: 这样的话就要让我们的用户标识是数字才行,如果是用guid做的用户标识就要做一定的处理将其转化成数字,这样方便我们在给用户设置是否登陆。现在假设我们的用户标识是从1~1000。用户标识对应的就是在key中的偏移量。这时我们就会得到每天对应的二进制序列,然后就可以用bitop命令去得到逻辑与运算之后的key/value。如果这个key对应偏移量(用户标识)是1,就是连续登陆了七天,处理的逻辑是十分简单的。控制器简单逻辑如下:1 [HttpPost]2 public IActionResult LoginForEveryday()3 {4 var tran = _redis.GetTransaction();56 for (int i = 0; i < 7; i++)7 {8 for (int j = 0; j < 1000; j++)9 {10 //i day,j userId11 SetBit(i, j, tran);12 }13 }14 tran.Execute();15 //get the result16 _redis.BitOP(_bitWise, _res, _redisKeys.ToArray());17 IList<int> res = new List<int>();18 for (int i = 0; i < 1000; i++)19 {20 if (_redis.GetBit(_res, i) == true)21 {22 res.Add(i);23 }24 }25 return Json(new { code = "000", data = res, count = res.Count });26 }272829 private void SetBit(int day, int userId, StackExchange.Redis.ITransaction tran)30 {31 switch (day)32 {33 case 0:34 if (_rd.Next(0, 10) > 3)35 {36 tran.StringSetBitAsync(_first, u
Redis简单案例(四) Session的管理
负载均衡,这应该是一个永恒的话题,也是一个十分重要的话题。毕竟当网站成长到一定程度,访问量自然也是会跟着增长,这个时候,一般都会对其进行负载均衡等相应的调整。现如今最常见的应该就是使用Nginx来进行处理了吧。当然Jexus也可以达到一样的效果。既然是负载均衡,那就势必有多台服务器,如果不对session进行处理,那么就会造成Session丢失的情况。有个高大上的名字叫做分布式Session。举个通俗易懂的例子,假设现在有3台服务器做了负载,用户在登陆的时候是在a服务器上进行的,此时的session是写在a服务器上的,那么b和c两台服务器是不存在这个session的,当这个用户进行了一个操作是在b或c进行处理的,而且这个操作是要登录后才可以的,那么就会提示用户重新登陆。这样显然就是很不友好,造成的用户体验可想而知。背景交待完毕,简单的实践一下。相关技术说明ASP.NET Core演示的两个站点所用的技术Redis用做Session服务器Nginx/Jexus用做反向代理服务器,演示主要用了Nginx,最后也介绍了Jexus的用法IIS/Jexus用做应用服务器,演示用了本地的IIS,想用Jexus来部署可参考前面的相关文章先来看看不进行Session处理的做法,看看Session丢失的情况,然后再在其基础上进行改善。在ASP.NET Core中,要使用session需要在Startup中的ConfigureServices添加 services.AddSession();  以及在Configure中添加 app.UseSession(); 才能使用。在控制器中的用法就是 HttpContext.Session.XXX ,下面是演示控制器的具体代码:1 [HttpGet("/")]2 [ResponseCache(NoStore =true)]3 public IActionResult Index()4 {5 ViewBag.Site = "site 1";6 return View();7 }8 [HttpPost("/")]9 public IActionResult Index(string sessionName,string sessionValue)10 {11 //set the session12 HttpContext.Session.Set(sessionName,System.Text.Encoding.UTF8.GetBytes(sessionValue));13 return Redirect("/about?sessionName="+sessionName);14 }1516 [HttpGet("/about")]17 [ResponseCache(NoStore = true)]18 public IActionResult About(string sessionName)19 {20 byte[] bytes;21 ViewBag.Site = "site 1";22 //get the session23 if (HttpContext.Session.TryGetValue(sessionName, out bytes))24 {25 ViewBag.Session = System.Text.Encoding.UTF8.GetString(bytes);26 }27 else28 {29 ViewBag.Session = "empty";30 }31 return View();32 }其中的ViewBag.Site是用来标识当前访问的是那个负载的站点。这用就不用去查日记访问了那个站点了,直接在页面上就能看到了。从Session的用法也看出了与之前的有所不同,Session的值是用byte存储的。我们可以写个扩展方法把它封装一下,这样就方便我们直接向之前一样的写法,不用每次都转成byte再进行读写了。视图比较简单,一个写Session,一个读Session。Index.cshtml用于填写Session的信息,提交后跳转到About.cshtml。1 @{2 ViewData["Title"] = "Home Page";3 }4 <div class="row">5 <div class="col-md-6">6 <form method="post" action="/">7 <div class="form-group">8 <label>session name</label>9 <input type="text" name="sessionName" />10 </div>11 <div class="form-group">12 <label>session value</label>13 <input type="text" name="sessionValue" />14 </div>15 <button type="submit">set session</button>16 </form>17 </div>18 </div>19 <div class="row">20 <div class="col-md-6">21 <p>22 site: @ViewBag.Site23 </p>24 </div>25 </div>Index.cshtml 1 @{2 ViewData["Title"] = "About";3 }4 <p>@ViewBag.Session </p>5 <p>site:@ViewBag.Site</p>About.cshtml到这里,我们是已经把我们要的“网站”给开发好了,下面是把这个“网站”部署到IIS上面。我们要在IIS上部署两个站点,这两个站点用于我们负载均衡的使用。两个站点的区分就是ViewBag.Site,一个显示site1,一个显示site2。ASP.NET Core在IIS上部署可能不会太顺畅,这时可以参考dotNET Core的文档,至于为什么没有放到Linux下呢,毕竟是台老电脑了,开多个虚拟机电脑吃不消,云服务器又还没想好要租那家的,所以只好放到本地的IIS上来演示了,想在Linux下部署ASP.NET Core可以参考我前面的博文,也是很简单的喔。这是部署到本地IIS上面的两个站点,site1和site2。站点我们是已经部署OK了,还是要先检查一下这两个站点是否能正常访问。如果这两个不能正常访问,那么我们下面的都是。。。OK!能正常访问,接下来就是今天下一个主角Nginx登场的时候了。用法很简单,下面给出主要的配置,主要的模块是upstream,这个是Nginx的负载均衡模块,更多的细节可以去它的官网看一下。这里就不做详细的介绍,毕竟这些配置都十分的简单。 Nginx的配置也配好了,接下来就是启动我们的Nginx服务器,执行 /usr/local/nginx/sbin/nginx 即可,最后就是访问我们Nginx这个空壳站点http://192.168.198.128:8033(实际是访问我们在IIS上的那2个站点),然后就可以看看效果了,建议把浏览器的缓存禁用掉,不然轮询的效果可能会出不来。可以看到轮询的效果已经出来了,访问Linux下面的Nginx服务器,实际上是访问IIS上的site1和site2。我们是在站点2 设置了session,但是在站点2却得不到这个session值,而是在站点1才能得到这个值。这是因为我们用的算法是Nginx默认的轮询算法,也就是说它是一直这样循环访问我们的站点1和站点2,站点1->站点2 ->站点1->站点2....,演示是在站点2设置Session并提交,但它是提交到了站点1去执行,执行完成后Redirect到了站点2,所以会看到站点2上没有session的信息而站点1上面有。好了,警报提醒,Session丢失了,接下来我们就要想办法处理了这个常见并且棘手的问题了, 本文的处理方法是用Redis做一台单独的Session服务器,用这台服务器来统一管理我们的Session,当然这台Redis服务器会做相应的持久化配置以及主从或Cluster集群,毕竟没人能保证这台服务器不出故障。思路图如下: 思路有了,下面就是把思路用代码实现。在上面例子的基础上,添加一个RedisSession类,用于处理Session,让其继承ISession接口1 using Microsoft.AspNetCore.Http;2 using System;3 using System.Collections.Generic;4 using System.Threading.Tasks;56 namespace AutoCompleteDemo.Common7 {8 public class RedisSession : ISession9 {10 private IRedis _redis;11 public RedisSession(IRedis redis)12 {13 _redis = redis;14 }1516 public string Id17 {18 get19 {20 return Guid.NewGuid().ToString();21 }22 }2324 public bool IsAvailable25 {26 get27 {28 throw new NotImplementedException();29 }30 }3132 public IEnumerable<string> Keys33 {34 get35
谈谈使用Redis缓存时批量删除的几种实现
前言在使用缓存的时候,我们时不时会遇到这样一个需求,根据缓存键的规则去批量删除这些数据,比较常见的就是按前缀去删除。举个简单的例子,Redis中现在有几百个商品的数据,这些数据的key值是有一定规律的,都是以product:id的形式存在的。现在由于不得以为的原因要删除这几百个商品的数据,这个时候我们肯定就要把缓存键以product:开头的给全部删除掉。其实这个需求在Redis中是可以很容易去实现的。来看看几种常见的做法。常见的几种做法用Keys命令找到key之后执行删除操作用Scan命令找到key之后执行删除操作(2.8.0版本之后)添加缓存数据的时候,可以同时将key存放到一个SET中,然后依据这个SET来执行删除操作对于Keys命令,网上有不少血的教训,对于生产环境还是要谨慎谨慎再谨慎!能不用就别用。Scan命令的话是大部分人推荐的做法,是增量式迭代的一个命令。存到SET中就相对繁琐一点,而且额外占用了一部分内存。而且在进行删除的时候还要从这里读取出相应的key,同时也要移除这部分key的数据。下面来看看如何在.NET Core中来处理,主要还是针对SCAN的做法。示例操作Redis用的是StackExchange.Redis。使用IServer.Keys可能有人会有疑惑,不是说Keys命令尽量不要用吗?怎么你还用?这个还真的要解释一下!可能从方法上,我们找遍所有IServer和IDataBase接口都找不到纯粹的SCAN命令(SetScan,HashScan等除外)。但是如果看过里面的实现,你就会知道是为什么了!传送门:Keys可以看看下图高亮的两行代码:大致意思就是,如果你用的Redis的版本支持SCAN命令,走的就是SCAN,反之只能是KEYS了。下面定义一个查找RedisKey的方法。private static RedisKey[] SearchRedisKeys(IServer server, string pattern){var keys = server.Keys(pattern: pattern).ToArray();Console.WriteLine("Search Count-{0}",keys.Length);return keys;}知道那些Key要删除,剩下的就比较简单了!private static void KeysOrScanSolution(IServer server,IDatabase db, string pattern){db.KeyDelete(SearchRedisKeys(server, pattern));}使用IDatabase.ExecuteIServer.Keys可以说是隐式的调用了SCAN命令,那么我们自然也可以显式的去调用这个命令来完成这些。private static RedisKey[] SearchRedisKeys(IDatabase db,string pattern){var keys = new HashSet<RedisKey>();int nextCursor = 0;do{RedisResult redisResult = db.Execute("SCAN", nextCursor.ToString(), "MATCH", pattern, "COUNT", "1000");var innerResult = (RedisResult[])redisResult;nextCursor = int.Parse((string)innerResult[0]);List<RedisKey> resultLines = ((RedisKey[])innerResult[1]).ToList();keys.UnionWith(resultLines);}while (nextCursor != 0);return keys.ToArray();}删除的代码。private static void ExecuteSolution(IDatabase db, string pattern){db.KeyDelete(SearchRedisKeys(db, pattern));}当然还有一种做法是调用lua脚本去完成,这里就不细说了。总结虽然上面几种做法能比较简单的处理这个问题,但是在拿出这些Keys的时候,客户端的内存占用可能会比较大,尤其是有大量符合条件的缓存项的时候。涉及缓存的诸多操作(包含根据前缀去删除缓存项),我也在EasyCaching中实现了相应的操作,后面也会不断的抽时间来完善这一项目,有兴趣的朋友可以关注一下。文中的示例代码 RedisBatchRemoveSolution
浅谈Redis之慢查询日志
首先我们需要知道redis的慢查询日志有什么用?日常在使用redis的时候为什么要用慢查询日志?第一个问题:慢查询日志是为了记录执行时间超过给定时长的redis命令请求第二个问题:让使用者更好地监视和找出在业务中一些慢redis操作,找到更好的优化方法 在Redis中,关于慢查询有两个设置--慢查询最大超时时间和慢查询最大日志数。1. 可以通过修改配置文件或者直接在交互模式下输入以下命令来设置慢查询的时间限制,当超过这个时间,查询的记录就会加入到日志文件中。CONFIG  SET  slowlog-log-slower-than  num设置超过多少微妙的查询为慢查询,并且将这些慢查询加入到日志文件中,num的单位为毫秒,windows下redis的默认慢查询时10000微妙即10毫秒。2. 可以通过设置最大数量限制日志中保存的慢查询日志的数量,此设置在交互模式下的命令如下:CONFIG  SET  slowlog-max-len  num设置日志的最大数量,num无单位值,windows下redis默认慢查询日志的记录数量为128条。命令的解析:CONFIG 命令会使redis客户端自行去寻找redis的.conf 配置文件,找到对应的配置项进行修改。 以上的都是在交互模式下对redis进行配置,跟直接在.conf文件下修改配置行没有什么区别,都是可以实现以上的慢查询日志记录功能的,但是需要注意的是,在客户端的交互模式下输入CONFIG SET命令,只针对当前的会话来执行日志记录的设置,其他的会话(重新启动redis服务端),那么还是老样子,按照redis.conf文件的默认设置来执行?为什么会是这样的?因为redis是基于内存的,当一个退出一个客户端之后,所有的设置都会退回到默认版本。下次想设置慢查询日志配置,还是需要重新键入命令。那么在.conf文件下中修改配置呢?这种办法就相对一劳永逸了,因为每次服务端的启动都是以配置文件为基础的,所以slowlog日志会默认以.conf文件中的设置为标准。 即使这样,当做一些测试的时候,个人比较喜欢直接在交互模式下修改,交互模式下修改可以在当前的状态下和以后开启redis客户端(在服务端还没重启的条件下)都会执行慢查询日志的记录功能。而如果在.conf文件中修改配置项,那么需要重新启动redis服务器,来使这个功能生效,下次需要修改配置,还得到.conf文件来重新配置。 为更完整描述配置文件过程,我这里写一下如何在.conf文件中如何修改配置项windows操作系统下用记事本,linux操作系统下用sublim text 或者vim打开。找到对应下面就会找到配置选项了那么接下来,如何查看慢查询呢?又是进入交互模式下,命令很简单。SLOWLOG GET(当然也可以用小写,redis客户端对大小写没有太严格的限制)以windows为例查看记录如下为了方便解说,我设置超时时间为0毫秒,日志记录为1条那么记录的中的1)2)3)4)分别表示什么呢?1)表示日志唯一标识符uid2)命令执行时系统的时间戳3)命令执行的时长,以微妙来计算4)命令和命令的参数 做日志查询的时候,可以通过3)来查看是具体的命令运行时间(注意:再强调一次,时间的单位是微妙,但对于一个插入操作来说,10000微妙,也就是10毫秒即0.01秒已经可以算是慢操作了)哪些操作出了问题。当然这只限于测试使用,如果需要当业务出现redis插入查询缓慢的事件,需要去查看redis生成的持久型日志,这需要额外去配置一些内容,其中涉及到了集群和分布式,这里先点到为止。笔者希望自己往后有更深刻的认识的时候再来写一遍相关的文章。
Redis压缩列表
此篇文章是主要介绍Redis在数据存储方面的其中一种方式,压缩列表。本文会介绍1. 压缩列表(ziplist)的使用场景 2.如何达到节约内存的效果?3.压缩列表的存储格式 4. 连锁更新的问题  5. conf文件配置。在实践上的操作主要是对conf配置文件进行配置,具体上没有确切的一个值,更多是经验值。也有的项目会直接使用原本的默认值。此篇对于更好地理解一个数据库底层的存储逻辑会有一点帮助。修学储能,既要博,也要渊。希望这篇文章对同样也是在学习Redis的各位同伴有点用。 一、压缩列表(ziplist)的使用场景:Redis为了优化数据存储,节约内存,在列表、字典(哈希键)和有序集合的底层实现了使用压缩列表这一优化方案。例如,假如一个哈希键里面存储的字符串比较短,那么Redis就会将它用压缩列表的格式去存储,即转换为字节数组存储。而一个哈希键内部存储的整数值比较小,同样也会把它存储为压缩列表的一个节点。同理,列表键的对小数据的存储跟哈希键的操作类似。 如此说来,压缩列表并不是开发者可以直接调用的Redis中的一种存储数据结构,而是Redis中为优化数据存储而在底层做的一项努力。理解好这点还是比较重要的。 二、如何达到节约内存的效果?压缩列表是一种序列化的数据结构,这种数据结构的功能是将一系列数据与其编码信息存储在一块连续的内存区域,这块内存物理上是连续的。但逻辑上被分为多个组成部分,即节点。目的是为了在一定可控的时间复杂度条件下尽可能的减少不必要的内存开销,从而达到节省内存的效果。需要理解是怎么达到节约内存作用的,还需要去了解压缩列表的存储格式。 三、压缩列表的存储格式:压缩列表(ziplist)是Redis列表键、哈希键和有序集合键的底层实现之一,其实质是一种序列化的数据存储结构。有别于普通情况下,Redis用双端链表表示列表,使用散列表表示哈希键,用散列表+跳跃表表示有序集合。当一个列表或者哈希字典/有序集合只包含很少内容,并且每一个列表项或者哈希项/有序集合项如果是小整数,或者比较短的字符串。那么Redis就会用压缩列表来做底层的实现。 压缩列表由一系列经Redis特殊编码的连续内存块组成,每一个内存块称为一个节点(entry),而一个压缩列表可以包含很多个节点。每个节点存储的数据格式可以是字节数组(中文字符串等都会转换为字节数组)或者整数值。字节数组的长度可以是以下的其中一种:1. 长度小于等于63字节(2的6次方)2. 长度小于等于16383字节(2的14次方)3. 长度小于等于4294967295字节(2的32次方)整数值可能是以下六种中的其中一种:1. 4位长,介于0-12之间的无符号整数2. 1字节长的有符号整数3. 3字节长的有符号整数4. int16_t类型整数5. int32_类型整数6. int64_t类型整数 普通存储格式下和压缩列表存储格式下的不同点:列表存储结构典型的为双端链表,每一个值都是用一个节点来表示,每个节点都会有指向前一个节点和后一个节点的指针,以及指向节点包含的字符串值的指针。而字符串值又分为3个部分存储,第一部分存储字符串长度,第二部分存储字符串值中剩余可用的字节量,第三部分存储的则是字符串数据本身。所以一个节点往往都需要存储3个指针、2个记录字符串信息的整数、字符串本省和一个额外的字节。总体上额外的开销是很大的(21字节)。 压缩列表节点的格式:每一个节点都有previous_entry_length,encoding,content三个部分组成,在遍历压缩列表的时候是从后往前遍历的。1. previous_entry_length记录了前一个节点的长度,只要用当前指针减去这个值就可以达到前一个节点的起始地址。2. encoding记录了节点content属性所保存数据的类型和长度3. content记录了一个节点的值 显然压缩列表这种方式节约了不少存储空间。但同时也会引发下面的问题。 四、连锁更新的问题:一般而言如果前一个节点的整体长度小于254字节,previous_entry_length属性只需要1个字节的空间来保存这个长度值。而当前一个节点大于254字节的时候,previous_entry_length属性要用5个字节长的空间来记录长度值。当长度为254字节左右的节点前插入一个新的节点的时候,需要增加previous_entry_length来记录这个节点到新节点的偏移量。这个时候,这个节点的长度肯定就大于254字节了。所以这个节点的后一个节点就不能只用一个字节的previous_entry_length来记录这个节点的信息了,而是需要5个字节来记录。如果连续多个节点的长度都为254字节左右,在其中的某一个节点前/后发生节点的插入和删除(删除的推理与插入相反,原本用5字节记录前一节点的可能变为1字节),都可能引发连锁的更新,显然,这样对系统地运行效率是很不利的。不过,在实际应用中这种情况还是比较少发生的。    而双端链表在节点的更新、增加和删除上显得就会“轻松”很多了。 因为每一个节点存储的信息都是相对独立的。   实践意义:要预估一个节点大概占据多少字节的存储空间,适当地调整字段的存储格式而不要使存储的字段值占据存储空间落在254字节(除去encoding属性和previous_entry_length属性)左右。 Redis中查看字符串和哈希键值的长度相关命令:1. 查询字符串键对应的值长度命令:Strlen例如:127.0.0.1:6379> strlen m_name(integer) 8 2. 查询哈希键某一个域长度命令:Hstrlen例如:127.0.0.1:6379> hstrlen good_list good_list1(integer) 226  五、Conf文件配置:通过修改配置文件,可以控制是否使用压缩列表存储相关键的最大元素个数和最大元素的大小Conf文件中的配置:1.[] -max-ziplist-entries : 表示对于键的最大元素个数,即一个键中在该指定值下的数量的节点个数都会用压缩列表来储存[] -max-ziplist-value :表示压缩列表中每个节点的最大体积是多少字节实际使用中,一个列表键/哈希键的某一个元素往往存储着比较大的信息量,会大于64字节,所以配置时很有可能会比64大,同时考虑到实际存储数据的容量大小以及上面谈到的previous_entry_length的大小问题,对[] -max-ziplist-value进行合理的配置。 配置文件内容:############## ADVANCED CONFIG ##########################哈希键# Hashes are encoded using a memory efficient data structure when they have a# small number of entries, and the biggest entry does not exceed a given# threshold. These thresholds can be configured using the following directives.hash-max-ziplist-entries 512hash-max-ziplist-value 64有序集合键# Similarly to hashes and lists, sorted sets are also specially encoded in# order to save a lot of space. This encoding is only used when the length and# elements of a sorted set are below the following limits:zset-max-ziplist-entries 128zset-max-ziplist-value 64列表键,比较特殊,直接使用制定大小kb字节数表示(有些conf文件的列表键与hash键的表达式没太大区别)# Lists are also encoded in a special way to save a lot of space.# The number of entries allowed per internal list node can be specified# as a fixed maximum size or a maximum number of elements.# For a fixed maximum size, use -5 through -1, meaning:# -5: max size: 64 Kb <-- not recommended for normal workloads# -4: max size: 32 Kb <-- not recommended# -3: max size: 16 Kb <-- probably not recommended# -2: max size: 8 Kb <-- good# -1: max size: 4 Kb <-- good# Positive numbers mean store up to _exactly_ that number of elements# per list node.# The highest performing option is usually -2 (8 Kb size) or -1 (4 Kb size),# but if your use case is unique, adjust the settings as necessary.list-max-ziplist-size -2 案例:修改配置前使用默认配置:hash-max-ziplist-entries 512hash-max-ziplist-value 64 127.0.0.1:6379> hstrlen good_list good_list1(integer) 226127.0.0.1:6379> object encoding good_list"hashtable" 修改配置:hash-max-ziplist-entries 512hash-max-ziplist-value 254注意:修改配置后需要重启服务器127.0.0.1:6379> hstrlen good_list good_list1(integer) 226127.0.0.1:6379> object encoding good_list"ziplist" 可以看到存储方式已将变为ziplist 较官方的压力测试和指导建议:当一个压缩列表的元素数量上升到几千(实际使用可能远小于这个值)的时候,压缩列表的性能可能会下降,因为Redis在操作这种结构的时候,编解码会出现一定的压力。压缩列表的长度限制在500-2000之内,每个元素体积限制在128字节或以下,压缩列表的的性能都会处于合理范围之内。 参考资料:《redis设计与实现》 
引言
Redis 小白指南(二)- 聊聊五大类型:字符串、散列、列表、集合和有序集合引言开篇《Redis 小白指南(一)- 简介、安装、GUI 和 C# 驱动介绍》已经介绍了 Redis 的安装、GUI 和 C# 驱动等基本知识,这一篇主要是梳理一下 Redis 的 5 种类型的信息与指令。 目录字符串类型(String)散列类型(Hash)列表类型(List)集合类型(Set)有序集合类型(SortedSet)其它命令 字符串类型(String)1.介绍:字符串类型是 Redis 中最基本的数据类型,可以存储二进制数据、图片和 Json 的对象。字符串类型也是其他 4 种数据库类型的基础,其它数据类型可以说是从字符串类型中进行组织的,如:列表类型是以列表的形式组织字符串,集合类型是以集合的形式组织字符串。2.命令:【备注】包括 INCR 在内的所有 Redis 命令都是原子操作。 3.命令测试:图:简单的命令测试 4.命名:建议:“对象类型:对象ID:对象属性”命名一个键,如:“user:1:friends”存储 ID 为 1 的用户的的好友列表。对于多个单词则推荐使用 “.” 进行分隔。 5.应用:(1)访问量统计:每次访问博客和文章使用 INCR 命令进行递增;(2)将数据以二进制序列化的方式进行存储。散列类型(Hash)1.介绍:散列类型采用了字典结构(k-v)进行存储。散列类型适合存储对象。可以采用这样的命名方式:对象类别和 ID 构成键名,使用字段表示对象的属性,而字段值则存储属性值。如:存储 ID 为 2 的汽车对象。 2.命令: 3.命令测试:图:简单的命令测试 4.应用:(1)文章内容存储: 列表类型(List)1.介绍:列表类型(list)可以存储一个有序的字符串列表,常用的操作是向两端添加元素。列表类型内部是使用双向链表实现的,也就是说,获取越接近两端的元素速度越快,代价是通过索引访问元素比较慢。 2.命令: 3.命令测试:【解析】向列表的左边添加元素“1”,再依次加入“2”、“3”然后:在列表的右边依次加入两个元素“0”、“-1”: 4.应用:(1)显示社交网站的新鲜事、热门评论和新闻等;(2)当队列使用;(3)记录日志。 集合(Set)1.介绍:字符串的无序集合,不允许存在重复的成员。多个集合类型之间可以进行并集、交集和差集运算。 2.命令: 3.图解交、并、差集: 4.命令测试:5.应用:(1)文章标签。 有序集合(SortedSet)1.介绍:在集合类型的基础上添加了排序的功能。 2.命令: 3.命令测试:  4.应用:(1)点击量排序 其它命令1.获得符合规则的键名列表KEYS patternpattern 支持 glob 风格通配符: 2.判断一个键是否存在EXISTS key如果键存在则返回整数类型 1,否则返回 0 3.删除键DEL key [key ...]可以删除一个或者多个键,返回值是删除的键的个数 4.获得键值的数据类型TYPE key 这里只是进行了一些命令的整理,具体的使用很多时候还是需要自己进行到官方文档进行学习和搜索。 系列《Redis 小白指南(一)- 简介、安装、GUI 和 C# 驱动介绍》《Redis 小白指南(二)- 聊聊五大类型:字符串、散列、列表、集合和有序集合》《Redis 小白指南(三)- 事务、过期、消息通知、管道、优化内存空间》《Redis 小白指南(四)- 数据的持久化保存》  【博主】反骨仔【原文】http://www.cnblogs.com/liqingwen/p/6919308.html 【GitHub】https://github.com/liqingwen2015/Wen.Helpers/blob/master/Wen.Helpers.Common/Redis/RedisHelper.cs【参考】《Redis 入门指南》 
[Redis]Redis的数据类型
存储String字符串,使用get,set命令,一个键最大存储512M 存储Hash哈希,使用HMSET和HGETALL命令,参数:键,值例如:HMSET user:1 username taoshihan password taoshihanHGETALL user:1 存储List列表,可以重复,使用命令lpush和lrange,lpush的参数:键,值1,值2…例如:lpush infos taoshihan nanlrange的参数:键,开始索引,结束索引例如:lrange infos  0  -1 (-1是全部) 存储Set集合,不可以重复,使用命令sadd和smemberssadd的参数:键,值1,值2…例如:sadd users zhangsan li wangwusmembers的参数:键例如:smembers users 存储Zset有序集合,不可以重复,使用命令zadd和zrangebyscorezadd的参数:键,分数 值1 分数2 值2…例如:zadd members 1 zhangsan 2 li 3 wangwuzrangebyscore的参数:键,开始索引,结束索引例如:zrangebyscore users 0 1  知乎:redis的基本数据结构有哪些,都有什么应用? 李波:简单说明如下字符串(strings):存储整数(比如计数器)和字符串(废话。。),有些公司也用来存储json/pb等序列化数据,并不推荐,浪费内存哈希表(hashes):存储配置,对象(比如用户、商品),优点是可以存取部分key,对于经常变化的或者部分key要求atom操作的适合列表(lists):可以用来存最新用户动态,时间轴,优点是有序,缺点是元素可重复,不去重集合(sets):无序,唯一,对于要求严格唯一性的可以使用有序集合(sorted sets):集合的有序版,很好用,对于排名之类的复杂场景可以考虑 位图(bitmaps):这个不是新增的数据类型,只是可以把字符串类型按照单个位的形式进行操作,没有实际使用过。2016-03-03更新,网上很多人用bitmaps来做活跃用户统计和用户签到功能,性能比去数据库load高很多。计数器(hyperloglogs,翻译待定):如名字,添加元素只记录元素个数,并不会存储元素本身,节省空间并且避免重复count,这个感觉直接用incr就可以实现地理空间(geospatial indexes):用来做地理位置查询,比如两点之间的距离,一个点附近有多少元素,适合点比较固定的场景,或者只考虑当前位置的场景,像附近的人这种就不适合,一是需要考虑某段时间内的点,二是点经常更新,压力比较大
通过mysql自动同步redis
在服务端开发过程中,一般会使用MySQL等关系型数据库作为最终的存储引擎,Redis其实也可以作为一种键值对型的数据库,但在一些实际场景中,特别是关系型结构并不适合使用Redis直接作为数据库。这俩家伙简直可以用“男女搭配,干活不累”来形容,搭配起来使用才能事半功倍。本篇我们就这两者如何合理搭配以及他们之间数据如何进行同步展开。一般地,Redis可以用来作为MySQL的缓存层。为什么MySQL最好有缓存层呢?想象一下这样的场景:在一个多人在线的游戏里,排行榜、好友关系、队列等直接关系数据的情景下,如果直接和MySQL正面交手,大量的数据请求可能会让MySQL疲惫不堪,甚至过量的请求将会击穿数据库,导致整个数据服务中断,数据库性能的瓶颈将掣肘业务的开发;那么如果通过Redis来做数据缓存,将大大减小查询数据的压力。在这种架子里,当我们在业务层有数据查询需求时,先到Redis缓存中查询,如果查不到,再到MySQL数据库中查询,同时将查到的数据更新到Redis里;当我们在业务层有修改插入数据需求时,直接向MySQL发起请求,同时更新Redis缓存。在上面这种架子中,有一个关键点,就是MySQL的CRUD发生后自动地更新到Redis里,这需要通过MySQL UDF来实现。具体来说,我们把更新Redis的逻辑放到MySQL中去做,即定义一个触发器Trigger,监听CRUD这些操作,当操作发生后,调用对应的UDF函数,远程写回Redis,所以业务逻辑只需要负责更新MySQL就行了,剩下的交给MySQL UDF去完成。一. 什么是UDFUDF,是User Defined Function的缩写,用户定义函数。MySQL支持函数,也支持自定义的函数。UDF比存储方法有更高的执行效率,并且支持聚集函数。UDF定义了5个API:xxx_init()、xxx_deinit()、xxx()、xxx_add()、xxx_clear()。官方文档(http://dev.mysql.com/doc/refman/5.7/en/adding-udf.html)给出了这些API的说明。相关的结构体定义在mysql_com.h里,它又被mysql.h包含,使用时只需#include<mysql.h>即可。他们之间的关系和执行顺序可以以下图来表示:1. xxx()这是主函数,5个函数至少需要xxx(),对MySQL操作的结果在此返回。函数的声明如下:char *xxx(UDF_INIT *initid, UDF_ARGS *args, char *result, unsigned long *length, char *is_null, char *error);long long xxx(UDF_INIT *initid, UDF_ARGS *args, char *is_null, char *error);double xxx(UDF_INIT *initid, UDF_ARGS *args, char *is_null, char *error);SQL的类型和C/C++类型的映射:SQL TypeC/C++ TypeSTRINGchar *INTEGERlong longREALdouble2. xxx_init()xxx()主函数的初始化,如果定义了,则用来检查传入xxx()的参数数量、类型、分配内存空间等初始化操作。函数的声明如下:my_bool xxx_init(UDF_INIT *initid, UDF_ARGS *args, char *message);3. xxx_deinit()xxx()主函数的反初始化,如果定义了,则用来释放初始化时分配的内存空间。函数的声明如下:void xxx_deinit(UDF_INIT *initid);4. xxx_add()在聚合UDF中反复调用,将参数加入聚合参数中。函数的声明如下:void xxx_add(UDF_INIT *initid, UDF_ARGS *args, char *is_null,char *error);5. xxx_clear()在聚合UDF中反复调用,重置聚合参数,为下一行数据的操作做准备。函数的声明如下:void xxx_clear(UDF_INIT *initid, char *is_null, char *error);二. UDF函数的基本使用在此之前,需要先安装mysql的开发包:[root@localhost zhxilin]# yum install mysql-devel -y我们定义一个最简单的UDF主函数:1 /*simple.cpp*/2 #include <mysql.h>34 extern "C" long long simple_add(UDF_INIT *initid, UDF_ARGS *args, char *is_null, char *error)5 {6 int a = *((long long *)args->args[0]);7 int b = *((long long *)args->args[1]);8 return a + b;9 }1011 extern "C" my_bool simple_add_init(UDF_INIT *initid, UDF_ARGS *args, char *message)12 {13 return 0;14 }由于mysql提供的接口是C实现的,我们在C++中使用时需要添加:extern "C" { ... }接下来编译成动态库.so:[zhxilin@localhost mysql-redis-test]$ g++ -shared -fPIC -I /usr/include/mysql -o simple_add.so simple.cpp-shared 表示编译和链接时使用的是全局共享的类库; -fPIC编译器输出位置无关的目标代码,适用于动态库;-I /usr/include/mysql 指明包含的头文件mysql.h所在的位置。编译出simple_add.so后用root拷贝到/usr/lib64/mysql/plugin下:[root@localhost mysql-redis-test]# cp simple_add.so /usr/lib64/mysql/plugin/紧接着可以在MySQL中创建函数执行了。登录MySQL,创建关联函数:mysql> CREATE FUNCTION simple_add RETURNS INTEGER SONAME 'simple_add.so';Query OK, 0 rows affected (0.04 sec)测试UDF函数:mysql> select simple_add(10, 5);+-------------------+| simple_add(10, 5) |+-------------------+| 15 |+-------------------+1 row in set (0.00 sec)可以看到,UDF正确执行了加法。创建UDF函数的语法是CREATE FUNCTION xxx RETURNS [INTEGER/STRING/REAL] SONAME '[so name]';删除UDF函数的语法是 DROP FUNCTION simple_add;mysql> DROP FUNCTION simple_add;Query OK, 0 rows affected (0.03 sec)三. 在UDF中访问Redis跟上述做法一样,只需在UDF里调用Redis提供的接口函数。Redis官方给出了Redis C++ Client (https://github.com/mrpi/redis-cplusplus-client),封装了Redis的基本操作。源码是依赖boost,需要先安装boost:[root@localhost dev]# yum install boost boost-devel然后下载redis cpp client源码:[root@localhost dev]# git clone https://github.com/mrpi/redis-cplusplus-client使用时需要把redisclient.h、anet.h、fmacros.h、anet.c 这4个文件考到目录下,开始编写关于Redis的UDF。我们定义了redis_hset作为主函数,连接Redis并调用hset插入哈希表,redis_hset_init作为初始化,检查参数个数和类型。1 /* test.cpp */2 #include <stdio.h>3 #include <mysql.h>4 #include "redisclient.h"5 using namespace boost;6 using namespace std;78 static redis::client *m_client = NULL;910 extern "C" char *redis_hset(UDF_INIT *initid, UDF_ARGS *args, char *result, unsigned long *length, char *is_null, char *error) {11 try {12 // 连接Redis13 if(NULL == m_client) {14 const char* c_host = getenv("REDIS_HOST");15 string host = "127.0.0.1";16 if(c_host) {17 host = c_host;18 }19 m_client = new redis::client(host);20 }2122 if(!(args->args && args->args[0] && args->args[1] && args->args[2])) {23 *is_null = 1;24 return result;25 }2627 // 调用hset插入一个哈希表28 if(m_client->hset(args->args[0], args->args[1], args->args[2])) {29 return result;30 } else {31 *error = 1;32 return result;33 }34 } catch (const redis::redis_error& e) {35 return result;36 }37 }3839 extern "C" my_bool redis_hset_init(UDF_INIT *initid, UDF_ARGS *args, char *message) {40 if (3 != args->arg_count) {41 // hset(key, field, value) 需要三个参数42 strncpy(message, "Please input 3 args for: hset('key', &