主从架构
创建 一主两从文件夹。
mkdir master slave1 slave2
复制 etc/redis/redis.conf 文件分别到三个文件夹下。
cp /etc/redis/redis.conf /master
cp /etc/redis/redis.conf /slave1
cp /etc/redis/redis.conf /slave2
依次修改配置
设置master端口号为7000,slave1端口号为7001,slave2端口号为7002
依次修改master、slave1、slave2中bind为0.0.0.0
在从slave1,slave2配置文件中找到slaveof
取消注释后第一个数据填写master的bind 第二个数据填写master的端口号
salveof 0.0.0.0 7000
配置好之后使用下列代码依次打开7000、7001、7002端口
reids-server master/redis.conf
reids-server slave1/redis.conf
reids-server slave2/redis.conf
打开三个页面分别进入master、slave1、slave2
redis-cli =p 7000 --raw
redis-cli =p 7001 --raw
redis-cli =p 7002 --raw
此时:
master节点进行写操作成功后,会将数据同步到两个slave节点中。
端口7001、7002的slave变成了只读状态,无法进行写操作。
即使master宕机,从节点slave也无法暂时替代master结点进行写操作。
为了实现master宕机后,可以有新的节点来替代master进行写操作,就有了哨兵机制
哨兵机制
当主节点宕机后,会自动选出一个从节点来替代主节点进行读写操作。
创建sentinel.conf文件,加入一行
sentinel monitor 给该哨兵命名(自己起名) 主节点ip 主节点端口号 需要几票同意(单数)
例子: sentinel monitor mymaster 0.0.0.0 7000 1
解释一下最后一项 在sentinel中有一套自己的算法,当主节点宕机后sentinel会在从节点中选举出一个新的主节点。而最后的数字就代表需要几票才能通过选举,一定要写单数。
安装redis-sentinel
apt install redis-sentinel
安装后使用:
redis-sentinel /master/sentinel.conf
进入sentinel
根据上一节的主从设置打开三个结点master、slave1、slave2。此时运行
redis-cli -p 7000 shutdown
关闭当前主节点模拟宕机。
这时哨兵会等待主节点响应,默认为15秒,如需更改时间,在配置文件中加入
sentinel down-after-milliseconds mymaster 5000
代表主节点5秒没有相应就触发选举。
redis中测试
开启三个节点,开启哨兵
可以看到最下面,当前主节点为端口号7001对应的节点,现在我把7001端口关闭。
此时可以看到,经过redis的哨兵机制选举出了端口7002为当前主节点。
下面去springboot中测试一下
在sentinel配置文件中port下方添加一句话
bind 0.0.0.0
表示开启远程连接
在springboot的配置文件中随着哨兵的选举更改配置中的端口号
#单节点
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.database=0
redis依赖:
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
application.properties
server.port=8080
#单节点
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.database=0
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.url=jdbc:mysql://localhost:3306/test?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#开启二级缓存
mybatis-plus.configuration.cache-enabled=true
#开启日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
Emp实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Emp implements Serializable {
private int id;
private String occ;
}
EmpMapper
@Mapper
//开启缓存
@CacheNamespace(implementation = RedisCache.class,eviction = RedisCache.class)
@CacheNamespaceRef(EmpMapper.class)
开启事务支持
//@Transactional(propagation = Propagation.SUPPORTS)
@EnableTransactionManagement
public interface EmpMapper extends BaseMapper<Emp> {
}
ApplicationUtil 获取spring工厂
@Component
public class ApplicationUtil implements ApplicationContextAware {
//拿到spring工厂数据
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext=applicationContext;
}
//获取工厂中的方法,并返回
public static Object getBean(String beanName){
return applicationContext.getBean(beanName);
}
}
RedisCache 实现Cache接口
public class RedisCache implements Cache {
//放入缓存的namespace com.tan.UserMapper.UserMapper"
private final String id;
private RedisTemplate redisTemplate;
public RedisCache(String id) {
this.id = id;
}
@Override
public String getId() {
return this.id;
}
//缓存放入值
@Override
public void putObject(Object o, Object o1) {
//通过applicationContext工厂工具类,获取redisTemplate
getRedisTemplate();
redisTemplate.opsForHash().put(id.toString(), getKeyToMD5(o.toString()), o1);
}
//redis中获取数据
@Override
public Object getObject(Object o) {
getRedisTemplate();
return redisTemplate.opsForHash().get(id.toString(), getKeyToMD5(o.toString()));
}
//根据指定的o 删除缓存
@Override
public Object removeObject(Object o) {
return null;
}
//删除所有缓存
@Override
public void clear() {
getRedisTemplate();
redisTemplate.delete(id);
}
@Override
public int getSize() {
getRedisTemplate();
return redisTemplate.opsForHash().size(id).intValue();
}
@Override
public ReadWriteLock getReadWriteLock() {
return null;
}
//防止代码冗余
private void getRedisTemplate(){
if (null == redisTemplate){
redisTemplate = (RedisTemplate) ApplicationUtil.getBean("redisTemplate");
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
}
}
//统一key格式
private String getKeyToMD5(String key){
return DigestUtils.md5DigestAsHex(key.getBytes());
}
}
测试类
@SpringBootTest
class JedisApplicationTests {
@Autowired
private EmpMapper empMapper;
@Test
public void test1(){
empMapper.insert(new Emp(1,"财务部"));
}
}
咱们先用redis单节点配置进行测试:
#单节点
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.database=0
可以看到提示信息无法在从节点写入数据,这就证明当前配置无法在主节点宕机后,再使用redis进行写操作。
那么现在就需要根据哨兵的选举来动态的改变当前可以进行写操作的节点:
#根据哨兵选举获取节点
#master是自己起的哨兵名字
#nodes是当前主节点地址和哨兵port
spring.redis.sentinel.master=mymaster
spring.redis.sentinel.nodes=127.0.0.1:26379
修改之后再次运行程序,报错:该数据已存在。
这是因为刚才我运行过一次insert操作,缓存中出错不影响mysql存储数据。删掉mysql中对应数据再次运行:
运行成功。
现在我让当前主节点7002宕机,然后等哨兵推选出新的主节之后,再测试一下。
可以看到当前主节点为7001 ,springboot还是从当前哨兵中获取到了可以执行写操作的主节点。