Redis4.0新特性

在12月初,redis发布了4.0-rc1。其中做了很多新特性的介绍。本文就来简单说下有哪些东西。

不过在没过几天之后又发布了redis 4.0-rc2,做了很多紧急修复。不过本来也不是正式版,问题很多也很正常。大家可以自己下载来玩玩,不要用于生产环境。

新特性如下:

模块系统

这个可以说是redis4.0最大的一个变化了。它可以让用户自己编写redis代码去扩展redis功能。可以实现自己的数据结构和相关的功能接口。

作者自己就通过这个模块系统做了神经网络数据模型。也有很多人通过这个系统开发了限速系统、索引系统等。

这个功能给redis带来了无限的可能性,让redis不只是单纯的k-v存储了。

其实优酷有个团队自己本身也有对redis进行封装,也就是做了这样的事情。现在想想,redis本身提供了支持,提供了高层api,这样的实现性能等方面一定会有很大提升。

改进主从复制PSYNC2.0

在之前redis的复制,一种是全量复制,也就是在redis从服务器宕机之后,重连需要全量复制,性能很差。

后来引入了PSYNC进行部分赋值。主从会维护一个偏移量,当从宕机之后,再启动的时候可以进行部分复制就好了。

4.0 中对PSYNC进行了改进,引入了 tag 标签,对于每次复制,都由 标签+偏移量 来定义,并存储在RDB文件中,这样,各个slave中都记录了标签和偏移量,相当于互相之间都认识了,当某一个slave变为master之后,还可以通过 标签+偏移量 来使用 PSYNC 进行部分重新复制。

缓存回收的优化

redis 4.0 对现有的回收策略进行了优化,使其更加健壮、快速、精准。

同时还引入了新的回收策略LFU(Least Frequently Used),对最不常用的缓存数据进行清理。

非阻塞删除UNLINK

之前删除键,使用DEL命令,当键比较大的时候,性能会比较差,而且是单线程阻塞,导致其他线程的执行可能超时。redis4.0新增了UNLINK命令来异步删除,先删除键的引用,然后后台启用新的线程去删除键。

同样,FLUSHALL和FLUSHDB也增加了ASYNC参数去异步操作。

新的内存监控命令

MEMORY有更多的参数,可以监控内存数据。

查看某个key的内存使用、查看内存使用细节、申请释放内存、深入查看内存分配的内部状态。

具体可以通过MEMORY HELP查看。

Redis 集群对 NAT / Docker 的支持

这个可以在redis.conf文件中配置,里面也有详细的介绍。

优化

redis现在可以是用更少的内存存储以往相同的数据。

另外还有很多小的优化和特性,具体看这里

模块系统应该是这个版本最大的特性了,当然,由于刚出来,问题也很多。所以再次提醒大家玩玩就好。

本文原创于赵伊凡BLOG转载注明出处。

秒杀系统实现分析

秒杀的特点

再讲怎么实现之前先说说秒杀有什么特点。

秒杀的场景比较特别,一般都会在一些集中的时间点对某些商品进行抢购。

我们先来考虑下常规的购买流程,浏览商品——加入购物车——下单——减库存——支付——支付完成。而多数人的操作其实都是浏览商品,加入购物车,下单购买的占少数。而秒杀的场景,其实大家已经省略掉了前面的操作,直接选择好了商品,就要直接下单、支付了。而且买的还都是同一个商品。这块问题就来了,非常多的人全都来操作这一部分集中的业务数据,对系统的冲击之大可想而知。

比如一般我们来处理库存的方式,需要对数据库加行级锁,然后减库存(同时需要确认库存数量,不能超售),然后释放。一般情况还好,因为大家都在购买不同的商品,所以锁加在不同的记录上面。但是在秒杀场景中,锁需要加载同一个商品上面,当大量请求过来,所有的请求都在等待给这个记录加锁,结果会多惨可想而知。

秒杀的优化

请求尽量拦截在系统上游

从客户端到负载服务,再到业务服务,再到数据库,我们要尽量能把请求拦截到前面就不要往后放。

总体来说的思路有限流、消峰、排队等。

客户端层

记得之前红包也发了个新年版的微信,就是把很多静态资源提前缓存到了客户端,到时候没有的请求,有的就不用请求了。优化了高峰秒杀的服务压力。另外对于商品秒杀,也可以把页面缓存,一些不关键业务静态化,比如商品评价、相关推荐等,减少服务器压力。

记得去年前年的春晚微信红包,摇一摇有机会抽到红包。但是大家都在摇,每个人摇都给你去系统抽一下,系统压力好大,如果你摇三次给你发一次请求呢,是不是系统的压力下降到了1/3?

再举个例子,12306,大家刷票,为什么平时使用的时候一点事没有,而春节、国庆抢票就很难。其实人家已经优化的很好了,但是还是会有很大压力,为什么,就是大家不停的在刷新。想想是不是春节的时候一直在刷票,不管是手动还是软件刷,人家一直查压力很大啊。于是页面上显示几秒才能重新查询一次(但是这样也限制不了外挂党),这样有效降低了服务器压力。

其实很多地方的查询功能也是类似,点了查询,会有个倒计时,几秒之后才能查,按钮置灰。这样可以防止不停的发送请求。尤其是12306这种,查询结果出的稍微一慢,用户还会觉得没查出来,就会不停的点击增加服务器负担。

上面举的两个例子,都是拦截到了客户端(应用、浏览器)层面,但是如果有人用外挂,或者直接写脚本发请求呢(想起月饼时事件)?正常用户也会有很多穿透客户端的,下面就需要在nginx这类服务器层面去做拦截了。(也可能lvs)

负载服务层

对于恶意用户,假如我们客户端限制的是3秒能发一次请求,我们在负载服务一层,限制超过2秒一次的请求,把这个uid给他封锁一定时间,对于短期秒杀场景直接全站封禁也可以。做的松一点就是3秒只放过1个请求,而不封锁帐号。

(这里有个小tip给大家,出现这种异常的情况,我们可以认为就是非法用户了,直接封了也无所谓,但是文案的提示尽量不要说你的帐号被封了什么的,给他一个和一般用户一样的没抢到什么的提示最好。不然写脚本的人会一点点摸索你的频率,伪装成正常用户。)

如果不考虑封锁帐号,也可以增加负载层面的缓存,对于一定时间内的请求,都返回第一次请求的结果,而不去请求后端服务数据。

在大量请求时为了让缓存的结果或者计数的结果尽量一致,可以按UID来映射到具体服务,保证请求得到的结果一致。(对于内存计数,希望同一UID落到一个服务上,缓存结果也是如此)

这里另说一句,千万不要觉得,我们一个nginx负载多少,到时候真出现了大量秒杀的情况,预估多少量,除以目前单nginx负载,就是我们需要的nginx数量。对于单机部署多个nginx或者多nginx协调的情况,都是可能影响性能的,平均响应时间也会变长,需要实际测试出拐点才是正确的做法。

业务服务层

对于秒杀,我举个我们这里实际遇到的例子。我们有红包的业务,对于抢红包的用户来说,就是个秒杀场景。比如发一次红包,可以20个人抢,那么透过数据库的就没有必要太多,一般会做多几个的冗余量(比如5个),这样其他的请求就没必要去与数据库交互了,直接快速返回没抢到就好了。透过去的这25个再进行数据库的行级锁进行减少库存等相关操作。对于上百万千万的请求来说,这25个请求就并不是很多了,数据库也可以轻松抗住。这里还要注意,要使用乐观锁减库存,进一步减少超卖可能。

update xxx set num = num -1 where where id = 1 and num > 0;

加锁的时候检查其实会更好,多出来的5个就不会处理了。

为什么要多放一些呢,假如在进行减库存等相关操作出错了,不要重放,直接抛弃让后面的补上就好了。重放还是复杂了些,同时也符合fail fast规则。

这里具体的实现技术,使用缓存、队列都可以完成,后面说缓存,队列的话不管是用哪种中间件,比如mq、redis队列,都可以轻松搞定。当然对于关键大型的抢购业务,也可以放很多,比如1w商品,放2w入队列。不可能一半都失败吧。

业务上其实也可以配合一些优化,比如很多东西并不需要精细化的显示,就可以粗劣的显示一下,比如红包的剩余个数,可以只显示还有没有,而没必要显示还剩几个(当然也可以显示,但是业务上接受具体数值显示不准的情况)。

数据库

到数据库这一次的压力其实已经很小了,只需要保证数据的准确性就可以了。

对于大量商品同时秒杀的情况,可以按照商品id进行分表或者分库,提升数据库的性能。

多用缓存

在秒杀优化中,一定要充分的利用缓存这一工具。推荐的缓存工具使用redis。

比如在负载服务的时候,可以使用redis作为通用计数器。业务服务的地方,使用redis的队列,或者配合队列做计数器,把超过25个的请求全部快速返回没有抢到。

另外对于读的业务,尽量多的使用缓存,上面提到过,如果是商品页的话,完全可以把整个页面的内容都缓存起来,这里推荐页面的缓存可以在nginx上做。

对于缓存服务挂了的情况,重启一定要记得预热,不过在高并发的秒杀业务下,我们基本上是不允许服务挂了的情况,所以一定要做好服务保护,当过载过高的时候可以抛弃请求,不要让服务挂了。

异步处理

对于订单生成、物流状态等信息,完全可以交给队列后面的服务去处理,并不需要实时处理。把一些不着急的业务逻辑放到异步队列后面处理也是优化的核心。

尽量让http请求能够快速返回。我们的抢红包业务,由于服务与客户端维持了长连接,所以都快速返回了,然后抢到的和一小部分多放过去的量,是通过异步返回的结果,其他的请求都是http快速返回没抢到的结果。

用户体验

秒杀优化的过程中,不能丢失了正常用户的体验。

可以异步化处理一些业务,比如我们上面提到的抢红包业务,虽然抢到的结果是异步发送的,但是速度也不会太久,用户体验就不会太差。

对于商品的抢购,如果具体结果异步处理,可以先来个中间页面,提示用户的抢购结果,或者提示个抢购中。这块最早小米就做了,但是总是抢购中,然后没抢到,大家心里也不是很开心。但是总比啥都没有卡在那好。

最后说点别的

对于一般的企业,如果预估秒杀会比较激烈的话,一定要提到带宽,如果是云服务的话,建议新弄一组机器来做这块的业务,降低对正常业务的影响。

多做压测总是好的。

可以针对秒杀场景做些简化需求,没必要让他和一般商品销售需求一致,尽可能的砍掉更多的不必要功能,保证服务正常运行。

特殊情况可以增加验证机制,比如苹果使用了最烦人的,需要下单用户给苹果发短信,然后获取短信验证码,才能预定店里拿手机。不排除会被刷,但是谁叫人家厉害呢,你不买别人买。但是我们的验证服务尽量还是不要影响到用户的下单流程。

多做提前需求准备,比如建议用户提前把东西加入购物车或者增加一键购这样的功能。建议用户提前选择好收货地址等等。

做倒计时,秒杀一般都是从某一时间点准时开始的,做个倒计时,可以方式用户总是刷新看开枪没,一直重新访问页面也很消耗服务器性能呀。

很多网站初期对于用户的注册还不是很在意,结果出了很多僵尸用户,这些帐号在秒杀的时候被某些工作室用来刷接口。这个建议想办法把这部分用户区别出来。然后只要是这部分用户想要秒杀,需要验证手机号等复杂操作才能进行,防止工作室僵尸用户刷接口秒杀。

本文原创于赵伊凡BLOG转载请注明出处,超过3000字全原创手敲实在不容易。

