Jade Dungeon

redis安装与配置

安装

redis历史版本都写在README.md里: https://github.com/redis/redis-hashes/blob/master/README

http://download.redis.io/releases/

linux

安装:

$ wget http://download.redis.io/releases/redis-2.8.13.tar.gz
$ tar xzf redis-2.8.13.tar.gz
$ cd redis-2.8.13
$ make

启动服务:

$ src/redis-server
$ src/redis-cli

redis> set foo bar
OK

redis> get foo
"bar"

windows

解压后redis-2.6\bin\release里还有两个zip包,分别是32位和64位版。解压到redis 目录。

启动服务:

redis-server.exe redis.conf 

基本操作

查看系统版本信息:info

清除所有数据:flushall

批量删除指定的key:

redis-cli -h 10.8.89.242 -p 11221 -a p@ssw0rd KEYS "octopus:tasks:" | xargs redis-cli -h 10.8.89.242 -p 11221 -a p@ssw0rd DEL

Java工具

Jedis

Jedis是Redis的官方首选Java开发包:

<dependency> 
	<groupId>redis.clients</groupId> 
	<artifactId>jedis</artifactId> 
	<version>2.0.0</version> 
	<type>jar</type> 
	<scope>compile</scope> 
</dependency> 

测试调用:

import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.junit.Before;
import org.junit.Test;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

/**
 * @author: flychao88 Time: 2012.5.7 16:23:15
 */
public class RedisTest {

	JedisPool pool;
	Jedis jedis;

	@Before
	public void setUp() {
		pool = new JedisPool(new JedisPoolConfig(), "172.16.100.184");

		jedis = pool.getResource();
		jedis.auth("password");
	}

	/**
	 * Redis存储初级的字符串 CRUD
	 */
	@Test
	public void testBasicString() {
		// -----添加数据----------
		jedis.set("name", "minxr"); // 向key-->name中放入了value-->minxr
		System.out.println(jedis.get("name")); // 执行结果:minxr

		// -----修改数据-----------
		// 1、在原来基础上修改
		// 很直观,类似map 将jarorwar append到已经有的value之后
		jedis.append("name", "jarorwar");
		System.out.println(jedis.get("name")); // 执行结果:minxrjarorwar

		// 2、直接覆盖原来的数据
		jedis.set("name", "闵晓荣");
		System.out.println(jedis.get("name")); // 执行结果:闵晓荣

		// 删除key对应的记录
		jedis.del("name");
		System.out.println(jedis.get("name")); // 执行结果:null

		/**
		 * mset相当于 jedis.set("name","minxr"); jedis.set("jarorwar","闵晓荣");
		 */
		jedis.mset("name", "minxr", "jarorwar", "闵晓荣");
		System.out.println(jedis.mget("name", "jarorwar"));
	}

	/**
	 * jedis操作Map
	 */
	@Test
	public void testMap() {
		Map<String, String> user = new HashMap<String, String>();
		user.put("name", "minxr");
		user.put("pwd", "password");
		jedis.hmset("user", user);
		// 取出user中的name,执行结果:[minxr]-->注意结果是一个泛型的List
		// 第一个参数是存入redis中map对象的key,后面跟的是放入map中的对象的key,
		// 后面的key可以跟多个,是可变参数
		List<String> rsmap = jedis.hmget("user", "name");
		System.out.println(rsmap);

		// 删除map中的某个键值
		jedis.hdel("user", "pwd");
		// 因为删除了,所以返回的是null
		System.out.println(jedis.hmget("user", "pwd"));
		// 返回key为user的键中存放的值的个数1
		System.out.println(jedis.hlen("user"));
		// 是否存在key为user的记录 返回true
		System.out.println(jedis.exists("user"));
		// 返回map对象中的所有key [pwd, name]
		System.out.println(jedis.hkeys("user"));
		// 返回map对象中的所有value [minxr, password]
		System.out.println(jedis.hvals("user"));

		Iterator<String> iter = jedis.hkeys("user").iterator();
		while (iter.hasNext()) {
			String key = iter.next();
			System.out.println(key + ":" + jedis.hmget("user", key));
		}
	}

	/**
	 * jedis操作List
	 */
	@Test
	public void testList() {
		// 开始前,先移除所有的内容
		jedis.del("java framework");
		System.out.println(jedis.lrange("java framework", 0, -1));
		// 先向key java framework中存放三条数据
		jedis.lpush("java framework", "spring");
		jedis.lpush("java framework", "struts");
		jedis.lpush("java framework", "hibernate");
		// 再取出所有数据jedis.lrange是按范围取出,
		// 第一个是key,第二个是起始位置,第三个是结束位置,
		// jedis.llen获取长度 -1表示取得所有
		System.out.println(jedis.lrange("java framework", 0, -1));
	}

	/**
	 * jedis操作Set
	 */
	@Test
	public void testSet() {
		// 添加
		jedis.sadd("sname", "minxr");
		jedis.sadd("sname", "jarorwar");
		jedis.sadd("sname", "闵晓荣");
		jedis.sadd("sanme", "noname");
		// 移除noname
		jedis.srem("sname", "noname");
		// 获取所有加入的value
		System.out.println(jedis.smembers("sname"));
		// 判断 minxr 是否是sname集合的元素
		System.out.println(jedis.sismember("sname", "minxr"));
		System.out.println(jedis.srandmember("sname"));
		System.out.println(jedis.scard("sname")); // 返回集合的元素个数
	}

	@Test
	public void test() throws InterruptedException {
		// keys中传入的可以用通配符
		// 返回当前库中所有的key [sose, sanme, name, jarorwar, foo, sname, java
		// framework, user, braand]
		System.out.println(jedis.keys("*"));
		// 返回的sname [sname, name]
		System.out.println(jedis.keys("*name"));
		// 删除key为sanmdde的对象 删除成功返回1 删除失败(或者不存在)返回 0
		System.out.println(jedis.del("sanmdde"));
		// 返回给定key的有效时间,如果是-1则表示永远有效
		System.out.println(jedis.ttl("sname"));
		// 通过此方法,可以指定key的存活(有效时间) 时间为秒
		jedis.setex("timekey", 10, "min");
		// 睡眠5秒后,剩余时间将为<=5
		Thread.sleep(5000);
		// 输出结果为5
		System.out.println(jedis.ttl("timekey"));
		// 设为1后,下面再看剩余时间就是1了
		jedis.setex("timekey", 1, "min");
		// 输出结果为1
		System.out.println(jedis.ttl("timekey"));
		// 检查key是否存在
		System.out.println(jedis.exists("key"));
		System.out.println(jedis.get("timekey"));
		// 因为移除,返回为null
		System.out.println(jedis.rename("timekey", "time"));
		// 因为将timekey 重命名为time 所以可以取得值min
		System.out.println(jedis.get("time"));

		// jedis 排序
		// 注意,此处的rpush和lpush是List的操作。是一个双向链表(但从表现来看的)
		jedis.del("a"); // 先清除数据,再加入数据进行测试
		jedis.rpush("a", "1");
		jedis.lpush("a", "6");
		jedis.lpush("a", "3");
		jedis.lpush("a", "9");
		System.out.println(jedis.lrange("a", 0, -1)); // [9, 3, 6, 1]
		System.out.println(jedis.sort("a")); // [1, 3, 6, 9] //输入排序后结果
		System.out.println(jedis.lrange("a", 0, -1));
	}

}

还有一个详细的例子在:

http://www.importnew.com/19321.html

Java调用Lua脚本

Redis 使用lua的好处

  • 减少网络开销:本来5次网络请求的操作,可以用一个请求完成, 原先5次请求的逻辑放在redis服务器上完成。使用脚本,减少了网络往返时延。
  • 原子操作:Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。
  • 复用:客户端发送的脚本会永久存储在Redis中,意味着其他客户端可以复用这一脚本 而不需要使用代码完成同样的逻辑。

添加依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

编写Lua脚本:

local jobs_key_ht = KEYS[1];
local bucket_key_zset = KEYS[2];
local topic_id = ARGV[1];
local content = ARGV[2];
local score = ARGV[3];

--  HSET KEY_NAME FIELD VALUE
redis.call('HSET',jobs_key_ht,topic_id,content) ;

-- ZADD KEY_NAME SCORE1 VALUE1.. SCOREN VALUEN
redis.call('ZADD',bucket_key_zset,score,topic_id) ;

编写代码执行lua脚本:

DefaultRedisScript redisScript = new DefaultRedisScript<>();
redisScript.setScriptSource(
		new ResourceScriptSource(new ClassPathResource("lua/xxx.lua")));
Boolean flag = redisTemplate.execute(
		redisScript, keys, RedisKeyUtil.getTopicId(arg), arg, runTimeMillis);
// 执行会返回true 或者false

持久化

