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转载注明出处。

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

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,对key做过期时间的时候,碰到了个问题,这里原因就不说了,我对一个key设置了过期时间为100天,结果测试过程中并没有什么问题,但是线上却频频报错。

组件使用的是spring-data-redis&Jedis。

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

at org.springframework.data.redis.connection.jedis.JedisExceptionConverter.convert(JedisExceptionConverter.java:47)

at org.springframework.data.redis.connection.jedis.JedisExceptionConverter.convert(JedisExceptionConverter.java:36)

at org.springframework.data.redis.PassThroughExceptionTranslationStrategy.translate(PassThroughExceptionTranslationStrategy.java:37)

at org.springframework.data.redis.FallbackExceptionTranslationStrategy.translate(FallbackExceptionTranslationStrategy.java:37)

at org.springframework.data.redis.connection.jedis.JedisConnection.convertJedisAccessException(JedisConnection.java:181)

at org.springframework.data.redis.connection.jedis.JedisConnection.expire(JedisConnection.java:773)

at org.springframework.data.redis.core.RedisTemplate$7.doInRedis(RedisTemplate.java:648)

at org.springframework.data.redis.core.RedisTemplate$7.doInRedis(RedisTemplate.java:641)

at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:190)

at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:152)

at org.springframework.data.redis.core.RedisTemplate.expire(RedisTemplate.java:641)

……

 

Caused by: redis.clients.jedis.exceptions.JedisConnectionException: Unknown reply: 2

at redis.clients.jedis.Protocol.process(Protocol.java:128)

at redis.clients.jedis.Protocol.read(Protocol.java:187)

at redis.clients.jedis.Connection.getIntegerReply(Connection.java:201)

at redis.clients.jedis.BinaryJedis.expire(BinaryJedis.java:330)

at org.springframework.data.redis.connection.jedis.JedisConnection.expire(JedisConnection.java:771)

… 25 more

看这个错误,有点莫名其妙了,搜了一下,发现好多都是说单例线程不安全的问题。让用线程池,但是我这里使用的是spring的封装,也确实是使用线程池了的。

实在搞不定,于是决定跟下代码,看看会是什么原因。

首先根据错误提示,了解到是在执行expire的时候出的问题,如下:

at org.springframework.data.redis.core.RedisTemplate.expire(RedisTemplate.java:641)

也就是除了自己代码的上一行错误信息,这条错误信息可以让我定位到错误是因为执行对某个key进行设置过期时间导致的。

接下来看具体的代码:

public Boolean expire(K key, final long timeout, final TimeUnit unit) {

final byte[] rawKey = rawKey(key);

final long rawTimeout = TimeoutUtils.toMillis(timeout, unit);

 

return execute(new RedisCallback<Boolean>() {

 

public Boolean doInRedis(RedisConnection connection) {

try {

return connection.pExpire(rawKey, rawTimeout);

} catch (Exception e) {

// Driver may not support pExpire or we may be running on Redis 2.4

return connection.expire(rawKey, TimeoutUtils.toSeconds(timeout, unit));

}

}

}, true);

}

这里其实就已经引起我的一点注意了,这个注释实在是,,

首先看他是先利用pexpire命令来执行,而不是我们想要的expire命令,如果被捕获异常的或,就用expire命令执行。这里注释写的如果使用的驱动不支持pExpire命令,或者是2.4版本的redis的话,就会执行expire。首先我确认了自己使用的驱动,是支持这个命令的。

接着上官网,根据官方命令介绍,pexpire是redis2.6才开始支持的。一会再看版本,先看pExpire里面的代码。

(其实根据下面一行堆栈错误信息可以知道,其实具体执行的是catch里面的那行)

at org.springframework.data.redis.core.RedisTemplate$7.doInRedis(RedisTemplate.java:648)

我们先看下pExpire执行了什么可能导致抛异常。

public Boolean pExpire(byte[] key, long millis) {

 

/*

*  @see DATAREDIS-286 to avoid overflow in Jedis

*

*  TODO Remove this workaround when we upgrade to a Jedis version that contains a

*  fix for: https://github.com/xetorthio/jedis/pull/575

*/

if (millis > Integer.MAX_VALUE) {

 

return pExpireAt(key, time() + millis);

}

 

try {

if (isPipelined()) {

pipeline(new JedisResult(pipeline.pexpire(key, (int) millis), JedisConverters.longToBoolean()));

return null;

}

if (isQueueing()) {

transaction(new JedisResult(transaction.pexpire(key, (int) millis), JedisConverters.longToBoolean()));

return null;

}

return JedisConverters.toBoolean(jedis.pexpire(key, (int) millis));

} catch (Exception ex) {

throw convertJedisAccessException(ex);

}

}

这个TODO才真是让我烦,咋还这样啊。这里因为要使用pExpire命令,所以把单位转换成了毫秒,100天是8640000000,Integer.MAX_VALUE是2的31次方减1,是2147483647,很明显,大了很多。所以改执行pExpireAt命令了,这个命令是根据Unix时间去设置具体过期时间的。所以它这里调用了redis的server命令,time去获取当前系统时间,然后加上需要的过期时间8640000000就是最终的过期时间了。

这里的time是个坑,如果你使用的是twemproxy,这是一个在redis cluster出现之前的一个分布式解决方案。

官方代码库中的wiki有个支持的命令列表:

https://raw.githubusercontent.com/twitter/twemproxy/master/notes/redis.md

从这里可以搜到,它不支持TIME命令。(相关命令可以通过redis官网查看)

但是很明显,堆栈中并没有对TIME命令报错的信息,所以就要正常执行pExpireAt方法了,这里免直接执行jedis.pexpireAt,如果版本低于2.6,那么不支持这个命令。

public Boolean pExpireAt(byte[] key, long unixTimeInMillis) {

try {

if (isPipelined()) {

pipeline(new JedisResult(pipeline.pexpireAt(key, unixTimeInMillis), JedisConverters.longToBoolean()));

return null;

}

if (isQueueing()) {

transaction(new JedisResult(transaction.pexpireAt(key, unixTimeInMillis), JedisConverters.longToBoolean()));

return null;

}

return JedisConverters.toBoolean(jedis.pexpireAt(key, unixTimeInMillis));

} catch (Exception ex) {

throw convertJedisAccessException(ex);

}

}

抛出异常,被最外层接住,使用expire命令去处理。

回来看expire方法。

public Boolean expire(byte[] key, long seconds) {

 

/*

*  @see DATAREDIS-286 to avoid overflow in Jedis

*

*  TODO Remove this workaround when we upgrade to a Jedis version that contains a

*  fix for: https://github.com/xetorthio/jedis/pull/575

*/

if (seconds > Integer.MAX_VALUE) {

 

return pExpireAt(key, time() + TimeUnit.SECONDS.toMillis(seconds));

}

 

try {

if (isPipelined()) {

pipeline(new JedisResult(pipeline.expire(key, (int) seconds), JedisConverters.longToBoolean()));

return null;

}

if (isQueueing()) {

transaction(new JedisResult(transaction.expire(key, (int) seconds), JedisConverters.longToBoolean()));

return null;

}

return JedisConverters.toBoolean(jedis.expire(key, (int) seconds));

} catch (Exception ex) {

throw convertJedisAccessException(ex);

}

}

晕,怎么又来了。哦,没事,这次是秒,没那么大了。直接执行下面的jedis.expire方法。通过堆栈信息也可以知道确实执行这一行了。

at org.springframework.data.redis.connection.jedis.JedisConnection.expire(JedisConnection.java:771)

这次怎么感觉没什么问题了。根据最下面的堆栈信息网上看。

public Long expire(final byte[] key, final int seconds) {

checkIsInMulti();

client.expire(key, seconds);

return client.getIntegerReply();

}

继续看堆栈信息。

at redis.clients.jedis.BinaryJedis.expire(BinaryJedis.java:330)

然后对比代码是执行了client.getIntegerReply();出的问题。

public Long getIntegerReply() {

flush();

pipelinedCommands–;

return (Long) Protocol.read(inputStream);

}

看这里,就是Protocol.read(inputStream);有问题了,跟进去,里面有个process(is);。

private static Object process(final RedisInputStream is) {

try {

byte b = is.readByte();

if (b == MINUS_BYTE) {

processError(is);

} else if (b == ASTERISK_BYTE) {

return processMultiBulkReply(is);

} else if (b == COLON_BYTE) {

return processInteger(is);

} else if (b == DOLLAR_BYTE) {

return processBulkReply(is);

} else if (b == PLUS_BYTE) {

return processStatusCodeReply(is);

} else {

throw new JedisConnectionException(“Unknown reply: ” + (char) b);

}

} catch (IOException e) {

throw new JedisConnectionException(e);

}

return null;

}