到此秒杀就说的差不多了,当然秒杀还会涉及到很多东西,这也需要开发人员一步一步的成长,再去提升自己系统的稳定性。数据库扩容,分库、更多的缓存等优化都是需要慢慢进行的。

spring-data-redis的一个缺陷导致redis报错(精简版)

原文在这里

为什么要再写个精简版呢,一个是原来的文章偷懒排版太差,另一个是原来的文章代码贴得太多怕大家看的头疼。有同事也遇到这个问题了,搜到了我的文章,表示太长,不想看,好吧,下面是精简版。

本篇只简述问题、原理、解决方案,要看详细心路历程就去看原来的那篇文章吧。

问题

之前用spring-data-redis,调用redis的expire方法的时候,出现了很奇怪的错误。

jedis.exceptions.JedisConnectionException: Unknown reply: 3
org.springframework.data.redis.RedisConnectionFailureException: Unknown reply: 3; nested exception is redis.clients.jedis.exceptions.JedisConnectionException: Unknown reply: 3

就类似的可能这个3是别的什么奇怪的字符都有可能。

原理

spring在做expire处理的时候,不管你用的单位是什么,他最后都会处理成毫秒传给redis,但是这里用的类型是int,是不是很烦,int最大值是2147483647,算一下支持多少天,24.8天。这里就知道原因了,只需要把失效时间设置为小于24.8天就OK了。另一种方案就是别用spring-data-redis去处理。

本文原创于赵伊凡BLOG

使用七牛云为wordpress站点免费加速

如果你比较关注本站的话,可以发现本站的速度相比以前有了一些提升,其实还是挺慢,主要wordpress里面有一些统计的东西需要访问美国的网站,导致加速速度没有办法特别快。但是相比以前好了很多。

这里的一个原因就是由于本站现在所有的静态都放到了七牛云上。

前端的同学可以谷歌浏览器F12看到,我这里加载的很多图片地址的域名,是ocv8什么什么开头的的一串域名,这个就是七牛提供的一个域名。

先说说为什么选择七牛,这里也不是打广告,我也没收钱,就是因为七牛提供长期的免费额度。10GB的免费存储空间,每月国内10GB的免费下载流量,10GB的免费海外下载流量,100万次免费GET请求(调用七牛API),10万次免费PUT请求(调用七牛API)。这免费额度已经算是相当高了,对于我们普通个人站来说,绝对够用。

这里我们只是为wordpress加速的话,主要需要关注国内下载流量、PUT次数,如果站外多的话,还可以关心下海外的下载流量。如果流量不够的话,可以付费购买,用多少扣多少,如果流量真能用到那么多,应该也不会在意这点小钱了吧,其实很便宜。

接下来说下怎么用。很简单,跟着下面步骤走,都能会。

①首先去七牛注册一个帐号,然后登录上去。进入开发中平台。
②选择添加资源(七牛可能会改版,但是内容应该还是一样的),找到七牛官方资源的,“对象存储”,然后点击立即添加按钮。
③进去新的页面之后,有个资源名要填,随便按要求填你的资源名就好,存储地区按照喜好选个吧,我在北京所以选了个华北的。访问控制选择公开。接下来确认创建即可。
④这时候在资源列表里就能看到你的资源了。在测试域名一栏能看到你的域名,把这个记下来。包括空间容量、API调用次数等。要看具体统计可以点击“数据统计”按钮。
⑤内容管理里面是你所有的文件,目前肯定还没有。然后点返回之后一个“镜像存储”的tab,点击进入。这里有个镜像源,填写你站点的域名,需要加http,然后下面勾选使用默认robots.txt文件(这个是为了不让搜索引擎收录图片,对你的站点SEO有帮助),点击保存设置即可。剩下的就不用管了,接下来我们去wordpress后台操作。
⑥在wordpress插件里搜索WP Super Cache,这里推荐这个插件而不是七牛推荐的插件,这个会比较简单些。搜索出来后安装、启用。
⑦接下来去WP Super Cache的设置页面进行配置。在设置页面点击CDN标签,勾选开启CDN支持,并在Off-site URL一栏填写刚刚七牛给我们的测试域名,然后包含目录填写“wp-content,wp-includes”,应该默认就是填好的。后面的不用管,我们最后再说。填完直接点击保存修改就好。这时候我们的CDN就已经弄好啦。
⑧第一次打开页面会感觉有点慢,这是因为第一次七牛需要去你网站同步静态数据,在第二次再访问这些页面的时候就好了。

到这里,我们就已经完成七牛CDN的免费配置了。可能过两天七牛会给你打电话,你就说个人站,用不了多少流量什么的,就完事了,他就是个回访,很友好。

最后我们再说说,怎么把这个该死的超长域名替换掉。想要替换必须得是在中国备案了的域名,如果没有备案,不好意思,替换不来,所以我的也就没有替换。其实没啥关系。

想要使用自己的域名,也很简单,在七牛添加一个“融合 CDN 加速”的资源,直接填写你的加速的普通域名就可以了。接下来,你需要把你的这个静态域名的CNAME记录指向七牛给你的域名下,这个七牛都有详细的引导。最后再去你wordpress里的插件设置那,CDN一栏,附加 CNAME 记录填写你自己的域名就可以了。这个会覆盖上面的域名配置。

到此我们就可以成功使用七牛CDN为我们的站点加速了。看了下速度,都在个位毫秒以内,速度很快。另外需要提醒一下大家的是,有时候由于修改了哪里的配置,或者安装了什么插件、升级什么的情况,可能导致域名又用回我们自己的站点了,这时候也可以到插件的设置页面,通用一栏,缓存功能还是启用,重新点击一下更新按钮就可以了。

本文原创于赵伊凡BLOG转载请注明出处。

现在我们就可以愉快的使用七牛CDN啦。不紧加速,还能省流量,多好~

Redis基数统计——HyperLogLog小内存大用处

我们一直都知道,redis几大常用数据结构,字符串、散列、列表、集合、有序集合。其实后来Redis做了很多补充,其中之一就是HyperLogLog,另外的还有GEO(地理位置),是3.2版本加的。

这里我们就来简单介绍下HyperLogLog结构。

先说用处:这个结构可以非常省内存的去统计各种计数,比如注册ip数、每日访问IP数、页面实时UV(PV肯定字符串就搞定了)、在线用户数等。

这里看到所有的用处都是xxx数,所以这个数据结构的特点就是,可以比较准确的估算出你要统计的数量,但是却无法知道统计的详细内容。比如统计每日访问IP数,可以获取当时访问过的IP总数量,但是没法知道这些IP都是什么。

有得必有失,当然你要统计上面提到的那些内容,可以用集合来处理,这样可以知道数量,也能获得所有的详细列表。但是一个大型的网站,每天IP比如有100万个呢,我们粗算一个IP消耗15字节,那么100万个IP就是15M,如果1千万,就是150M。

再来看看我们的HyperLogLog,在Redis中每个键占用的内容都是12K,理论存储近似接近2^64个值,不管存储的内容是什么。12K,知道这个数据结构的作用了吧。这也是为什么他不能知道里面的详细内容了。这是一个基于基数估算的算法,只能比较准确的估算出基数,可以使用少量固定的内存去存储并识别集合中的唯一元素。而且这个估算的基数并不一定准确,是一个带有 0.81% 标准错误(standard error)的近似值。

这里当你记录的内容越多,和集合使用的内容就越容易产生鲜明的对比,因为HyperLogLog结构,在范围允许的情况下无论多少值,都置灰占用12K内存。

这样比如我们把每日IP记录下来,假设每天有一亿个IP访问,如果使用集合的话,一天的内存使用就是1.5G,假设我们存储一个月的记录,就需要45G容量。但是使用HyperLogLog的话,一天12K,一个月360K。如果我们不需要知道IP具体信息的话,完全可以把这些记录留在内存一年、或者不删都行。如果需要,我们也会把所有的IP访问记录通过其他途径存储起来。把每天的信息存储起来,我们可以计算每月IP总数(MERGE),一年的IP总数等(去重)。

下面介绍一下HyperLogLog的命令,其实他和集合的命令比较像,只是命令少,不能获取列表而已。另外这个数据结构需要2.8.9及以上的版本才能使用哦~

PFADD

在执行这个命令之后,HyperLogLog内部的结构会被更新,并有所反馈,如果执行完之后HyperLogLog内部的基数估算发生了变化,那么就会返回1,否则(认为已经存在)就返回0。
这个命令还有一个比较神器的就是可以只有键,没有值,这样的意思就是只是创建空的键,不放值。
如果这个键存在,不做任何事情,返回0;不存在的话就创建,并返回1。

这个命令的时间复杂度为O(1),所以就放心用吧~

命令例子:

redis> PFADD  ip:20160929  "1.1.1.1"  "2.2.2.2"  "3.3.3.3"
(integer) 1
redis> PFADD  ip:20160929 "2.2.2.2"  "4.4.4.4"  "5.5.5.5"  # 存在就只加新的
(integer) 1
redis> PFCOUNT  ip:20160929  # 元素估计数量没有变化
(integer) 5
redis> PFADD  ip:20160929 "2.2.2.2"  # 存在就不会增加
(integer) 0

其实我们发现在少的时候还是挺准的,哈哈。

PFCOUNT

其实在上面的学习中我们已经用过这个了,这里再来介绍下。

当命令作用于单个键的时候,返回这个键的基数估算值。如果键不存在,则返回0。
当作用于多个键的时候,返回这些键的并集估算值。类似于把这些键都合并了之后,在调用这个命令输出。

这个命令在作用于单个值的时候,时间复杂度为O(1),并且具有非常低的平均常数时间;在作用于N个值的时候,时间复杂度为O(N),这个命令的常数复杂度会比较低些。

命令例子:

redis> PFADD  ip:20160929  "1.1.1.1"  "2.2.2.2"  "3.3.3.3"
(integer) 1
redis> PFCOUNT  ip:20160929
(integer) 3
redis> PFADD  ip:20160928  "1.1.1.1"  "4.4.4.4"  "5.5.5.5"
(integer) 1
redis> PFCOUNT  ip:20160928  ip:20160929
(integer) 5

PFMERGE

合并(merge)多个HyperLogLog为一个HyperLogLog。其实这个也很好理解,而合并后的估算基数也近似于所有HyperLogLog估算基数的并集。

这个命令的第一个参数为目标键,剩下的参数为要合并的HyperLogLog。命令执行时,如果目标键不存在,则创建后再执行合并。

这个命令的时间复杂度为O(N),其中N为要合并的HyperLogLog的个数。不过这个命令的常数时间复杂度比较高。

命令例子:

redis> PFADD  ip:20160929  "1.1.1.1"  "2.2.2.2"  "3.3.3.3"
(integer) 1
redis> PFADD  ip:20160928  "1.1.1.1"  "4.4.4.4"  "5.5.5.5"
(integer) 1
redis> PFMERGE ip:201609   ip:20160928   ip:20160929
OK
redis> PFCOUNT  ip:201609
(integer) 5

本文原创与赵伊凡BLOG转载注明出处。

到此HyperLogLog所有的命令就都介绍完了,没错,目前就只有这三个。其实也很简单的,知道了这个结构的用法,也就知道什么时候适合用了,对我们非常珍贵的内存还是很有帮助。

Redis学习笔记(十一)——Redis持久化

redis通常被我们用作缓存,而很多场合我们也只把他作为缓存使用。关于Redis的各种用法,我前面也已经介绍了一遍,有兴趣的可以点回去看下。

很多时候,我们的一些数据没有做比如Mysql的持久化,就是想要全部存到Redis里面,这时候Redis也提供了相应的持久化支持。

Redis支持的持久化方式

定时快照方式(snapshot)
基于命令追加方式(AOF)
虚拟内存(vm)
Diskstore方式

快照方式