Redis支持RDB与AOF两种持久化机制,持久化可以避免因进程异常退出或down机导致的数据丢失问题,在下次重启时能利用之前的持久化文件实现数据恢复。

Redis提供了两种持久化方式:1 RDB快照方式 2 AOF方式

RDB方式:满足一定条件时,会创建一个子进程,复制当前的数据,把数据写入到硬盘中某个文件,写入完成后替换原来的存储文件。数据一般存储在dump.rdb中。UNIX系统中支持写时复制,即刚开始会执行持久化写入磁盘的操作,如果此时有其他的数据发生改变,就复制一份数据执行。

除了这种自动的快照方式,还支持命令方式持久化:

  • SAVE:通过阻塞的方式,用父进程来持久化,此时无法执行其他的请求。
  • BGSAVE:通过fork子进程的方式,持久化。

AOF方式:每次操作都会记录命令,这样会造成某些命令的冗余,比如添加了一个属性,再删除,那么这两个操作都是冗余的。redis提供了一些优化,所以可以避免这些冗余信息。命令记录在appendonly.aof中

RDB持久化

RDB持久化即通过创建快照(压缩的二进制文件)的方式进行持久化,保存某个时间点的全量数据。RDB持久化是Redis默认的持久化方式。RDB持久化的触发包括手动触发与自动触发两种方式。

手动触发

  • save, 在命令行执行save命令,将以同步的方式创建rdb文件保存快照,会阻塞服务器的主进程,生产环境中不要用
  • bgsave, 在命令行执行bgsave命令,将通过fork一个子进程以异步的方式创建rdb文件保存快照,除了fork时有阻塞,子进程在创建rdb文件时,主进程可继续处理请求

自动触发

  • redis.conf中配置save m n定时触发,如save 900 1表示在900s内至少存在一次更新就触发
  • 主从复制时,如果从节点执行全量复制操作,主节点自动执行bgsave生成RDB文件并发送给从节点
  • 执行debug reload命令重新加载Redis时
  • 执行shutdown且没有开启AOF持久化

redis.conf中RDB持久化配置


# 只要满足下列条件之一,则会执行bgsave命令
save 900 1 # 在900s内存在至少一次写操作
save 300 10
save 60 10000
# 禁用RBD持久化,可在最后加 save ""

# 当备份进程出错时主进程是否停止写入操作
stop-writes-on-bgsave-error yes  
# 是否压缩rdb文件 推荐no 相对于硬盘成本cpu资源更贵
rdbcompression no

AOF持久化

AOF(Append-Only-File)持久化即记录所有变更数据库状态的指令,以append的形式追加保存到AOF文件中。在服务器下次启动时,就可以通过载入和执行AOF文件中保存的命令,来还原服务器关闭前的数据库状态。

redis.conf中AOF持久化配置如下


# 默认关闭AOF,若要开启将no改为yes
appendonly no

# append文件的名字
appendfilename "appendonly.aof"

# 每隔一秒将缓存区内容写入文件 默认开启的写入方式
appendfsync everysec 

# 当AOF文件大小的增长率大于该配置项时自动开启重写(这里指超过原大小的100%)。
auto-aof-rewrite-percentage 100

# 当AOF文件大小大于该配置项时自动开启重写
auto-aof-rewrite-min-size 64mb

AOF持久化的实现包括3个步骤:

  • 命令追加:将命令追加到AOF缓冲区
  • 文件写入:缓冲区内容写到AOF文件
  • 文件保存:AOF文件保存到磁盘

其中后两步的频率通过appendfsync来配置,appendfsync的选项包括

  • always, 每执行一个命令就保存一次,安全性最高,最多只丢失一个命令的数据,但是性能也最低(频繁的磁盘IO)
  • everysec,每一秒保存一次,推荐使用,在安全性与性能之间折中,最多丢失一秒的数据
  • no, 依赖操作系统来执行(一般大概30s一次的样子),安全性最低,性能最高,丢失操作系统最后一次对AOF文件触发SAVE操作之后的数据

AOF通过保存命令来持久化,随着时间的推移,AOF文件会越来越大,Redis通过AOF文件重写来解决AOF文件不断增大的问题(可以减少文件的磁盘占有量,加快数据恢复的速度),原理如下:

  • 调用fork,创建一个子进程
  • 子进程读取当前数据库的状态来“重写”一个新的AOF文件(这里虽然叫“重写”,但实际并没有对旧文件进行任何读取,而是根据数据库的当前状态来形成指令)
  • 主进程持续将新的变动同时写到AOF重写缓冲区与原来的AOF缓冲区中
  • 主进程获取到子进程重写AOF完成的信号,调用信号处理函数将AOF重写缓冲区内容写入新的AOF文件中,并对新文件进行重命名,原子地覆盖原有AOF文件,完成新旧文件的替换