找到根源了。原来这里的b返回的不是这里声明的几个常量啊。根据错误信息打印出的2,对比下ASCII可以知道,2是正文开始。好吧,其实这里就是传输返回协议的内容了。如果开头协议不对就不行了。

什么叫不对呢?来来来,看这里,官方介绍的协议。

http://redis.io/topics/protocol

它是用的是RESP。

RESP protocol description

The RESP protocol was introduced in Redis 1.2, but it became the standard way for talking with the Redis server in Redis 2.0. This is the protocol you should implement in your Redis client.

RESP is actually a serialization protocol that supports the following data types: Simple Strings, Errors, Integers, Bulk Strings and Arrays.

The way RESP is used in Redis as a request-response protocol is the following:

Clients send commands to a Redis server as a RESP Array of Bulk Strings.

The server replies with one of the RESP types according to the command implementation.

In RESP, the type of some data depends on the first byte:

For Simple Strings the first byte of the reply is “+”

For Errors the first byte of the reply is “-”

For Integers the first byte of the reply is “:”

For Bulk Strings the first byte of the reply is “$”

For Arrays the first byte of the reply is “*”

Additionally RESP is able to represent a Null value using a special variation of Bulk Strings or Array as specified later.

In RESP different parts of the protocol are always terminated with “\r\n” (CRLF).

不同的情况会有不同的协议开头。这里就不具体介绍了大家有兴趣自己往下看。

其实与redis服务端交互是在进行协议信息的交互,废话了,不过如果无法理解服务端相应的内容的话,那就报错了。

后来把程序改了,把缓存时间缩小为20天,换算得到毫秒数为1728000000小于2147483647了,这样就可以执行pExpire而不是pExpireAt了。线上停止报错,正常执行了。计算一下2147483647最大支持的时间是24.8天。所以大家看着设置吧。

最终只是改了时间就解决了,但是实际上为什么在expire处出现unknow reply不太好确认。

而导致执行catch里的expire,这个原因可以确认是执行了pExpireAt导致的,把时间改小了之后会执行pExpire,则不再报错。两者的唯一区别就是时间的区别,也就是那个if的判断,怀疑pExpireAt与pExpire那里(毕竟这个问题是2014年3月多才提交出来的,怀疑线上jar没有这段if判断导致报错)。

这里总结的两点,一个是我们通过查问题发现,TIME命令不能在使用了twemproxy代理中使用,这里通过spring的代码可以发现如果设置时间过长是一定会遭遇这个的。

另一个是我们学习到了redis的通信协议。

 

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

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

使用hash作为redis的存储类型以节省内存

一般我们存储一个键,很自然的就会使用get/set去存储,实际上这并不是很好的做法。

实际上redis存储一个key会有一个最小内存,不管你存的这个键多小,都不会低于这个内存。所以我们可以把key复用,存储hash类型。下面的内容转自http://oldblog.antirez.com/post/redis-weekly-update-7.html,我大概翻译一下。

如果我们希望执行SET foo bar,我们实际可以这样:

1、把foo通过SHA1算法计算出其值,0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33。
2、使用前四个字符作为真实的key
3、使用前四个字符“0bee”作为hash的key,来存储上述值

所以我们在存储值的时候,执行HSET 0bee foo bar。

下面是一些计算:

1、使用SHA1后的前四个字符作为key,这样一个key里面有65536个键值可以存储。
2、如果有一百万的个键值,差不多每个hash可以存储15个键
3、如果一千万个键值,差不多每个hash可以存储152个键

所以我们可以在redis.conf中设置如下参数:

hash-max-zipmap-entries 512
hash-max-zipmap-value 512

有一个数据的对比:

正常情况下存储一千万的数据需要1.7GB
使用hash存储一千万的数据需要300MB

dump file只有200MB

所以我们在存储纯key-value的数据的时候可以采用hash来提高内存使用效率。

本文原创于赵伊凡BLOG

Redis学习笔记(十)——过期时间、访问限制与缓存

过期时间