快照是默认的持久化方式,其通过配置文件中的save参数来配置,支持多种配置方式组合。

save 600 10
save 60 5

上述配置中,当600秒有10个key被修改了,或者60秒内有5个key被修改了,都会触发快照。

快照保存方式为操作系统fork出主进程的一个子进程,父进程继续处理client请求,子进程读取全部数据写入到临时文件中,由于os的copy on write机制,父子进程会共享相同的物理页面,当父进程处理写请求时os会为父进程要修改的内存页创建副本,而不是写共享的页。所以子进程的地址空间内的数据是fork时整个数据库的一个快照。

快照方式的缺点:

非增量写文件,所以每次都会重新读取所有内存数据写文件,当数据很大的时候,虽然因为有两个进程不会导致子进程影响主进程,但是这也会导致大量的磁盘IO,导致系统性能下降。
快照方式是定时快照,所以当服务down掉的时候,恢复起来不一定是最新的数据。一般这种应用用于可以接受不一致数据的情况。

AOF方式

本身Redis是不打算做AOF方式的,但是应好多网友要求,官方还是出了AOF的方式持久化。AOF全程是Append-only file,这是一种实时持久化的方式。

配置为appendonly yes

Redis当收到命令之后,会调用write函数把命令写道文件中(默认是appendonly.aof文件)。当数据库重启之后,其会读取文件并把内容全部执行一遍来重建数据。不过由于操作系统的内核会缓存write的修改,正常情况下不会立即写道文件中,所以我们重启之后还是会有一定的数据丢失。不过我们可以通过配置appendfsync always来让操作系统调用fsync函数强制操作系统不缓存,直接写文件。但是这样性能可能会比较差。

另外还提供appendfsync everysec配置,提供每秒持久化。另外当参数为no的时候就完全依赖操作系统了,这个就不好说了。

AOF方式同样也是使用操作系统的fork,主进程继续处理client请求,子进程在持久化的时候把内存的命令写入一个新文件中,同时主进程接收到的其他命令缓存起来在子进程完成之后把缓存的命令也写入进去。然后通过重命名替换老文件。

问题:

这里有个问题就是,对一个key进行inc命令100次,这个日志文件就会记录100个命令。实际上这个数据我们在恢复的时候并不需要那么多命令的。这还会导致aof文件非常大。
另外就是在重启恢复的时候也会比较慢,因为是记录的命令,而不是数据。在恢复的时候需要把aof文件中的数据全部执行一遍,就像上面说的,inc100次,需要执行100次命令,这样会非常慢。有时候几十G的数据需要恢复好几个小时。

虚拟内存方式

这种其实已经废弃了。

主要原因就是重启慢、纪录慢、程序也很复杂。

作者博客这里

这个大家知道有这个方式就好了,我们也不会去使用。

diskstore方式

这种方式也是一个实验性方式。使用的是B-Tree(未来可能使用B+-Tree实现)。

总结

使用Redis尽量不要使用持久化,如果非要使用的话,优先使用快照,其次是AOF。

fork进程这点大家了解一下会知道实际上,当操作系统调用fork函数创建子进程之后,操作系统会给子进程和主进程一样的存储数据和空间。所以当持久化的时候,实际上内存基本上来说是会翻倍的。所以常规建议是redis的内存使用尽量保持在系统物理内存的3/5以内。

本文原创于赵伊凡BLOG

高性能mysql读书笔记4(查询性能优化1)

优化mysql,除了需要最优的库表设计、很好的索引设计以外,还需要编写合理的查询。如果查询写的比较烂,索引有可能根本就用不上。那样不管库表、索引设计的再好也没用。

下面会介绍如何写出高效的查询语句,以及明白高效与低效的原因。

为什么查询速度会慢

查询需要快速,主要是要响应时间快。

查询需要消耗的时间如下:网络、CPU计算、生成统计信息和执行计划、锁等待(互斥等待)等,尤其是向底层存储引擎检索数据的时候的这些调用。这些调用需要在内存操作、CPU操作和内存不足时导致的IO操作上消耗时间。

有些操作我们做了一些不必要的额外操作、甚至重复调用。所以我们需要优化这些查询,去优化和消除这些操作花费的时间。

慢查询基础:优化数据访问

查询性能低的最主要原因就是访问的数据过多。有时候我们可能并不总是需要那么多的数据,但是我们仍然去访问那么多的数据。

通常我们从两方面去分析会比较有效:

1、确认应用程序是否在检索大量超过需要的数据。通常是访问了太多的行,也有可能是访问了太多的列。
2、确认Mysql是否在分析大量超过需要分析的数据。

是否向数据库请求了不需要的数据

有些查询会请求超过需要的数据,然后这些多余的数据会被应用程序丢弃。这会给Mysql服务器带来额外的负担,并增加网络开销,另外也会消耗应用服务器的CPU和内存资源。

下面是一些常见的错误:

查询不需要的记录

使用ResultSet返回结果集的时候,我们可能只需要前10条,一些人会天真的以为应用程序是一个循环,拿一条数据,我们关闭结果集,就不会继续查询了。实际上Mysql不会这样,而会全部查出,然后存在内存里。这种情况我们最好使用LIMIT来截取数据。

多表关联时返回全部列

join时最好只取需要的列,减少子查询内容(有时更可以让子查询使用覆盖查询)。

总是取出全部的列

使用select *进行查询,当我们看到这样的语句的时候,总要用怀疑的眼光看看是否真的需要所有的列,这样会导致查询无法使用索引覆盖查询。不过很多人觉得这样可以简化开发,同时这样写其实有时候可以使用到查询缓存。

多次查询相同的数据

有时候一个接口需要几处相同的数据,我们尽可能的把一样的数据通过一次查询获取出来。不要一遍遍的查询,耗费性能。

Mysql是否在扫描额外的记录

在确定只返回了需要的记录之后,我们需要在确定下是否只扫描了需要的记录。衡量Mysql查询开销最简单的三个准则就是:

响应时间
扫描行数
返回行数

这三个指标都会记录到Mysql的慢日志中,我们主要查看是否扫描了过多的行这点最为重要。

响应时间

响应时间其实只是表面上的值。

响应时间=服务时间+排队时间。

一般比较常见的排队等待时间是消耗在了I/O及锁上。一般这个并不是很好分析时间是否在一个合理的范围,多数是凭经验去判断这样一个查询,它的响应时间是否在一个合理的值。

扫描的行数和返回的行数

理想情况下,扫描的行数和返回的行数应该是相同的,但是理想的情况往往很少,尤其是在做关联查询的时候。但是我们也应该尽可能的减少扫描的行数。

扫描行数和访问类型

Mysql有好几种方式可以查询一条数据,有些方式需要扫描好多行才能获得一条数据,有些方式不需要扫描就能获取数据。

在EXPLAIN语句返回的type列反映了返回类型。访问类型有很多种:从全表扫描到索引扫描、范围扫描、唯一索引扫描、常数引用等。这里列的是速度从慢到快,扫描的行数也少从多到少。

如果查询没有办法找到合适的访问类型,那么最好的办法就是为之建立一个合适的索引。

比如select * from xxx where id=1

这种查询,当id是主键索引的时候,我们使用EXPLAIN的时候发现,type是ref,其另一个参数值rows为1也说明了这个查询只需要访问一条数据。如果没有索引呢,这时候type会是ALL,同时rows一般会比较大,约等数据条数,另外这里的Extra参数会显示为Using where,标识Mysql是通过where条件来确定数据行的。

一般Mysql通过三种方式应用where条件:

在索引中使用where条件过滤不匹配的记录,这是在存储引擎层做的。
使用覆盖索引返回记录(Extra会提示为Using index),直接从索引过滤并返回需要的记录。
从数据表返回数据然后过滤数据(Extra会提示为Using where)。

所以说创建好的索引可以大大优化我们的查询性能,但也并不总是可以优化,有时候确实是只能访问很多数据而没有什么好的索引能够帮忙。

一般我们如果发现,一个查询扫描的行比返回的行多很多,我们通常可以采用下面的方法去解决:

使用索引覆盖扫描。
改变库表结构,比如使用单独的汇总表。
重写查询语句,让Mysql能够更好的优化这个查询(后面会说)。

本文原创于赵伊凡BLOG

找回密码功能设计的思考

最近在看我们系统的注册登录、账户相关的内容设计。看到了找回密码这块的内容,我就想了一下,发现问题还是挺大的。

好的找回密码设计

好的找回密码的设计要至少符合以下几点:

不要有专门的表保存这些信息(主要是验证相关的信息);
注册帐号的时候使用邮箱或者手机号作为找回密码凭证(本文暂时只讨论邮箱的方式找回);
只能使用一次,并且要有过期时间;
重复发送验证token应该一样(不该每次点击重新发送都是新的,而且还都可用)。

其实最后一点有两种设计方式,一种是点击重发之后还发之前的那个,另外一种是发个新的,但是之前发的那个失效。

找回密码调研

最近也看了很多网站的找回密码的方式,其中包括但不限于腾讯、淘宝、谷歌、FB等。

首先腾讯用的是“忘了密码?”,进入之后输入qq号和验证码之后提供两种方式找回,一种是密保问题、一种是手机短信。另外腾讯的申诉找回也是相当厉害。

然后是淘宝,使用的是“忘了登录密码?”,进入之后是输入帐号及验证码,会提示几种找回方式。一是短信、二是宝令动态口令、三是人工申诉(上传证件)。

之后是谷歌,使用的是“需要帮助?”,这个提示进去之后会让选择是忘了密码还是用户名、还能选择其他问题。点击忘记密码让输入注册时的邮箱,我输入了自己的帐号,他会给我显示出我的帐号基本信息,问我最后一次记得的使用的密码,我就不输入了,然后有个“我不清楚”的按钮,点击之后让输入手机号,然后输入了之后会提示是否把验证码发到手机,同时还有我无法使用自己的手机的功能,点击之后会提示“Google 十分重视用户的安全和隐私。请回答下面一组问题,以便帮助我们判断您是该帐户的所有者而非试图盗用帐户的黑客。”这里之后就是让填写联系邮箱了,我就不继续测试了,总之谷歌的找回密码方式还是很多样的。

FB的同样也是“忘记密码?”,点击之后让输入邮箱搜索账户。之后有三种方式重置密码:使用谷歌帐号重置(不知道是不是因为我的帐号就是谷歌邮箱导致)、给我邮箱发重置链接、给我发短信重置密码。开始我以为是重置链接呢,写的就是啊,结果是让我输入六位验证码。不过他也有个连接可以认证,有两个参数,一个是u,一个是六位验证码,估计就是提交后的链接吧。应该会有防爆破机制。

验证的方式

看来现在多数都希望使用手机来验证了。毕竟邮箱可以随意注册,但是手机号一般一个人不会有太多个。

不过不管是手机还是邮箱,如果不小心,都会出现大问题。

手机方式:http://www.wooyun.org/bugs/wooyun-2010-018055
邮件方式:http://www.2cto.com/Article/201305/215934.html
http://www.wooyun.org/bugs/wooyun-2013-017322

我们这里暂时只讨论邮箱的方式找回密码。

这样看来,很多链接都是使用参数形式,一个token,一个email作为账户凭证。

其实我觉得最好不要在链接中暴露帐号信息。

rubychina上看到的一个解决方案

看到一种设计方式是给每个人一个token,并且这个token会随着salt的变化而变化,同时在获取验证码的时候,把这个token和过期时间合起来进行编码作为参数发给用户。如果需要判断一个验证链接只能用一次,可以再加一个create时间到发送给用户的链接中,同时服务器保存一个重置密码时间,用来判断是否是第一次使用,第二次则失效。