AOF的重写也分为手动触发与自动触发

  • 手动触发: 直接调用bgrewriteaof命令
  • 自动触发: 根据auto-aof-rewrite-min-size和auto-aof-rewrite-percentage参数确定自动触发时机。其中auto-aof-rewrite-min-size表示运行AOF重写时文件最小体积,默认为64MB。auto-aof-rewrite-percentage表示当前AOF文件大小(aof_current_size)和上一次重写后AOF文件大小(aof_base_size)的比值。自动触发时机为 aof_current_size > auto-aof-rewrite-min-size &&(aof_current_size - aof_base_size)/aof_base_size> = auto-aof-rewrite-percentage

RDB vs AOF

RDB与AOF两种方式各有优缺点。

RDB的优点:与AOF相比,RDB文件相对较小,恢复数据比较快(原因见数据恢复部分) RDB的缺点:服务器宕机,RBD方式会丢失掉上一次RDB持久化后的数据;使用bgsave fork子进程时会耗费内存。

AOF的优点: AOF只是追加文件,对服务器性能影响较小,速度比RDB快,消耗内存也少,同时可读性高。 AOF的缺点:生成的文件相对较大,即使通过AOF重写,仍然会比较大;恢复数据的速度比RDB慢。

数据库的恢复

服务器启动时,如果没有开启AOF持久化功能,则会自动载入RDB文件,期间会阻塞主进程。如果开启了AOF持久化功能,服务器则会优先使用AOF文件来还原数据库状态,因为AOF文件的更新频率通常比RDB文件的更新频率高,保存的数据更完整。

在数据恢复方面,RDB的启动时间会更短,原因有两个:

  • RDB 文件中每一条数据只有一条记录,不会像AOF日志那样可能有一条数据的多次操作记录。所以每条数据只需要写一次就行了,文件相对较小。
  • RDB 文件的存储格式和Redis数据在内存中的编码格式是一致的,不需要再进行数据编码工作,所以在CPU消耗上要远小于AOF日志的加载。

但是在进行RDB持久化时,fork出来进行dump操作的子进程会占用与父进程一样的内存,采用的copy-on-write机制,对性能的影响和内存的消耗都是比较大的。比如16G内存,Redis已经使用了10G,这时save的话会再生成10G,变成20G,大于系统的16G。这时候会发生交换,要是虚拟内存不够则会崩溃,导致数据丢失。所以在用redis的时候一定对系统内存做好容量规划。

RDB、AOF混合持久化

Redis从4.0版开始支持RDB与AOF的混合持久化方案。首先由RDB定期完成内存快照的备份,然后再由AOF完成两次RDB之间的数据备份,由这两部分共同构成持久化文件。该方案的优点是充分利用了RDB加载快、备份文件小及AOF尽可能不丢数据的特性。缺点是兼容性差,一旦开启了混合持久化,在4.0之前的版本都不识别该持久化文件,同时由于前部分是RDB格式,阅读性较低。

开启混合持久化

aof-use-rdb-preamble yes

数据恢复加载过程就是先按照RDB进行加载,然后把AOF命令追加写入。

持久化方案的建议

  • 如果Redis只是用来做缓存服务器,比如数据库查询数据后缓存,那可以不用考虑持久化,因为缓存服务失效还能再从数据库获取恢复。
  • 如果你要想提供很高的数据保障性,那么建议你同时使用两种持久化方式。如果你可以接受灾难带来的几分钟的数据丢失,那么可以仅使用RDB。
  • 通常的设计思路是利用主从复制机制来弥补持久化时性能上的影响。即Master上RDB、AOF都不做,保证Master的读写性能,而Slave上则同时开启RDB和AOF(或4.0以上版本的混合持久化方式)来进行持久化,保证数据的安全性。

主从复制

