= redis简单调研 =
== 简介 ==
redis是一个key-value存储系统。和Memcached类似,它支持存储的value类型相对更多,包括string、list、set、zset和hash。这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。在此基础上,redis支持各种不同方式的排序。与memcached一样,为了保证效率,数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。
参考:http://redis.io/documentation
拥有丰富的客户端API,如c,java,ruby,python,c++,c#等等
参考:http://redis.io/clients
适用的场合:
1. 需要精准设定过期时间的应用;
2. 实时系统;
3. Pub/sub构建实时消息系统;
4. 缓存;
5. ……
实际应用案例:
1. sina微博;
2. Twitter;
3. 淘宝秒杀;
4. ……
== 快速入门 ==
安装redis:
1. 下载:http://www.redis.io/download
2. 解压:tar zxvf redis-2.6.16.tar.gz
3. 编译:make
4. 运行:./src/redis-server ./src/redis-cli
配置redis:
1. 两种方法指定配置参数
* 在配置文件redis.conf中配置
* 在启动redis-server是指定
2. 运行过程中也可以修改配置
通过命令:CONFIG SET 和 CONFIG GET
参考:http://redis.io/topics/config
redis数据类型
1. Strings
2. Lists
3. Sets
4. ZSets
5. Hashes
参考:http://redis.io/topics/data-types
== Redis特性 ==
=== 安全性 ===
参考:http://redis.io/topics/security
1. 网络安全设置
可以对ip地址进行bind,只接受来自此ip的请求;
2. 设置访问密码
* 密码明文配置在redis.conf中,要保证此文件安全;
* 密码要足够可靠,redis client可以高速尝试密码;
3. 禁用命令或者重命名命令
保证一些敏感命令不被其他客户使用
=== 主从复制 ===
参考:http://redis.io/topics/replication
1. 特点
* 使用异步复制;
* Master可以拥有多个slave;
* Slave可以接受其他slave的复制请求;
* 主从复制不会阻塞master;
* 在slave上也是非阻塞;
* slave是只读的;
2. 缺点
每次断开重建连接之后都得进行全量同步,增量同步不支持;
3. 配置
slave <masterip> <masterport>
=== 事务控制 ===
参考:http://redis.io/topics/transactions
1. 命令基础
MULTI, EXEC, DISCARD, WATCH
2. 简单机制
当client发出multi命令后,进入事务上下文状态,后续命令并不立即执行,先放入一个队列中,当接收到exec命令后,按顺序执行队列中的所有命令,然后将所有命令的运行结果返回打包给client。
3. 不足
事务中的一个命令执行失败并不回滚其他命令。
=== 持久化机制 ===
参考:http://redis.io/topics/persistence
1. 可选的持久化方式
* RDB持久化,在指定的时间间隔内生成数据集的时间点快照
* AOF持久化,记录服务器执行的所有写操作命令
* RDB+AOF
* 关闭持久化
2. RDB优点
* 文件紧凑,适合用于备份
* 适用于灾难恢复
* 可以最大化性能,父进程fork一个子进程进行磁盘IO
* RDB恢复大数据集时速度比AOF快
3. RDB缺点
* 数据不是非常可靠,可能会丢失近几分钟的数据
* 数据集特别庞大进行写磁盘时候,子进程可能会非常耗时,使得服务器在短时间内停止处理客户端请求
4. AOF优点
* 数据非常耐久,可以设置fsync策略
* 人为可读,未写入完整命令时,可以人为处理最后一条命令
* AOF文件体积过大时,后台会自动进行重写
* AOF文件只进行追加,易修复
5. AOF缺点
* AOF文件比RDB体积大
* 性能比RDB慢,在处理巨大的写入时,RDB可以提供更有保证的最大延迟
* AOF过去曾发生过bug:重新载入时无法将数据集恢复成原样
=== Pipeline批量发送请求 ===
参考:http://redis.io/topics/pipelining
1. 客户端批量发送命令,服务器也批量回复结果;
2. Some benchmark(1w次的ping)
running over the loopback interface, where pipelining will provide the smallest improvement as the RTT is already pretty low:
=== 导入海量数据 ===
参考:http://redis.io/topics/mass-insert
1. 利用redis-cli --pipe
类似:cat data.txt | redis-cli --pipe -h host -p port
2. 需要一定的格式
参考:http://redis.io/topics/protocol
3. 举例
=== Redis分区 ===
参考:http://www.redis.cn/topics/partitioning.html
1. 目的:将数据分别存到多个redis实例中,可以支持更大的数据,可以扩展多核和多个计算机能力,以及网络带宽什么。
2. 分区方法:
* client side partitioning
* Proxy assisted partitioning;(twemproxy)
* Query routing;(redis集群中有实现)
3. 缺陷:
* 多key操作不支持;
* 多key事务不支持;
* 分区粒度是关键,不能对一个key下面非常多元素的sorted set分片;
* 数据处理更复杂,例如备份需要合并多个主机的RDB/AOF数据;
* 添加删除容量会比较复杂;(虽然redis集群计划支持,但是其他客户端或者代理不支持)
4. Store or cache
* 如果用作cache,使用一致性hash容易向上向下扩展;
* 用作存储,要在key和固定节点之间做映射,并且使用固定数量节点;
5. Presharding(预分片)
一开始的时候,计划好实例数,全部都启动,后续需要增加机器时,将实例从一台服务器移到另一台服务器。
=== Redis高可用 ===
参考:http://redis.io/topics/sentinel
1. Redis自身提供的工具Sentinel
* 2.6版本可用了,但是还在beta版本;
* 任务和功能:
a. 监控master和slave是否正常工作;
b. 通知管理者;
c. 自动故障转移;
2. 其他工具,比如keepalived
Keepalived 是一个用c写的路由选择软件,通过VRRP 协议提供高可用。机器之间使用VRRP路由协议切换VIP,切换速度秒级,且不存在脑裂问题。可以实现一主多备,主挂后备自动选举,漂移VIP,切换速度秒级;切换时可通过运行指定脚本更改业务服务状态。
=== Redis集群 ===
参考:http://redis.io/presentation/Redis_Cluster.pdf
还在实现当中,当前还不可用,预计3.0版本可以完成。
届时可以实现:1. 自动分片;2. 易于扩展。
== Redis的代理twemproxy ==
参考:https://github.com/twitter/twemproxy
=== 能做什么?===
1. 在客户端和众多redis实例之间作为代理;
2. 在配置的redis实例之间进行自动数据分片;
3. 支持一致性hash,支持不同的策略和hash方法;
4. 内部判断进行pipeline;
=== 安装 ===
autoreconf -fvi
./configure --enable-debug=log
make
./src/nutcracker -h
=== 配置 ===
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
timeout: 5000
servers:
- 127.0.0.1:6379:1 server1
- 127.0.0.1:6380:1 server2
利用"nc host port"可以看到twemproxy的状态信息,json格式,port默认22222
=== 不足 ===
1. 不支持批量操作;
2. 性能损耗,最坏的情况损耗不会超过20%;
3. 报错机制不完善;
== 性能测试 ==
=== 官方benchmark ===
参考:http://redis.io/topics/benchmarks
=== 测试环境 ===
Cpu: Intel(R) Pentium(R) Dual CPU E2160 @ 1.80GHz (双核)
Memory: 2G
Ubuntu 12.04 (Linux 3.2.0-29 x64)
数据:1061498条记录(百万级别)
数据大小:764458359Bytes=729.0443MB
Redis使用rdb持久化
使用普通socket跨网络传输
=== 内存占用 ===
全部导入数据后,查看redis占用了929346528Bytes=886.2939MB。
Redis所耗的内存跟数据大小比为1.21 ~ 1.22之间,基本在1.215左右。
=== 普通场景 ===
| | 所耗时间 | 性能
| DirectlyWrite | 236秒 | 4497.87条/s
| PipelinedWrite| 18秒 | 58972.11条/s
| DirectlyRead | 238秒 | 4460.08条/s
| PipelinedRead | 21秒 | 50547.52条/s
注意:使用pipelined后,差不多每秒传输40.5MB的数据,也有可能是网络带宽受限了!
=== 使用代理 ===
| | 所耗时间 | 性能
| DirectlyWritep1r1 | 300s | 3538.33/s
| DirectlyWritep1r2 | 298s | 3562.07/s
| DirectlyReadp1r1 | 296s | 3586.14/s
| DirectlyReadp1r2 | 366s | 2900.27/s
| | 所耗时间 | 性能
| PipelinedWritep1r1 | 17s | 62441.06/s
| PipelinedWritep1r2 | 17s | 62441.06/s
| PipelinedReadp1r1 | 19s | 55868.32/s
| PipelinedReadp1r2 | 19s | 55868.32/s
=== 利用protocol ===
由于value中有中文字符,估计是编码导致的bug,导入有错误,故未进行测试。
=== 进入swap ===
写性能:每次导入30w的记录
读性能:
对一份30w数据进行不断的读:大约4次之后,全部数据都从虚拟内存pagein到内存中
不断的读不同的数据
== 系统架构 ==
1. 负载均衡
2. 可扩展性
3. 高可用性
== Hadoop+Redis ==
在hadoop中用java写入Redis,可以采用两种方式,一个是派生OutputFormat类,在其中写redis,不用调整reduce逻辑;另一个是直接在reduce中写redis。
1. 直接写redis
public static class parseDataIntoRedisReducer
extends Reducer<Text,Text,Text,Text> {
private Jedis jedis;
private Pipeline pl;
private int count = 0;
public void reduce(Text key, Iterable<Text> values, Context context
) throws IOException, InterruptedException {
String rKey = key.toString();
String value;
Iterator<Text> iter = values.iterator();
while (iter.hasNext()) {
value = iter.next().toString();
pl.set(rKey, value);
if (Constant.PL_NUM == ++count) {
pl.sync();
count = 0;
}
}
}
protected void setup(Context context) throws IOException, InterruptedException {
jedis = new Jedis(Constant.host, Constant.port, Constant.timeout);
jedis.connect();
pl = jedis.pipelined();
}
protected void cleanup(Context context) throws IOException, InterruptedException {
if (null != pl) pl.sync();
count = 0;
if (jedis.isConnected()) {
jedis.quit();
jedis.disconnect();
}
}
}
2. 派生OutputFormat
package com.cxz.test;
import java.io.IOException;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.*;
import redis.clients.jedis.Jedis;
/**
* Created with IntelliJ IDEA.
* User: chenxz
* Date: 13-8-29
* Time: 上午10:14
* To change this template use File | Settings | File Templates.
*/
public class RedisOutputFormat extends OutputFormat<Text, Text> {
public RecordWriter getRecordWriter(TaskAttemptContext context) throws IOException {
return new RedisRecordWriter();
}
public OutputCommitter getOutputCommitter(TaskAttemptContext context) throws IOException {
return new OutputCommitter() {
@Override
public void setupJob(JobContext jobContext) throws IOException {
//To change body of implemented methods use File | Settings | File Templates.
}
@Override
public void setupTask(TaskAttemptContext taskAttemptContext) throws IOException {
//To change body of implemented methods use File | Settings | File Templates.
}
@Override
public boolean needsTaskCommit(TaskAttemptContext taskAttemptContext) throws IOException {
return false; //To change body of implemented methods use File | Settings | File Templates.
}
@Override
public void commitTask(TaskAttemptContext taskAttemptContext) throws IOException {
//To change body of implemented methods use File | Settings | File Templates.
}
@Override
public void abortTask(TaskAttemptContext taskAttemptContext) throws IOException {
//To change body of implemented methods use File | Settings | File Templates.
}
};
}
public void checkOutputSpecs(JobContext context) throws IOException {
Jedis jedis = new Jedis(Constant.host, Constant.port);
if (!jedis.ping().equals("PONG")) {
throw new IOException("can not connect to redis!");
}
if (jedis.isConnected()) {
jedis.quit();
}
}
public static class RedisRecordWriter extends RecordWriter<Text, Text> {
public Jedis jedis;
public RedisRecordWriter() throws IOException {
jedis = new Jedis(Constant.host, Constant.port);
}
public void write(Text key, Text value) throws IOException {
if (key != null && value != null) {
jedis.set(key.toString(), value.toString());
}
}
public void close(TaskAttemptContext context) throws IOException {
if (jedis.isConnected()) {
jedis.quit();
}
}
}
}
在reduce中按照普通的方式编写逻辑
public void reduce(Text key, Iterable<Text> values, Context context
) throws IOException, InterruptedException {
Iterator<Text> iter = values.iterator();
while (iter.hasNext()) {
context.write(key, iter.next());
}
}
在job中指定输出格式
job.setOutputFormatClass(RedisOutputFormat.class);