这种方式也是可以的,但是我觉得还是侵入的数据库表。而且每次查询起来也不好。

我们的现状

我们现在的处理方式有两大问题,一个是可以通过不断回放请求来达到无限发送验证码到邮箱,另一个是每次发送的验证链接都不一样并且还都可以使用。

这有几个结果,一是我们的验证链接(token)是存在redis里面的,可以无限发,如果被攻击可以导致redis崩掉。另外一个是发很多,每次都不一样,而且还都能够使用,这样用户发很多可以无限重置了,这太乱来了。

这个问题暂时还没解决,打算五一后再看,我就先不说我们的项目地址了,哈哈。

我总结的方案

由于我们本身的token(举例链接为http://xxx/qwaszx0998,其中qwaszx0998部分即为token)是临时生成并且存在redis里面的,所以我想出了如下的方式:

首先一点很重要,限制单IP重复请求是很重要的,可以做一个type_ip的key(如resetpass_1.1.1.1),每次请求value加一,设置个过期时间(比如一小时、12小时、一天等)。可以有效防刷(具体可以参考这篇文章——《Redis学习笔记(十)——过期时间、访问限制与缓存》)。
我们之前采用生成临时token为key、email为value,并且设置过期时间。每次请求新的验证链接由于没有办法获取原token,只能每次生成,也没法把以前的立刻删掉。所以我这里考虑双向缓存,即token——email和email——token双向存储,同时都有相同的过期时间,然后每次点击重新发送的时候,通过email查出是否有token,有则直接返回,没有则新建两个缓存(如果每次要求都是新的链接,老链接失效,也可以同时删掉两个缓存重建新的缓存及过期时间)。
在验证并修改密码成功之后,同时删除两个缓存。

本文原创于赵伊凡BLOG

Twemproxy,Twitter 发布的 Redis 代理中间件

简介

由于单个redis实例对大内存管理能力有限(经验是6-8G比较适合,而内存的总大小尽量在系统内存的60%~80%,因为客户端、主从复制都是需要内存的,所以一个128G的内存最好分配称8G*10的redis实例),过大内存将导致redis性能大幅下降。所以我们需要redis集群来提高redis性能。

虽然2015年4月2号的时候redis的3.0.0发布了,支持redis cluster,但是还没有过生产环境的大幅应用来验证其可靠性,所以Twemproxy就成了我们现在对redis集群的最佳选择。

下面是其简单介绍,介绍的已经很详细了:

http://www.oschina.net/translate/twemproxy-a-twitter-redis-proxy

其实Twemproxy就是一个redis的代理,用于实现分片、HashTag、减少连接数等功能;尤其在有大量应用服务器的场景下Twemproxy的角色就凸显了,能有效减少连接数。

下面是集群的架构图

 

twemproxy代理架构
twemproxy代理架构

特性

  • 支持失败节点自动删除

可以设置重新连接该节点的时间 可以设置连接多少次之后删除该节点 该方式适合作为cache存储

  • 支持设置HashTag

通过HashTag可以自己设定将两个KEYhash到同一个实例上去。

  • 减少与redis的直接连接数

保持与redis的长连接 可设置代理与后台每个redis连接的数目

  • 自动分片到后端多个redis实例上

多种hash算法:能够使用不同的策略和散列函数支持一致性hash。 可以设置后端实例的权重

  • 避免单点问题

可以平行部署多个代理层.client自动选择可用的一个

  • 支持redis pipelining request

支持请求的流式与批处理,降低来回的消耗

  • 支持状态监控

可设置状态监控ip和端口,访问ip和端口可以得到一个json格式的状态信息串 可设置监控信息刷新间隔时间

  • 高吞吐量

连接复用,内存复用。 将多个连接请求,组成reids pipelining统一向redis请求。

缺点与不足

不支持针对多个值的操作,比如取sets的子交并补等(MGET 和 DEL 除外)。
不支持Redis的事务操作。
出错提示还不够完善。(发现错误命令就会返回服务器关闭了连接)
也不支持select操作。

其实Twemproxy不支持很多命令,但是大体上不影响使用,有些命令只支持根据分片规则设置过分片标签的key,下面会详细介绍各个配置参数。具体支持与不支持的命令如下:

https://github.com/twitter/twemproxy/blob/master/notes/redis.md

构建Twemproxy

linux上是非常容易构建的,但是需要这几个工具:autoconf、automake、libtool。安装方式大家自己google吧。接着执行如下命令构建:

$ git clone git@github.com:twitter/twemproxy.git
$ cd twemproxy
$ autoreconf –fvi
$ ./configure –enable-debug=full
$ make
$ src/nutcracker –h

这里要注意下,如上安装方式在有些服务器上可能在大量如mset时可能导致Twemproxy崩溃,需要使用如 CFLAGS=”-O1″ ./configure && make或CFLAGS=”-O3 -fno-strict-aliasing” ./configure && make来安装。其实这些官方github上都有介绍:

https://github.com/twitter/twemproxy

配置

配置文件在conf/nutcracker.yml,下面是简单的配置样例:

alpha:
  listen: 127.0.0.1:22121
  hash: fnv1a_64
  distribution: ketama
  auto_eject_hosts: true
  redis: true
  server_retry_timeout: 2000
  server_failure_limit: 1
  servers:
   - 127.0.0.1:6379:1
 
beta:
  listen: 127.0.0.1:22122
  hash: fnv1a_64
  hash_tag: "{}"
  distribution: ketama
  auto_eject_hosts: false
  timeout: 400
  redis: true
  servers:
   - 127.0.0.1:6380:1 server1
   - 127.0.0.1:6381:1 server2
   - 127.0.0.1:6382:1 server3
   - 127.0.0.1:6383:1 server4

alpha:是给当前分片配置起的名字,一个配置可以有多个分片策略。
listen:监听的ip和端口(ip:port或者name:port)。
hash:散列算法。
hash_tag:哈希标签。
distribution:分片算法。
timeout:连接后端Redis或接收响应的超时时间,默认是永久等待。
redis:是否是redis代理,如果是false则是memcached代理。
servers:代理的服务器列表,该列表会使用distribution配置的分片算法进行分片。

hash算法

one_at_a_time
md5
crc16
crc32 (crc32 implementation compatible with libmemcached)
crc32a (correct crc32 implementation as per the spec)
fnv1_64
fnv1a_64
fnv1_32
fnv1a_32
hsieh
murmur
Jenkins

哈希标签

允许使用key的一部分来分配键的存储位置。比如设置为{},则p:{id}:c和p:{id}:n会散列到一台服务器上。(我们仔细看下官方命令支持的文档,发现里面有一部分命令需要在一个实例的时候才支持使用,比如SUNION)

分片算法

  • ketama(一致性Hash算法):

ketama一致性hash算法,会根据服务器构造出一个hash ring,并为ring上的节点分配hash范围。ketama的优势在于单个节点添加、删除之后,会最大程度上保持整个群集中缓存的key值可以被重用。

  • modula(取模):

modula就是根据key值的hash值取模,根据取模的结果选择对应的服务器。

  • random(随机算法):

andom就是无论key值的hash是什么,都随机的选择一个服务器作为key值操作的目标。这种分片适合只读缓存。

servers

这个格式如下:

servers:
– ip:port:weight alias
– ip:port:weight alias

最好加上别名,这样可以在一个实例宕机的时候,更换机器只需要使用之前机器的别名,就可以继续映射了,如果没有别名,就得全部重新对应。

其他参数:

backlog
监听TCP 的backlog(连接等待队列)的长度,默认是512。
preconnect
是一个boolean值,指示twemproxy是否应该预连接pool中的server。默认是false。
server_connections
每个server可以被打开的连接数。默认,每个服务器开一个连接。
auto_eject_hosts
是一个boolean值,用于控制twemproxy是否应该根据server的连接状态重建群集。这个连接状态是由server_failure_limit阀值来控制。 默认是false。 (是否在节点故障无法响应时自动摘除该节点,如果作为存储需要设置为为false)
server_retry_timeout
单位是毫秒,控制服务器连接的时间间隔(重新连接一个临时摘掉的故障节点的间隔),在auto_eject_host被设置为true的时候产生作用。默认是30000 毫秒。
server_failure_limit
控制连接服务器的次数(节点故障无法响应多少次从一致性Hash环临时摘掉它),在auto_eject_host被设置为true的时候产生作用。默认是2。

关于key值长度的限制

memcache限制key值在250字符以内,redis则没什么限制,由于twemproxy将key值存放在连续的内存之中,所以twemproxy的key值的最大长度受到mbuf长度的限制。

mbuf的长度由-m指定,默认是16384字节,一般够用了。如果遇到key值过长的问题,可以调整这个参数。

mbuf的含义&调整

最小512字节,最大65536字节,默认16384字节。可以通过命令行的-m参数调整。

mbuf是twemproxy引以为傲的zero-copy技术的底层支撑,zero-copy意味着从客户端接收的数据直接被提交到redis-server,不需要经过中间的copy环节(看似不难,实际上操作起来很难做到)。

很明显,大尺寸的mbuf会增加性能,减少分包的次数,但是会增加对内存的消耗。

如何估计twemproxy的mbuf对内存的需求呢?公式如下:

max(client_connections, server_connections) * 2 * mbuf-size

因为存在client->twemproxy以及twemproxy->redis-server两个连接,所以mbuf是需要双份的。

大多客户端的连接会大于服务器连接池预设的连接数。我们假设1000个客户端连接,mbuf-size是16KB,那么大概会消耗掉1000*2*16KB=32M左右的内存。

启动

下面是启动参数:

Usage: nutcracker [-?hVdDt] [-v verbosity level] [-o output file]

[-c conf file] [-s stats port] [-a stats addr]

[-i stats interval] [-p pid file] [-m mbuf size]

Options:

-h, –help             : this help
-V, –version          : show version and exit
-t, –test-conf        : test configuration for syntax errors and exit
-d, –daemonize        : run as a daemon
-D, –describe-stats   : print stats description and exit
-v, –verbose=N        : set logging level (default: 5, min: 0, max: 11)
-o, –output=S         : set logging file (default: stderr)
-c, –conf-file=S      : set configuration file (default: conf/nutcracker.yml)
-s, –stats-port=N     : set stats monitoring port (default: 22222)
-a, –stats-addr=S     : set stats monitoring ip (default: 0.0.0.0)
-i, –stats-interval=N : set stats aggregation interval in msec (default: 30000 msec)
-p, –pid-file=S       : set pid file (default: off)
-m, –mbuf-size=N      : set size of mbuf chunk in bytes (default: 16384 bytes)

小结

使用Twemproxy时为了避免单点故障的问题,我们可以同时使用两个Twemproxy都代理那些redis节点,然后使用Jedis分片来使用。

另外豆瓣在去年年底出的Codis也是非常不错的,架构略有不同;同时使用Pre-Sharding机制,事先定好支持1024片,使得其扩容比较容易。性能测试结果好像很不错,同时支持Twemproxy的无缝迁移。

Redis Cluster

在这种机制下,没有中心节点(和代理模式的重要不同之处)。所以,一切开心和不开心的事情,都将基于此而展开。Redis Cluster将所有Key映射到16384个Slot中,集群中每个Redis实例负责一部分,业务程序通过集成的Redis Cluster客户端进行操作。客户端可以向任一实例发出请求,如果所需数据不在该实例中,则该实例引导客户端自动去对应实例读写数据。Redis Cluster的成员管理(节点名称、IP、端口、状态、角色)等,都通过节点之间两两通讯,定期交换并更新。

由此可见,这是一种非常“重”的方案。已经不是Redis单实例的“简单、可依赖”了。可能这也是延期多年之后,才近期发布的原因之一。

本文原创于赵伊凡BLOG