之前应该提到过redis的特性之一是可以设置键的超时时间。命令是expire。

redis > SET session:27e7a id1234
OK
redis > EXPIRE session:27e7a 1200
(integer) 1

EXPIRE命令返回1表示成功,返回0表示键值不存在或设置失败。

同时这里还有一个比较常用的命令是ttl,用于查看一个键还有多久时间会被删除。返回的是剩余时间(秒数)。

这里就不贴代码了,有一点需要说明的是,ttl命令在键不存在或被删除之后,会返回-2,在没有为键设置生存时间(即永久存在,建一个键之后的默认情况)时返回的是-1。大家可以亲自操作一把。

如果想要把一个设置过过期时间的键取消过期时间设置,则需要使用persist命令。

redis > SET session:27e7a id1234
OK
redis > EXPIRE session:27e7a 1200
(integer) 1
redis > TTL session:27e7a
(integer) 1092
redis > PERSIST session:27e7a
(integer) 1
redis > TTL session:27e7a
(integer) -1

这里需要说明一点的是,除了使用persist命令外,使用set、getset命令为键赋值,也会同时消除键的生存时间,如果需要可以重新使用expire命令为键设置生存时间。而其他对键的操作命令(如incr、lpush、hset、zrem)都不会影响键的生存时间。

expire命令的单位是秒,而且这个参数必须为整数,如果需要更精准的时间的话,需要使用pexpire命令设置,其单位为毫秒,同理也需要用pttl命令来看键的剩余毫秒数。当然使用expire命令设置的过期时间也是可以用pttl看键的剩余毫秒数的。

访问限制

有时候我们会有一个需求是需要限制一个用户对一个资源的访问频率,我们假定一个用户(用IP作为判断)每分钟对一个资源访问次数不能超过10次。

我们可以使用一个键,每次用户访问则把值加1,当值加到10的时候,我们设定键的过期时间为60秒,并且禁止访问。这时候下次访问发现值为10,则不让访问了,然后60秒后键被删除,这时候再次创建键。这样就可以解决,但是其实这样时间并不精准,问题还是挺大的。

我们还有一个方案:使用队列。前面的章节也说到了,使用列表类型可以用作队列。

我们设定一个队列rate.limiting.192.168.1.1(假定是这个IP),我们把每次的访问时间都添加到队列中,当队列长度达到10以后,判断当前时间与队列第一个值的时间差是否小于60,如果小于60则说明60秒内访问次数超过10次,不允许访问;否则说明可以访问,则把队列头的值删除,队列尾增加当前访问时间。

这种方法可以比较精准的实现访问限制,但是当限制的次数比较大时,这种方法占用的存储空间也会比较大。

缓存

有时候会把一些对CPU或IO资源消耗比较大的操作结果缓存起来,并设置一定时间的自动过期。比如我们设定一个微博外链的最热站点缓存放于新浪微博的首页,这样我们不可能每次访问都重新计算最热的外链站点,所以我们可以设定两小时更新一次。每次访问是判断这个键有没有,如果存在则直接返回,如果没有则通过计算把内容存入键中,并设定两小时的过期时间。

然而在很多场合这种方法会很恐怖,当服务器内存有限的时候,大量使用缓存切设置生存时间过长就会导致redis占用太多内存,而redis有时候会把系统内存都吃掉,导致系统崩溃。但是设置时间过短又会导致缓存的命中太低。

所以我们最好的办法是设定缓存的淘汰规则。这种方式比较适用于将redis用作缓存系统的时候比较好。

具体就是:修改配置文件中的maxmemory参数,限制redis的最大内存,当超出后会按照maxmemory-policy参数指定的策略删除不需要的键,直到redis占用的内存小于设定值。

规则

说明

volatile-lru

使用LRU算法删除一个键(只对设置了生存时间的键)

allkeys-lru

使用LRU算法删除一个键

volatile-random

随机删除一个键(只对设置了生存时间的键)

allkeys- random

随机删除一个键

volatile-ttl

删除生存时间最近的一个键

noeviction

不删除键,只返回错误

其中的LRU算法即是【最近最少使用】。

这里提一句,实际上redis根本就不会准确的将整个数据库中最久未被使用的键删除,而是每次从数据库中随机取3个键并删除这3个键里最久未被使用的键。上面提到的所有的随机的操作实际上都是这样的,这个3可以用过redis的配置文件中的maxmemeory-samples参数配置。