可以通过SLAVEOF命令或者slaveof选项,让一个服务器去复制另一个服务器。

  • Redis 使用异步复制,slave 和 master 之间异步地确认处理的数据量。
  • 一个 master 可以拥有多个 slave。
  • slave 可以接受其他 slave 的连接。除了多个 slave 可以连接到同一个 master 之外, slave 之间也可以像层叠状的结构(cascading-like structure)连接到其他 slave 。 自 Redis 4.0 起,所有的 sub-slave 将会从 master 收到完全一样的复制流。
  • Redis 复制在 master 侧是非阻塞的。这意味着 master 在一个或多个 slave 进行初次同步或者是部分重同步时,可以继续处理查询请求。
  • 复制在 slave 侧大部分也是非阻塞的。当然这个是可配的,如果在redis.conf 配置是非阻塞的,可以使用旧数据集处理查询请求;如果配置的是阻塞的,slave 会返回一个 error 给客户端。

建立主从复制

  • 主服务器:127.0.0.1:6379
  • 从服务器:127.0.0.1:12345

在从服务器127.0.0.1:12345执行:

127.0.0.1:12345> SLAVEOF 127.0.0.1 6379
OK

就能成功建立主从关系,主从服务器的数据会保持一致 比如主服务器存储数据:

127.0.0.1:6379> set msg "hello world"
OK

然后从服务器就能直接获取数据:

127.0.0.1:12345>get msg
"hello world"

删除数据也是一样,主从会保持一致。

主从复制原理

首先,Redis 的复制分为同步(sync)和命令传播(command propagate)两个操作:

  • 同步操作用于将从服务器数据库的状态更新为主服务器所处的状态。
  • 命令传播则相反,它主要作用在主服务器的数据库状态更改时, 导致主从服务器的数据库状态出现不一致时,让主从回到一致的的过程。

接下来详细说说这两种复制。

用同步方式复制

文字解说:

  • 客户端向从服务器发送SLAVEOF命令,先是判断是否是第一次复制, 第一次是复制一般是刚开始组建主从关系。
  • 是第一次复制:从服务器会向主服务器发送PSYNC ? -1命令, 请求主服务器执行「完整重同步」操作。
  • 主服务器接到「完整重同步」请求之后,将在后台执行BGSAVE命令, 在后台生成一个.RDB文件, 并使用一个「复制积压缓冲区」记录从现在开始执行的所有写命令。
  • BGSAVE命令执行完毕之后,主服务器会将.RDB文件以及缓冲区中记录的写命令 发送给从服务器,还会向从服务器返回+FULLRESYNC [主服务器ID] [复制偏移量] (和图中的 偏移量 是一个)。
  • 从服务器接收到后,会载入.RDB文件,并执行主服务器给的写命令, 以此来达到和主服务器一致的数据状态。
  • 如果不是第一次复制,那么说明从服务器可能是断线,导致和主服务器数据状态不一致, 需要同步主服务器的数据。那么从服务器会按照下面的步骤来请求部分同步。
  • 向主服务器发送PSYNC [主服务器ID] [复制偏移量]
    • 主服务器ID是断线前的主服务器,用于定位去同步那个主服务器的。
    • 偏移量是第一次复制时主服务器传过来的,是上一次同步的位置, 用于定位具体的同步位置的。
  • 主服务器接收到从服务器的命令后,并找到相应同步的位置后,会给从服务器发送 +CONTINUE命令,表示将于从服务器执行部分同步操作,之后主服务器会将保存在 「复制积压缓冲区」对应「复制偏移量」之后的所有数据发送给从服务器, 但是如果找不到偏移量之后的数据,就会进行「完整同步」, 这样就可以让从服务器达到和主服务器一致的状态。

命令传播

主从服务器同步成功后,并不会一致保持这个状态,主服务器可能会执行写命令, 这也主从数据就不知一致了。

为了处理这种问题,主服务器会把自己执行的写命令发送给从服务器, 当从服务器执行完这些命令之后,主从服务器的数据就一致了。

在命令传播阶段,从服务器默认会以每秒一次的频率,向主服务器发送命令:

REPLCONF ACK <replication_offset>

<replication_offset>是从服务器当前的复制偏移量。

发送REPLCONF ACK命令对于主从服务器有三个作用:

  • 检测主从服务器的网络状态。
  • 辅助实现min-slaves选项。
  • 检测命令丢失。

关键词讲解

  • 主服务器ID:用于标识一个服务器。
  • 每个服务器,无论是主服务器还是从服务器都有属于自己独一无二的服务器ID。
  • ID在服务器启动时生成,由40个随机的十六进制字符组成。
  • 复制积压缓冲区:复制积压缓冲区是由主服务器维护的一个固定长度、 先进先出(FIFO)队列,默认大小为 1MB。如下:
偏移量 ... 10086 10087 10088 10089 ...  
字节值 ... 3 '\r' '\n' '$' ...