Redis Sentinel的分布式特性介绍
Redis Sentinel是一个分布式系统,Sentinel运行在有许多Sentinel进程互相合作的环境下,它本身就是这样被设计的。有许多Sentinel进程互相合作的优点如下:
- 当多个Sentinel同意一个master不再可用的时候,就执行故障检测。这明显降低了错误概率。
- 即使并非全部的Sentinel都在工作,Sentinel也可以正常工作,这种特性,让系统非常的健康。
所有的Sentinels,Redis实例,连接到Sentinel和Redis的客户端,本身就是一个有着特殊性质的大型分布式系统。在这篇文章中,我将通过实例的形式从部署Redis Sentinel集群、使用Redis的Master Slave的模式部署Redis集群来介绍在Spring项目中如何使用Redis Sentinel实现缓存系统的高可用。
部署之前了解关于Sentinel的基本东西
- 一个健康的集群部署,至少需要三个Sentinel实例
- 三个Sentinel实例应该被放在失败独立的电脑上或虚拟机中,比如说不同的物理机或者在不同的可用区域上执行的虚拟机。
- Sentinel + Redis 分布式系统在失败期间并不确保写入请求被保存,因为Redis使用异步复制。可是有很多部署Sentinel的 方式来让窗口把丢失写入限制在特定的时刻,当然也有另外的不安全的方式来部署。
- 客户端必须支持Sentinel。大多数客户端库都支持Sentinel,但并不是全部。
- 没有高可用的设置是安全的,如果你在你的测试环境没有经常去测试,或者甚至在生产环境中你也没有经常去测试,如果Sentinel正常工作。 但是你或许有一个错误的配置而仅仅只是在很晚的时候才出现(凌晨3点你的主节点宕掉了)。
- Sentinel,Docker ,其他的网络地址转换表,端口映射应该很小心的使用:Docker执行端口重新映射,破坏Sentinel自动发现另外的Sentinel进程和一个主节点的从节点列表。在文章的稍后部分查看更过关于Sentinel和Docker的信息。
实例介绍
- 本实例通过部署3个Redis Sentinel实现Sentinel实例的高可用。
- 本实例通过部署1个Redis Master实例,3个Slave实例实现Redis缓存的高可用。
Redis Master实例和Slave实例部署
下载并安装Redis,此文不讲解,具体可参考http://blog.csdn.net/zyhlwzy/article/details/78366265。
可参考Redis主从(Master-Slave)复制(Replication)设置一文第8节Redis主从复制实战的部署方式部署Master-Slave,但是需要注意的是,本文需要的是一个Master实例,3个Slave实例。
Redis实例信息如下:
IP | Port | 备注 |
---|---|---|
192.168.199.126 | 6379 | Master |
192.168.199.249 | 6382 | Slave |
192.168.199.249 | 6383 | Slave |
192.168.199.249 | 6384 | Slave |
配置Master
下载并安装Redis,此文不讲解,具体可参考http://blog.csdn.net/zyhlwzy/article/details/78366265。
Master的配置很简单,我们开启守护进程即可(演示实例不设置验证信息,如有需要自行设置)。
进入redis配置文件目录(/data/redis/redis-4.0.1/redis.conf)编辑redis.conf进行如下配置。
daemonize yes
配置Slave
下载并安装Redis,此文不讲解,具体可参考http://blog.csdn.net/zyhlwzy/article/details/78366265。
创建Slave配置文件目录,拷贝redis.conf到对应目录并进行配置。
mkdir /data/redis/cluster
cd /data/redis/cluster
mkdir -p 6382
cp /data/redis/redis-4.0.1/redis.conf /data/redis/cluster/6382/redis-6382.conf
mkdir -p 6383
cp /data/redis/redis-4.0.1/redis.conf /data/redis/cluster/6383/redis-6383.conf
mkdir -p 6384
cp /data/redis/redis-4.0.1/redis.conf /data/redis/cluster/6384/redis-6384.conf
3个Slave实例的配置文件内容,注意修改下面加粗字体部分的内容即可(注意注释掉的要去掉注释),其他都相同:
配置选项 | 选项值 | 说明 |
---|---|---|
daemonize | yes | 默认情况下 redis 不是作为守护进程运行的,如果你想让它在后台运行,你就把它改成 yes。 |
pidfile | /data/redis/cluster/6382/redis-6382.pid | 当redis作为守护进程运行的时候,默认情况下它会写一个 pid 到 /var/run/redis.pid 文件里面 |
port | 6382 | 监听端口号,默认为 6379,如果你设为 0 ,redis 将不在 socket 上监听任何客户端连接。 |
database | 1 | 设置数据库的数目。 |
cluster-enabled | no | 启用或停用集群 |
cluster-config-file | /data/redis/cluster/6382/nodes-6382.conf | 集群配置文件,启动时自动生成 |
cluster-node-timeout | 15000 | 节点互联超时时间,单位毫秒 |
cluster-migration-barrier | 1 | 这个参数表示的是,一个主节点在拥有多少个好的从节点的时候就要割让一个从节点出来。 |
cluster-require-full-coverage | yes | 如果集群中某些key space没有被集群中任何节点覆盖,集群将停止接受写入 |
appendonly | yes | 启用aof持久化方式 |
dir | /data/redis/cluster/6382/ | 节点数据持久化存放目录 |
slaveof | 192.168.199.126 6379 | Master节点配置 |
如果配置成功并启动,通过客户端工具连接到Master时,输入如下命令
info replication
将会得到如下结果
# Replication
role:master
connected_slaves:3
slave0:ip=192.168.199.249,port=6384,state=online,offset=1127147,lag=1
slave1:ip=192.168.199.249,port=6383,state=online,offset=1127292,lag=1
slave2:ip=192.168.199.249,port=6382,state=online,offset=1127292,lag=1
master_replid:beba8e13c44c5a4afcf8c82889b524b2f76faa22
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:1127292
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:78717
repl_backlog_histlen:1048576
Redis Sentinel部署
3个Redis Sentinel信息如下
IP | Port | 备注 |
---|---|---|
192.168.199.126 | 26379 | Sentinel |
192.168.199.126 | 26479 | Sentinel |
192.168.199.126 | 26579 | Sentinel |
Redis安装完成之后,在redis目录(本实例是redis-4.0.1)下会有一个sentinel.conf文件,创建sentinel目录并将该配置文件拷贝进去重命名为自定义的sentinel配置文件。
mkdir /data/redis/sentinel
cp /data/redis/redis-4.0.1/sentinel.conf /data/redis/sentinel/sentinel_26379.conf
cp /data/redis/redis-4.0.1/sentinel.conf /data/redis/sentinel/sentinel_26479.conf
cp /data/redis/redis-4.0.1/sentinel.conf /data/redis/sentinel/sentinel_26579.conf
sentinel_26379.conf配置内容:
protected-mode no
port 26379
sentinel monitor mymaster 192.168.199.249 6384 2
sentinel failover-timeout mymaster 60000
sentinel config-epoch mymaster 1
sentinel leader-epoch mymaster 1
sentinel known-slave mymaster 192.168.199.126 6379
sentinel_26479.conf配置内容:
protected-mode no
port 26479
sentinel monitor mymaster 192.168.199.249 6384 2
sentinel failover-timeout mymaster 60000
sentinel config-epoch mymaster 1
sentinel leader-epoch mymaster 1
sentinel known-slave mymaster 192.168.199.126 6379
sentinel_26579.conf配置内容:
protected-mode no
port 26579
sentinel monitor mymaster 192.168.199.249 6384 2
sentinel failover-timeout mymaster 60000
sentinel config-epoch mymaster 1
sentinel leader-epoch mymaster 1
sentinel known-slave mymaster 192.168.199.126 6379
启动Redis Master实例
进入Redis Master服务器Redis bin目录,执行以下命令:
./redis-server /data/redis/redis-4.0.1/redis.conf
启动Redis Slave实例
进入Redis Slave服务器Redis bin目录,执行以下命令:
./redis-server /data/redis/cluster/6382/redis-6382.conf
./redis-server /data/redis/cluster/6383/redis-6383.conf
./redis-server /data/redis/cluster/6384/redis-6384.conf
启动Sentinel实例
进入Redis Slave服务器Redis bin目录,执行以下命令:
./redis-sentinel /data/redis/sentinel/sentinel_26379.conf
./redis-sentinel /data/redis/sentinel/sentinel_26479.conf
./redis-sentinel /data/redis/sentinel/sentinel_26579.conf
如果启动时出现如下信息,表示启动成功
Maven配置
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>1.7.5.RELEASE</version>
</dependency>
在config.properties中增加如下配置
#Redis sentinel使用
redis.sentinel1.host=192.168.199.126
redis.sentinel1.port=26379
redis.sentinel2.host=192.168.199.126
redis.sentinel2.port=26479
redis.sentinel3.host=192.168.199.126
redis.sentinel3.port=26579
redis.sentinel.masterName=mymaster
新建spring-redis-sentinel.xml配置文件,写入如下信息
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:jee="http://www.springframework.org/schema/jee"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:util="http://www.springframework.org/schema/util"
xmlns:task="http://www.springframework.org/schema/task" xmlns:cache="http://www.springframework.org/schema/cache"
xmlns:c='http://www.springframework.org/schema/c' xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-4.0.xsd
http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-4.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd
http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-4.0.xsd
http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache-3.1.xsd"
default-lazy-init="true">
<!-- 开启spring cache注解功能 -->
<cache:annotation-driven cache-manager="redisCacheManager" />
<context:annotation-config />
<context:property-placeholder
ignore-unresolvable="true" location="classpath:config.properties" />
<!-- Redis -->
<bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
<property name="maxTotal" value="${redis.maxTotal}" />
<property name="maxIdle" value="${redis.maxIdle}" />
<property name="maxWaitMillis" value="${redis.maxWait}" />
<property name="testOnBorrow" value="${redis.testOnBorrow}" />
</bean>
<bean id="sentinelConfiguration"
class="org.springframework.data.redis.connection.RedisSentinelConfiguration">
<property name="master">
<bean class="org.springframework.data.redis.connection.RedisNode">
<property name="name" value="${redis.sentinel.masterName}"></property>
</bean>
</property>
<property name="sentinels">
<set>
<bean class="org.springframework.data.redis.connection.RedisNode">
<constructor-arg name="host" value="${redis.sentinel1.host}"></constructor-arg>
<constructor-arg name="port" value="${redis.sentinel1.port}"></constructor-arg>
</bean>
<bean class="org.springframework.data.redis.connection.RedisNode">
<constructor-arg name="host" value="${redis.sentinel2.host}"></constructor-arg>
<constructor-arg name="port" value="${redis.sentinel2.port}"></constructor-arg>
</bean>
<bean class="org.springframework.data.redis.connection.RedisNode">
<constructor-arg name="host" value="${redis.sentinel3.host}"></constructor-arg>
<constructor-arg name="port" value="${redis.sentinel3.port}"></constructor-arg>
</bean>
</set>
</property>
</bean>
<!-- redis服务器中心 -->
<bean id="jedisConnectionFactory"
class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<constructor-arg name="sentinelConfig" ref="sentinelConfiguration"></constructor-arg>
<constructor-arg name="poolConfig" ref="poolConfig"></constructor-arg>
</bean>
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
<property name="connectionFactory" ref="jedisConnectionFactory" />
<property name="keySerializer">
<bean
class="org.springframework.data.redis.serializer.StringRedisSerializer" />
</property>
<property name="valueSerializer">
<bean
class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer" />
</property>
</bean>
<!-- redis缓存管理器 -->
<bean id="redisCacheManager" class="org.springframework.data.redis.cache.RedisCacheManager">
<constructor-arg name="redisOperations" ref="redisTemplate" />
</bean>
<bean id="redisUtils" class="ron.blog.blog_service.utils.RedisUtils" />
</beans>
在spring-context.xml引入spring-redis-sentinel.xml
<import resource="classpath:spring-redis-sentinel.xml" />
编写Redis帮助类RedisUtils
public class RedisUtils {
/**
* RedisTemplate是一个简化Redis数据访问的一个帮助类,
* 此类对Redis命令进行高级封装,通过此类可以调用ValueOperations和ListOperations等等方法。
*/
@Autowired
private RedisTemplate<Serializable, Object> redisTemplate;
/**
* 批量删除对应的value
*
* @param keys
*/
public void remove(final String... keys) {
for (String key : keys) {
remove(key);
}
}
/**
* 批量删除key
*
* @param pattern
*/
public void removePattern(final String pattern) {
Set<Serializable> keys = redisTemplate.keys(pattern);
if (keys.size() > 0)
redisTemplate.delete(keys);
}
/**
* 删除对应的value
* @param key
*/
public void remove(final String key) {
if (exists(key)) {
redisTemplate.delete(key);
}
}
/**
* 缓存是否存在
* @param key
* @return
*/
public boolean exists(final String key) {
return redisTemplate.hasKey(key);
}
/**
* 读取缓存
* @param key
* @return
*/
public Object get(final String key) {
Object result = null;
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
result = operations.get(key);
return result;
}
/**
*
* @Author Ron
* @param key
* @param hashKey
* @return
*/
public Object get(final String key, final String hashKey){
Object result = null;
HashOperations<Serializable,Object,Object> operations = redisTemplate.opsForHash();
result = operations.get(key, hashKey);
return result;
}
/**
* 写入缓存
*
* @param key
* @param value
* @return
*/
public boolean set(final String key, Object value) {
boolean result = false;
try {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
operations.set(key, value);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
*
* @Author Ron
* @param key
* @param hashKey
* @param value
* @return
*/
public boolean set(final String key, final String hashKey, Object value) {
boolean result = false;
try {
HashOperations<Serializable,Object,Object> operations = redisTemplate.opsForHash();
operations.put(key, hashKey, value);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 写入缓存
*
* @param key
* @param value
* @return
*/
public boolean set(final String key, Object value, Long expireTime) {
boolean result = false;
try {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
operations.set(key, value);
redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
}
以上的所有配置,如果不出问题,那么基本上我们已经集成了Spring与Redis Sentinel,接下来就是在我们的例子中使用。
在我们的Blog服务(BlogService)中,我们自动装配RedisUtils实例,在进入博客详细页面时,我们首先到Redis中获取是否存在我们所要查看的博客,如果没有,则从MySQL获取并写入Redis。
@Autowired
private BlogContentMapper blogContentMapper;//读者自行定义
@Autowired
RedisUtils redisUtils;
/**
* @Comment 获取博客内容
* @Author Ron
* @Date 2017年10月25日 下午3:05:27
* @return
*/
@Override
public BlogContent getBlog(String bid) {
if(redisUtils.exists(bid)){
logger.info("缓存命中博客"+bid);
return (BlogContent) redisUtils.get(bid);
}else{
logger.info("缓存尚未命中博客"+bid);
BlogContent blogContent = blogContentMapper.selectByPrimaryKey(bid);
redisUtils.set(bid, blogContent);
return blogContent;
}
}
实例演示
我们第一次进入指定博客页面时会在控制台打出缓存尚未命中博客的信息,第二次进入则会打出缓存命中博客字样。
博客详细页面:
第一次控制台打出的信息:
第二次刷新页面是空值台打出的信息:
先查看一下Sentinel信息:
输入命令:./redis-cli -p 26379 info Sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=mymaster,status=ok,address=192.168.199.126:6379,slaves=3,sentinels=4
我们可以看到Master的名称、ip地址、Slave的数量、Sentinel数量。
我们停掉一个Sentinel,在我们的控制台中就会监控到并出现连接丢失的错误日志,但是我们的系统仍然可用。
重新启动之后报错日志就停止。
我们停掉Master,在Sentinel中就会重新选举一个Slave为Master,同时在我们的Eclipse的控制台也可以看到重新选举的Master;
我们看到系统重新选举192.168.199.126 6382为Master。
注意:
启动服务时,可能会报连接Redis Sentinel超时,此时可能是防火墙未关闭造成,使用如下命令关闭防火墙即可:
在CentOS 7中默认使用firewall做为防火墙,下面是启动&关闭防火墙的命令:
// 启动firewall
systemctl start firewalld.service
// 关闭firewall
systemctl stop firewalld.service