本文发表自赵伊凡BLOG

Redis学习笔记(九)——事务进阶

事务除了本身的那个multi…exec命令组合外,还有一个命令,就是watch。

watch命令可以保证,watch的值在被修改后,事务中再执行的修改操作无法被执行(是整个事务无法执行,而不只是对那一个键值的操作无法执行)。

redis>SET key 111
OK
redis>WATCH key
OK
redis>SET key 222
OK
redis>MULTI
OK
redis>SET key 333
QUEUE
redis>EXEC
(nil)
redis>GET key
"222"

如上命令,当watch了key之后,我们修改了key的值,然后在后面在执行命令修改key的值,在执行了EXEC之后,返回的(nil)也就是没有返回,也就是说事务里的命令没有被执行。

也可以这样描述watch命令的意义,watch命令的作用是当被监视的键值被修改后阻止之后一个事务的执行。

另外watch命令的作用在执行一个exec命令之后就会消失,如果需要提前取消监视,可以执行unwatch命令。

本文发表自赵伊凡BLOG

Redis学习笔记(八)——事务入门

七、事务入门

在前面我们学习了redis的常用命令以及五种数据类型。大家是否都掌握了呢?其实这些还是很简单并且好记的。

这里重复一下链接地址:

Redis学习笔记(三)——Redis常用命令入门——字符串类型命令

Redis学习笔记(四)——Redis常用命令入门——散列类型

Redis学习笔记(五)——Redis常用命令入门——列表类型

Redis学习笔记(六)——Redis常用命令入门——集合类型

Redis学习笔记(七)——Redis常用命令入门——有序集合类型

下面我们接着学习数据库知识里面一个必不可少的内容——事务(Transaction)。在关系型数据库中我们已经很了解了,事务就是要么都执行,要么都不执行。这个应该不管在怎样的数据库中都是一样的。

1、基本命令

首先我先介绍下Redis事务的关键字:MULTI…EXEC。下面举个例子。

我们在Redis中添加数据,由于没有办法想关系型数据库一样一条语句把所有的数据全部添加进去(没有外键的情况下,有外键需要先添加外键所属的那条数据)。像是我们添加文章(这里以只添加文章title和文章的标签为例,这里我们假设要添加的文章id为10),我们需要如下操作:

redis>MULTI
OK
redis>set post:10:title helloWorld
QUEUE
redis>sadd post:10:title Java,Redis
QUEUE
redis>EXEC
1) (integer) 1
2) (integer) 1

这里我们发现,当输入MULTI时,Redis返回OK,告诉我们他已经知道这里是事务的开始了,接下来我们输入命令,他都会告诉我们已经加入到队列里了,当输入EXEC时,按顺序执行,并按顺序返回执行结果。

Redis在这里还有个处理,就是在事务中,如果在发送EXEC命令前客户端断了,则Redis会清空事务队列,而在EXEC后断开,则所有的命令还是会继续执行。

2、错误处理

如果事务在执行过程中出错了,Redis是怎么处理的呢?这里分两种情况:语法错误、运行错误。

(1)语法错误。比如参数不对或者命令不存在。

这种情况下,只要有一个命令有语法错误,Redis不会显示QUEUE而是报告错误信息,执行EXEC命令后Redis也会说有错误存在。语法正确的命令也不会执行(注意一下这里在2.6.5之前的版本会忽略错误的语句而执行其他正确的命令)

(2)运行错误。运行错误是在发送命令到队列时无法发现的错误。

比如先执行SET a 1,又执行SADD a 2,再执行SET a 3,这样在事务队列里面是发现不了的,但是在执行了EXEC时确实可以发现的。并且还会返回那条语句对应的错误信息。

但是这里不同的是,Redis在运行错误时,仅仅只是会忽略错误的语句,但是正确的语句依然会执行。所有我们GET a,返回的是3。

最后还有一点我们需要知道。Redis的事务没有关系数据库提供的回滚,也让Redis执行的更快并简洁。但是为此开发者必须在事务执行出错后自己收拾剩下的烂摊子。

不过在保持语法正确并且很好的规划好数据库(保证键的规范等)的使用,是不会出现上述运行错误的。

本文原创于本人个人博客,请访问 http://irfen.me