Redis持久化
Redis 是一个内存数据库,内存数据库的读写效率比传统数据库要快的多,但是保存在内存中也随之带来了一个缺点,一旦断电或者宕机,那么内存数据库中的数据将会全部丢失。Redis持久化是指在指定的时间间隔内将内存中的数据集快照(snapshotting)写入磁盘,恢复时将快照文件读入内存,Redis提供了两种持久化方式:
RDB快照(snapshot)
在默认情况下,Redis 将内存数据库快照保存在名字为dump.rdb的二进制文件中。可以redis.conf中进行设置save 60 1000 , 让它在“60秒内数据集有1000个改动时”这一条件被满足时,自动保存一次数据集。还可以手动执行命令生成RDB快照,进入redis客户端执行命令save或bgsave可以生成dump.rdb文件,每次命令执行都会将所有redis内存快照到一个新的rdb文件里,并覆盖原有rdb快照文件,自动生成rdb文件后台使用的是bgsave方式。
bgsave写时复制(COW)机制
Redis通过操作系统提供的写时复制技术(Copy-On-Write),在生成快照的同时,依然可以正常处理写命令。也就是说,bgsave命令执行时主线程会fork出一个子进程,这个子进程可以共享主线程的所有内存数据。子进程运行后,开始读取主线程的内存数据,并把它们写入一个RDB副本文件。如果主线程对这些数据也都是读操作,那么,主线程和 bgsave 子进程相互不影响。但如果主线程要修改了数据,会把数据写入到内存,同时也会把修改的数据写入到副本RDB中。
save与bgsave对比:
AOF(append-only file)
快照功能并不完美, 如果 Redis 因为某些原因而造成故障停机, 那么服务器将丢失最近写入、且仍未保存到快照中的那些数据。从1.1版本开始, Redis增加了一种持久化方式 :AOF 持久化,将修改的每一条指令记录进文件appendonly.aof中(先写入os cache,每隔一段时间fsync到磁盘),这样的话 当Redis重启时, 就可以通过重新执行AOF文件中的命令来重建数据集。
AOF重写:
AOF文件里可能有太多没用指令,比如incr自加命令,其实我们只需要存储key的最终值就好,这些多余的incr多余的命令就可以删除,所以AOF会定期根据内存的最新数据生成aof文件。还可以手动重写,进入redis客户端执行命令bgrewriteaof重写AOF,AOF重写会fork出一个子进程去做(与bgsave命令类似),不会对redis正常命令处理有太多影响。
redis.conf配置:
//RDB:
//设置RDB保存策略,关闭RDB只需要将所有的save保存策略注释掉即可
save 900 1
save 300 10
save 60 10000
//指定RDB文件名
dbfilename dump.rdb
//配置后台启动
daemonize yes
//指定RDB、AOF文件输出路径
dir /usr/local/redis-5.0.3/data
//AOF:
//打开AOF功能
appendonly yes
//指定AOF文件名称
appendfilename "appendonly.aof"
//控制多久将数据fsync到磁盘一次,推荐(并且也是默认)的措施为每秒fsync一次
appendfsync always://每次有新命令追加到AOF文件时就执行一次fsync,非常慢,也非常安全。
appendfsync everysec://每秒fsync一次,足够快,并且在故障时只会丢失1秒钟的数据。
appendfsync no://从不fsync,将数据交给操作系统来处理。更快,也更不安全的选择。
//控制AOF自动重写频率
# auto-aof-rewrite-min-size 64mb //aof文件至少要达到64M才会自动重写,文件太小恢复速度本来就很快,重写的意义不大
# auto-aof-rewrite-percentage 100 //aof文件自上一次重写后文件大小增长了100%则再次触发重写
RDB & AOF比较:
Redis启动时如果既有RDB文件又有AOF文件则优先选择AOF文件恢复数据,因为AOF一般来说数据更全一点。
Redis 4.0 混合持久化
重启Redis时,很少使用 RDB来恢复内存状态,因为会丢失大量数据。所以通常使用AOF日志重放,但是重放AOF日志性能相对 RDB来说要慢很多,这样在 Redis 实例很大的情况下,启动需要花费很长的时间。 Redis 4.0 为了解决这个问题,新增了混合持久化。
通过如下配置可以开启混合持久化(必须先开启aof):
aof-use-rdb-preamble yes
如果开启了混合持久化,AOF在重写时,不再是单纯将内存数据写入AOF文件,而是在重写之前的内存做RDB快照处理,并且将RDB快照内容和增量的AOF修改内存数据的命令存在一起,都写入新的AOF文件,等到重写完新的AOF文件会把文件名改名成appendonly.aof,覆盖原有的appendonly.aof文件,完成新旧两个AOF文件的替换。所以Redis重启时,可以先加载RDB的内容,然后再重放增量AOF日志就可以完全替代之前的AOF全量文件重放。
Redis数据备份策略:
可以写crontab定时调度脚本:
1、每小时都copy一份rdb或aof的备份到一个目录中去,仅仅保留最近48小时的备份
2、每天都保留一份当日的数据备份到一个目录中去,可以保留最近1个月的备份
3、每次copy备份的时候,把太旧的备份删除
4、每天晚上将当前机器上的备份复制一份到其他机器上,以防机器损坏
Redis主从架构
主从架构搭建步骤:
1、复制一份redis.conf文件:
cp redis.conf redis-6380.conf
2、修改复制文件中的相关配置:
port 6380 # 修改端口号
pidfile /var/run/redis_6380.pid # 把pid进程号写入pidfile配置的文件
logfile "6380.log" # 修改日志文件
dir /usr/local/redis-5.0.3/data/6380 # 指定数据存放目录
#bind 127.0.0.1 # 有多块网卡可以配多个ip,如果是内网环境可以注释掉bind
3、配置主从复制:
replicaof 127.0.0.1 6379 # 从192.168.0.60机器的6379的redis实例复制数据
replica-read-only yes # 配置从节点只读
4、启动从节点:
redis-server redis-6380.conf
5、连接从节点:
redis-cli -p 6380
Redis主从工作原理:
1、给master配置一个slave,不管这个slave是否是第一次连接上Master,它都会发送一个PSYNC命令给master请求复制数据。
2、master收到PSYNC命令后,会在后台进行数据持久化,通过bgsave生成最新的rdb快照文件,持久化期间,master会继续接收客户端的请求,它会把这些可能修改数据集的请求缓存在内存中。
3、当持久化进行完毕以后,master会把这份rdb文件数据集发送给slave,slave会把接收到的数据进行持久化生成rdb,然后再加载到内存中。然后,master再将之前缓存在内存中的命令发送给slave。
4、当master与slave之间的连接由于某些原因而断开时,slave能够自动重连Master,如果master收到了多个slave并发连接请求,它只会进行一次持久化,而不是一个连接一次,然后再把这一份持久化的数据发送给多个并发连接的slave。
主从复制(全量复制)流程:
数据部分复制:
当master和slave断开重连后,一般都会对整份数据进行复制。但从redis2.8版本开始,redis改用可以支持部分数据复制的命令psync去master同步数据,slave与master能够在网络连接断开重连后只进行部分数据复制(断点续传)。master会在其内存中创建一个复制数据用的缓存队列,缓存最近一段时间的数据,master和它所有的slave都维护了复制的数据下标offset和master的进程id,因此,当网络连接断开后,slave会请求master继续从所记录的数据下标开始,进行未完成的复制。如果master进程id变化了,或者从节点数据下标offset太旧,已经不在master的缓存队列里了,那么将会进行一次全量数据的复制。
主从复制(部分复制,断点续传)流程:
如果有很多从节点,可能多个从节点同时复制主节点导致主节点压力过大导致主从复制风暴,针对这种情况可以让部分从节点与从节点同步数据。
Redis哨兵高可用架构
哨兵sentinel是特殊的redis服务,不提供读写服务,主要用来监控redis实例节点。
哨兵架构下client端第一次连接会从哨兵中找出redis的主节点,后续会直接访问redis的主节点,不会每次都通过sentinel代理访问redis的主节点,当redis的主节点发生变化,哨兵会第一时间感知到,并且将新的redis主节点通知给client端(redis的client端需要实现订阅功能,订阅sentinel发布的节点变动消息),如果哨兵集群挂了,整个redis集群也会瘫痪。
redis哨兵架构搭建步骤:
1、复制一份sentinel.conf文件:
cp sentinel.conf sentinel-26379.conf
2、修改sentinel-26379.conf配置文件:
port 26379
daemonize yes #配置后台启动
pidfile /var/run/redis-sentinel-26379.pid # 不需要创建该文件,文件会自动生成
logfile 26379.log # 不需要创建该文件,文件会自动生成
dir /usr/local/redis-5.0.3/data # data目录是需要手动创建的
# sentinel monitor <master-redis-name> <master-redis-ip> <master-redis-port> <quorum>
# mymaster是<master-redis-name>,可以随意,客户端访问时会用到
# 2是一个quorum,表示当有2个哨兵认为一个master失效时,master才算真正失效(值一般为:sentinel总数/2 + 1)
sentinel monitor mymaster 192.168.0.60 6379 2
3、启动sentinel哨兵实例
redis-sentinel sentinel-26379.conf
4、查看sentinel的info信息
redis-cli -p 26379
执行info可以看到Sentinel的info里已经识别出了redis的主从
5、再配置两个sentinel,端口26380和26381,注意上述配置文件里的对应数字都要修改
复制命令: cp sentinel.conf sentinel-26380.conf
复制命令: cp sentinel.conf sentinel-26381.conf
如果有sentinel myid这一行删掉
sentinel集群都启动完毕后,会将哨兵集群的数据信息写入所有sentinel的配置文件里去(追加在文件的最下面),查看下如下配置文件sentinel-26379.conf,如下所示:
sentinel known-replica mymaster 192.168.0.60 6380 #代表redis主节点的从节点信息
sentinel known-replica mymaster 192.168.0.60 6381 #代表redis主节点的从节点信息
sentinel known-sentinel mymaster 192.168.0.60 26380 52d0a5d70c1f90475b4fc03b6ce7c3c56935760f #代表感知到的其它哨兵节点
sentinel known-sentinel mymaster 192.168.0.60 26381 e9f530d3882f8043f76ebb8e1686438ba8bd5ca6 #代表感知到的其它哨兵节点
redis主节点如果挂了,哨兵集群会重新选举出新的redis主节点,同时会修改所有sentinel节点配置文件的集群元数据信息,比如6379的redis如果挂了,假设选举出的新主节点是6380,此时哨兵集群的数据信息如下所示:
sentinel known-replica mymaster 192.168.0.60 6379 #代表主节点的从节点信息
sentinel known-replica mymaster 192.168.0.60 6381 #代表主节点的从节点信息
sentinel known-sentinel mymaster 192.168.0.60 26380 52d0a5d70c1f90475b4fc03b6ce7c3c56935760f #代表感知到的其它哨兵节点
sentinel known-sentinel mymaster 192.168.0.60 26381 e9f530d3882f8043f76ebb8e1686438ba8bd5ca6 #代表感知到的其它哨兵节点
同时还会修改sentinel文件里之前配置的mymaster对应的6379端口,改为6380
sentinel monitor mymaster 192.168.0.60 6380 2
当6379的redis实例再次启动时,哨兵集群根据集群数据信息就可以将6379端口的redis节点作为从节点加入集群
哨兵leader选举流程:
1、每个Sentinel以每秒钟一次的频率向它所知的Master/Slave以及其他 Sentinel 实例发送一个 PING命令,缺认是否存活
2、如果一个实例距离最后一次有效回复 PING 命令的时间超过 down-aftermilliseconds 选项所指定的值, 则这个实例会被这个Sentinel标记为主观下线。
3、如果多个哨兵(大于等于配置文件指定的值:quorum)都标记该master已下线,则此时把这个master标记为客观下线。
4、哨兵间会协商选出哨兵的leader进行故障转移工作,每个发现master进入下线的哨兵都可以要求其他哨兵选自己为leader,选举是先到先得。
5、每个哨兵每次选举都会自增选举周期,每个周期中只会选一个哨兵作为leader。如果所有超过一半的哨兵选举某哨兵作为leader。就由该哨兵进行故障转移操作,从存活的slave中选举出新的master,选举过程跟集群的master选举很类似。
6、哨兵集群只有一个哨兵节点时,redis的主从可以正常运行以及选举master,如果master挂了,那唯一的那个哨兵节点就是哨兵leader了,也可以正常选举新master。为了高可用一般也是至少部署三个哨兵节点。
SpringBoot集成redisTemplate连接哨兵代码示例:
引入redis依赖包:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
application.yml中加入redis相关配置:
server:
port: 8080
spring:
redis:
database: 0
timeout: 3000 #超时时间
sentinel: #哨兵模式
master: mymaster #主服务器所在集群名称
nodes: 192.168.0.60:26379,192.168.0.60:26380,192.168.0.60:26381
lettuce:
pool:
max-idle: 50
min-idle: 10
max-active: 100
max-wait: 1000
测试哨兵连接Redis代码:
public class JedisSentinelTest {
public static void main(String[] args) throws IOException {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(20);
config.setMaxIdle(10);
config.setMinIdle(5);
String masterName = "mymaster";
Set<String> sentinels = new HashSet<String>();
sentinels.add(new HostAndPort("192.168.0.60",26379).toString());
sentinels.add(new HostAndPort("192.168.0.60",26380).toString());
sentinels.add(new HostAndPort("192.168.0.60",26381).toString());
JedisSentinelPool jedisSentinelPool = new JedisSentinelPool(masterName, sentinels, config, 3000, null);
Jedis jedis = null;
try {
jedis = jedisSentinelPool.getResource();
System.out.println(jedis.set("sentinel", "zhuge"));
System.out.println(jedis.get("sentinel"));
} catch (Exception e) {
e.printStackTrace();
} finally {
//注意这里不是关闭连接,在JedisPool模式下,Jedis会被归还给资源池。
if (jedis != null)
jedis.close();
}
}
}
测试master选举代码:
@RestController
public class IndexController {
private static final Logger logger = LoggerFactory.getLogger(IndexController.class);
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 测试节点挂了哨兵重新选举新的master节点,客户端是否能动态感知到
* 新的master选举出来后,哨兵会把消息发布出去,客户端实际上是实现了一个消息监听机制,
* 当哨兵把新master的消息发布出去,客户端会立马感知到新master的信息,从而动态切换访问的masterip
*
* @throws InterruptedException
*/
@RequestMapping("/sentinel_test")
public void testSentinel() throws InterruptedException {
int i = 1;
while (true){
try {
stringRedisTemplate.opsForValue().set("zhangsan"+i, i+"");
System.out.println("设置key:"+ "zhangsan" + i);
i++;
Thread.sleep(1000);
}catch (Exception e){
logger.error("错误:", e);
}
}
}
}
Jedis
springboot集成jedis:
1、引入依赖包:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
2、application.properties中加入jedis相关配置
spring.redis.host=127.0.0.1
spring.redis.database=0
spring.redis.port=6379
spring.redis.password=
spring.redis.timeout=10
spring.redis.jedis.pool.max-active=10
spring.redis.jedis.pool.max-idle=10
spring.redis.jedis.pool.min-idle=10
spring.redis.jedis.pool.max-wait=10000
Redis管道(Pipeline)
管道可以使客户端可以一次性发送多个请求而不用等Redis的响应,等所有命令都发送完后再一次性读取Redis的响应,这样可以极大的降低多条命令执行的网络传输开销,管道执行多条命令的网络开销实际上只相当于一次命令执行的网络开销。是用pipeline方式打包命令发送,Redis会在处理完所有命令前先缓存起已执行完命令的处理结果。打包的命令越多,缓存消耗内存也越多。所以并不是打包的命令越多越好。
pipeline中发送的每个command都会被server立即执行,如果执行失败,将会在此后的响应中得到信息,不会影响后面的命令,继续执行。
jedis操作管道代码示例:
public class JedisTest {
public static void main(String[] args) throws IOException {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(20);
jedisPoolConfig.setMaxIdle(10);
jedisPoolConfig.setMinIdle(5);
JedisPool jedisPool = new JedisPool(jedisPoolConfig, "192.168.0.60", 6379, 3000, null);
Jedis jedis = null;
try {
//从redis连接池里拿出一个连接执行命令
jedis = jedisPool.getResource();
//管道的命令执行方式:cat redis.txt | redis-cli -h 127.0.0.1 -a password - p 6379 --pipe
//管道代码示例
Pipeline pl = jedis.pipelined();
for (int i = 0; i < 10; i++) {
pl.incr("pipelineKey");
pl.set("zhangsan" + i, "zhangsan");
}
List<Object> results = pl.syncAndReturnAll();
System.out.println(results);
} catch (Exception e) {
e.printStackTrace();
} finally {
//这里不是关闭连接,在JedisPool模式下,Jedis会被归还给资源池。
if (jedis != null)
jedis.close();
}
}
}
Redis Lua脚本
Redis在2.6推出了脚本功能,允许开发者使用Lua语言编写脚本传到Redis中执行。使用脚本的好处如下:
1、减少网络开销:本来5次网络请求的操作,可以用一个请求完成,原先5次请求的逻辑放在redis服务器上完成。使用脚本,减少了网络往返时延,和管道类似。
2、原子操作:Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入,也就是说会阻塞其他线程的命令。管道不是原子的,不过redis的批量操作命令(类似mset)是原子的。
3、替代redis的事务功能:redis自带的事务功能很鸡肋,而redis的lua脚本几乎实现了常规的事务功能,如果要使用redis的事务功能可以用redis lua替代。
客户端命令执行lua脚本示例:
eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
其中 “return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}” 是被求值的Lua脚本,数字2表示键的数量, key1和key2是键,分别使用 KEYS[1] 和 KEYS[2] 访问,而最后的 first 和 second 则是附加参数,可以通过 ARGV[1] 和 ARGV[2] 访问它们。
jedis代码操作lua脚本代码示例:
public class JedisTest {
public static void main(String[] args) throws IOException {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(20);
jedisPoolConfig.setMaxIdle(10);
jedisPoolConfig.setMinIdle(5);
JedisPool jedisPool = new JedisPool(jedisPoolConfig, "192.168.0.60", 6379, 3000, null);
Jedis jedis = null;
try {
//从redis连接池里拿出一个连接执行命令
jedis = jedisPool.getResource();
//lua脚本命令执行方式:redis-cli --eval /tmp/test.lua , 10
//lua脚本模拟一个商品减库存的原子操作
jedis.set("product_count_123", "15"); //初始化商品1234的库存
//get命令取出product_count_123的值,也就是15
String script = " local count = redis.call('get', KEYS[1]) " +
//通过tonumber转换库存为数字
" local a = tonumber(count) " +
//通过tonumber转换扣减数量为数字
" local b = tonumber(ARGV[1]) " +
//判断剩余库存是否大雨扣减库存
" if a >= b then " +
//a-b得到扣减后的库存,然后set进product_count_123中
" redis.call('set', KEYS[1], a-b) " +
//语法错误模拟异常,事物回滚
//" bb == 0 " +
//设置成功返回1
" return 1 " +
" end " +
//库存不够返回0
" return 0 ";
//script:lua脚本
//Arrays.asList("product_count_123"):把product_count_123转换成数组参数
//Arrays.asList("10"):把扣减数量10转换为数组参数
Object obj = jedis.eval(script, Arrays.asList("product_count_123"), Arrays.asList("10"));
System.out.println(obj);
} catch (Exception e) {
e.printStackTrace();
} finally {
//这里不是关闭连接,在JedisPool模式下,Jedis会被归还给资源池。
if (jedis != null)
jedis.close();
}
}
}
Lua脚本中不能出现死循环和耗时的运算,否则redis会阻塞,将不接受其他的命令, 所以使用时要注意不能出现死循环、耗时的运算。redis是单进程、单线程执行脚本。管道不会阻塞redis。
RedisTemplate
spring 封装了 RedisTemplate 对象来进行对redis的各种操作,支持所有的 redis 原生api,RedisTemplate中定义了对5种数据结构操作:
redisTemplate.opsForValue();//操作字符串
redisTemplate.opsForHash();//操作hash
redisTemplate.opsForList();//操作list
redisTemplate.opsForSet();//操作set
redisTemplate.opsForZSet();//操作有序set
StringRedisTemplate继承自RedisTemplate,也一样拥有上面这些操作。StringRedisTemplate默认采用的是String的序列化策略保存K-V,RedisTemplate默认采用的是JDK的序列化策略保存K-V。
springboot集成redisTemplate
1、pom文件中加入redis依赖包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2、application.properties中加入redis相关配置
spring.redis.host=127.0.0.1
spring.redis.database=0
spring.redis.port=6379
spring.redis.password=password
3、在需要使用的类中注入template对象并使用
@Autowired
public RedisTemplate redisTemplate;
redisTemplate.opsForValue().set("test", "test");
Redis客户端命令对应的RedisTemplate中的方法:
String结构:
Hash结构:
List结构:
Set结构: