Redis基础2(笔记)

一、Jedis

客户端工具,支持用Java语言操作Redis。

1.1 部署在Maven项目中

通过创建新的Maven项目,我们在pom.xml中,加上以下依赖。

<dependencies>
  	<dependency>
  		<groupId>redis.clients</groupId>
  		<artifactId>jedis</artifactId>
  		<version>3.2.0</version>
  	</dependency>
  </dependencies>
package jedis;

import redis.clients.jedis.Jedis;

public class JedisDemo {
	public static void main(String[] args) {
		//创建Jedis对象,填写内容见下构造函数,同时会建立连接
		Jedis jedis = new Jedis();
		//有密码的话还需要密码
		jedis.auth("");
		jedis.close();
		return;
	}
}

1.1.1 Jedis的构造函数

参数命名非常规范,见字知意。
主要的参数:

  • host :redis所在的主机地址
  • port:redis使用的端口号
  • ssl:是否使用ssl协议
public Jedis() {
    super();
  }

  public Jedis(final String host) {
    super(host);
  }

  public Jedis(final HostAndPort hp) {
    super(hp);
  }

  public Jedis(final String host, final int port) {
    super(host, port);
  }

  public Jedis(final String host, final int port, final boolean ssl) {
    super(host, port, ssl);
  }

  public Jedis(final String host, final int port, final boolean ssl,
      final SSLSocketFactory sslSocketFactory, final SSLParameters sslParameters,
      final HostnameVerifier hostnameVerifier) {
    super(host, port, ssl, sslSocketFactory, sslParameters, hostnameVerifier);
  }

  public Jedis(final String host, final int port, final int timeout) {
    super(host, port, timeout);
  }

  public Jedis(final String host, final int port, final int timeout, final boolean ssl) {
    super(host, port, timeout, ssl);
  }

  public Jedis(final String host, final int port, final int timeout, final boolean ssl,
      final SSLSocketFactory sslSocketFactory, final SSLParameters sslParameters,
      final HostnameVerifier hostnameVerifier) {
    super(host, port, timeout, ssl, sslSocketFactory, sslParameters, hostnameVerifier);
  }

  public Jedis(final String host, final int port, final int connectionTimeout, final int soTimeout) {
    super(host, port, connectionTimeout, soTimeout);
  }

  public Jedis(final String host, final int port, final int connectionTimeout, final int soTimeout,
      final boolean ssl) {
    super(host, port, connectionTimeout, soTimeout, ssl);
  }

  public Jedis(final String host, final int port, final int connectionTimeout, final int soTimeout,
      final boolean ssl, final SSLSocketFactory sslSocketFactory, final SSLParameters sslParameters,
      final HostnameVerifier hostnameVerifier) {
    super(host, port, connectionTimeout, soTimeout, ssl, sslSocketFactory, sslParameters,
        hostnameVerifier);
  }

  public Jedis(JedisShardInfo shardInfo) {
    super(shardInfo);
  }

  public Jedis(URI uri) {
    super(uri);
  }

  public Jedis(URI uri, final SSLSocketFactory sslSocketFactory, final SSLParameters sslParameters,
      final HostnameVerifier hostnameVerifier) {
    super(uri, sslSocketFactory, sslParameters, hostnameVerifier);
  }

  public Jedis(final URI uri, final int timeout) {
    super(uri, timeout);
  }

  public Jedis(final URI uri, final int timeout, final SSLSocketFactory sslSocketFactory,
      final SSLParameters sslParameters, final HostnameVerifier hostnameVerifier) {
    super(uri, timeout, sslSocketFactory, sslParameters, hostnameVerifier);
  }

  public Jedis(final URI uri, final int connectionTimeout, final int soTimeout) {
    super(uri, connectionTimeout, soTimeout);
  }

  public Jedis(final URI uri, final int connectionTimeout, final int soTimeout,
      final SSLSocketFactory sslSocketFactory, final SSLParameters sslParameters,
      final HostnameVerifier hostnameVerifier) {
    super(uri, connectionTimeout, soTimeout, sslSocketFactory, sslParameters, hostnameVerifier);
  }


通过下列代码,判断是否连通

System.out.print(jedis.ping());

结果是PONG则成功。
在这里插入图片描述

注意,需要在设置文件中,注释bind中本地绑定和关闭保护模式,修改后需要重启。
在这里插入图片描述

在这里插入图片描述
此外,注意端口是否放行。(请不要直接关闭防火墙)

1.2 操作尝试

首先我们通过 @Test 引入JUnit单元测试工具。(可以直接通过主类方法运行代码,这里只是笔者方便测试)
试着获取所有的键。

	@Test
	public void getKeys(){	
		Set<String> keys = jedis.keys("*");
		for(String key:keys) {
			System.out.println(key);
		}
	}
}

通过JUnit Test方式运行
在这里插入图片描述
在这里插入图片描述

使用例子:

	@Test
	public void KeysOperator() {
		//设置key
		System.out.println(jedis.set("k1","v1"));
		//获取keys
		Set<String> keys = jedis.keys("*");
		for(String key:keys) {
			System.out.println(key);
		}
		//k1存在否
		System.out.println(jedis.exists("k1"));
		//获取对应key
		System.out.println(jedis.get("k1"));
		//获取过期时间
		System.out.println(jedis.ttl("k1"));
	}

在这里插入图片描述

操作与Redis的命令基本一致,需要注意的是返回的数据类型。

1.2.1 验证码例子

  1. 随机生成6位数数字验证码,2分钟有效
  2. 输入验证码。点击验证,返回成功或失败
  3. 每个手机号每天只能输入三次

笔者之前的文章用的是guava和邮箱实现,
可以参考邮箱验证流程与token生成

此篇,我们只是简单模拟。

  1. 随机生成6位数数字验证码,2分钟有效
    使用Random生成,生成后在Redis中对key设置过期时间。

  2. 输入验证码。点击验证,返回成功或失败
    读取出来后比对即可

  3. 每个手机号每天只能输入三次
    手机号发送后incr自加,当大于3时拒绝发送。每天重置即可。

package jedis;

import java.util.Calendar;
import java.util.Random;
import java.util.Scanner;

import redis.clients.jedis.Jedis;

public class SendPhoneCode {
	
	
	public static void main(String[] args) {
		Scanner scanner = new Scanner(System.in);
		System.out.println("输入手机号<<");
		String phone = scanner.nextLine();
		String code = getCode();
		System.out.println("生成的验证码>>"+code);
		if(AddVerifyCode(phone,code)) 
			System.out.println("输入验证码<<");{
			code = scanner.nextLine();
			if(JudgeVerifyCode(phone,code)) {
				System.out.println("验证成功");
			}
		}
		
		scanner.close();		
	}
	
	
	
	/*
	 * 
	 * 生成验证码
	 * 返回生成的验证码
	 * */
	public static String getCode() {
		Random random = new Random();
		StringBuilder code = new StringBuilder();
		for(int i = 0;i < 6;i++) {
			code.append(random.nextInt(10));
		}
		return code.toString();
	}
	/*
	 * 添加验证码,以及记录发送的次数
	 * 
	 * 
	 * */
	public static boolean AddVerifyCode(String phone ,String code) {
		if(phone.isEmpty()) {
			System.out.println("有数据为空");
			return false;
		}
		Jedis jedis = new Jedis();
		String countKey = "VerifyCode" + phone + ":count";
		String codeKey = "VerifyCode" + phone + ":code";
		String count = jedis.get(countKey);
		boolean res = false;
		if(count == null) {
			String temp = jedis.setex(countKey,Calc().intValue(), "1");
			if(!temp.equals("OK")) {
				System.out.println("插入数据失败");
				res = true;
			}
		}
		else if(Integer.parseInt(count) < 3) {
			Long temp = jedis.incr(countKey);
			if(temp == 0) {
				System.out.println("插入数据失败");
				res = true;
			}
		}
		else {
			System.out.println("今天已发送过三次验证码,请明天尝试");
			res = true;
		}
		if(res) {
			jedis.close();
			return false;
		}
		jedis.setex(codeKey,120,code);
		jedis.close();
		return true;
	}
	/*
	 * 
	 * 计算当前时间距离第二天凌晨的还有多少时间
	 * 返回时间差
	 * */
	public static Long Calc() {
		Calendar cal = Calendar.getInstance();
        cal.add(Calendar.DAY_OF_YEAR, 1);
        cal.set(Calendar.HOUR_OF_DAY, 0);
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.MILLISECOND, 0);
        return (cal.getTimeInMillis() - System.currentTimeMillis()) / 1000;
	}
	/*
	 * 验证码校验
	 * 返回是否一致
	 * 
	 * */
	public static boolean JudgeVerifyCode(String phone,String code) {
		if(phone.isEmpty() || code.isEmpty()) {
			System.out.println("有数据为空");
			return false;
		}
		Jedis jedis = new Jedis();
		String codeKey = "VerifyCode" + phone + ":code";
		String Vcode = jedis.get(codeKey);
		if(Vcode == null || !code.equals(Vcode)){
			System.out.println("验证信息失败");
			jedis.close();
			return false;
		}
		
		jedis.close();
		return true;
	}
}


样例:
在这里插入图片描述
在这里插入图片描述

二、SpringBoot2部署Jedis

一般使用的是连接池模式。
前面用的就是非连接池的方式。

2.1 部署

在Springboot2项目下,引入下列依赖

		<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>

在Springboot项目配置文件中,我们做出如下设置

#服务器地址
spring.redis.host=
#端口号
spring.redis.port=6379
#使用的库
spring.redis.database=0
#连接超时时间
spring.redis.timeout=1800000
#最大连接数
spring.redis.lettuce.pool.max-active=20
#最大阻塞等待时间(为负则时无限制)
spring.redis.lettuce.pool.max-wait=-1
#连接池里面的最大空闲连接
spring.redis.lettuce.pool.max-idle=5
#连接池里面的最小空闲连接
spring.redis.lettuce.pool.min-idle=0

创建一个Config包,下面创建一个类,作为配置类。我们配置了两个类:

package com.XXX.config;

import java.time.Duration;

import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;

//启用缓存
@EnableCaching
@Configuration

//缓存配置类
public class RedisConfig extends CachingConfigurerSupport {
	/*
	 * redisTemplate配置序列化方式
	 * */
	@Bean
	public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){
		RedisTemplate<String,Object> template = new RedisTemplate<>();
		RedisSerializer<String> redisSerializer = new StringRedisSerializer();
		Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
		ObjectMapper om = new ObjectMapper();
		om.setVisibility(PropertyAccessor.ALL,JsonAutoDetect.Visibility.ANY);
		//如果JsonTypeInfo报错,试着将鼠标放在其上,通过弹出选项中的setup解决
		om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance , ObjectMapper.DefaultTyping.NON_FINAL,JsonTypeInfo.As.WRAPPER_ARRAY);
		jackson2JsonRedisSerializer.setObjectMapper(om);
		template.setConnectionFactory(factory);
		//key序列化方式
		template.setConnectionFactory(factory);
		//value序列化方式
		template.setHashKeySerializer(jackson2JsonRedisSerializer);
		return template;
	}
	
	
	@Bean
	public CacheManager cacheManager(RedisConnectionFactory factory) {
		RedisSerializer<String> redisSerializer = new StringRedisSerializer();
		Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
		ObjectMapper om =new ObjectMapper();
		om.setVisibility(PropertyAccessor.ALL,JsonAutoDetect.Visibility.ANY);
		om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance , ObjectMapper.DefaultTyping.NON_FINAL,JsonTypeInfo.As.WRAPPER_ARRAY);
		jackson2JsonRedisSerializer.setObjectMapper(om);
		//配置序列化(解决乱码问题),过期时间设为600秒
		RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
				.entryTtl(Duration.ofSeconds(600))
				.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
				.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
				.disableCachingNullValues();
		RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
				.cacheDefaults(config)
				.build();
		return cacheManager;
		
				
	}
}


测试能否使用

package com.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/redisTest")
public class RedisTestController {
	@Autowired
	private RedisTemplate<String, String> redisTemplate;
	
	@GetMapping
	public String testRedis() {
		//设置值到redis
		redisTemplate.opsForValue().set("name", "lucy");
		//获取设置的值
		String name = redisTemplate.opsForValue().get("name");
		return name;
	}
}

测试一下
在这里插入图片描述

三、Redis的事务

3.1 定义

事务是一个单独的隔离操作:所有命令序列化,按顺序执行。执行中,不会被其他客户端发送来的请求打断。

即主要是把命令串起来,防止被打断。

Multi-开启事务
组队阶段
放入redis命令
discard
放弃组队不再执行队伍中的命令
Exec-执行事务执行队伍中的命令
执行阶段

在这里插入图片描述
Java中就是如下表示:

		redisTemplate.setEnableTransactionSupport(true);
		List<Object> result = redisTemplate.execute(new SessionCallback<List<Object>>() {
		    public List<Object> execute(RedisOperations operations) throws DataAccessException {
		    	operations.watch(secondKillKey);
		        operations.multi();
		        
		        return operations.exec();
		    }
		});

3.2 注意

如果组队阶段某个命令有错误,只有报错命令不会被执行,其他命令仍旧会执行,不会回滚。即,没有原子性。

3.3 事务冲突

当多个事务同时想要执行。

3.3.1 悲观锁

认为自己操作时,总是有人也想要操作该数据。因此,执行时,操作会,用一个锁将操作数锁住,使得其他操作无法进行。

类似于synchronized、reetrantlock、mutex这种直接上锁的方式。

3.3.1.1 setnx

通过在执行前设置一个特殊的键值对,来判断该数据是否被上锁。

3.3.2 乐观锁

认为自己操作时,别人不是很频繁的操作该数据。因此,通过版本号来比对版本号是否一致,一致则说明没有问题。

类似于MYSQL的MVCC机制,注意和CAS不一样,因为CAS没办法解决ABA问题。

3.3.2.1 乐观锁watch监视key

监视key

watch key [key …]

取消监视(事务操作中有效)

unwatch

两个控制台同时监视test,然后分别先后执行事务,结果后执行的无法执行。
在这里插入图片描述

如果执行过 EXEC 或DISCARD,无需再执行 UNWATCH。

因为 EXEC 命令会执行事务,因此 WATCH 命令的效果已经产生了;而 DISCARD命令在取消事务的同时也会取消所有对 key 的监视,因此这两个命令执行之后,就没有必要执行 UNWATCH了。

Redis事务特性

  1. 单独的隔离性操作
  2. 没有隔离级别概念(因为单线程,但6之后又有多线程能力)
  3. 不保证原子性(失败后仍旧继续)

四、模拟秒杀

页面,笔者页面是放在SpringBoot资源下的动态页面中的,因此,路径会和放在外部的不大一样(同时避免了跨域问题)。


<!DOCTYPE html>
<html>
<head>
	<meta charset="UTF-8"/>
	<title>首页</title>
</head>
<script src="/static/js/vue.js"></script>
<body>
<h1>秒杀物品</h1>
<div id="SecondKill">

	<div @click="onSubmit" >秒杀</div>
	结果:{{response}}
</div>


</body>
<script>
    var app = new Vue({
      el: '#SecondKill',
      data:{
      	productId:"0101",
      	status:false,
      	response:"",
      },
      methods:{
      onSubmit:function(){
          if(this.status){
            this.response = "请勿重复提交";
            return;
          }
          this.status = true;
          const xml = new XMLHttpRequest()
          xml.open("post","/SecondKill",true);
          var formData = new FormData();
          formData.append('productId',this.productId);
          xml.send(formData);        
          xml.onreadystatechange = function(){
            if(xml.readyState === 4 && xml.status === 200){
              let result = JSON.parse(xml.responseText.toString())
              if(result.isSuccess){
                app.response = "秒杀成功!!";
              }
              else{
                app.response = "秒杀失败!!";
              }
              app.status = false;
            }else{
              app.response = "未知错误!!";
              app.status = false;
            }
          }
        }
      }
  })

  </script>

此处可以用非Springboot,用JavaWeb也可以,使用的就是最开始的连接方式。

控制层

package com.controller;

import java.util.Random;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.Service.SecondKillServiceImpl;

@RestController
public class RedisTestController {
	@Autowired
	SecondKillServiceImpl secondKillServiceImpl;
	@RequestMapping("/SecondKill")
	public String SecondKill(String productId) {
		//在此处我们生成一个随机ID,来表示用户传过来的UID
		int userId = new Random().nextInt(50000);
		//调用秒杀方法,获取结果
		boolean isSuccess = false;
		try {
			isSuccess = secondKillServiceImpl.doSecKill(String.valueOf(userId),productId);
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return "{\"isSuccess\":"+ isSuccess +"}";
	}
}


秒杀Service,笔者此处偷懒,直接实现的Service层。同时,也没有用多线程的方式执行。若用多线程,此处的RedisTemplate可以用ThreadLocal。

  • 同时,因为我们是一个Redis,而不是多个Redis,因此可以不用事务,而是基于关键操作利用它单线程类似于原子操作的方式解决。
  • 同时,笔者也给出了事务的写法,此处乐观锁会导致少卖,因此,可以通过获取result的值进行进一步判断。
  • 可以用Redis 调用LUA脚本,将多步和为一步(Redis调用LUA脚本视为一步操作)来解决库存遗留问题(Redis 2.6以上版本)。
package com.Service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.stereotype.Service;

@Service
public class SecondKillServiceImpl{
	@Autowired
	private RedisTemplate<String, String> redisTemplate;
	public boolean doSecKill(String uid,String productId) throws Exception{
		//判断uid与productId非空
		if(uid == null || productId == null) {
			return false;
		}
		//拼接key
		String secondKillKey = "sk:" + productId+":qt";
		String userKey = "sk:"+productId+":user";
		//获取库存,若库存为null,秒杀未开始
		String key = redisTemplate.opsForValue().get(secondKillKey);
		if(key == null) {
			return false;
		}
		
		//用户是否重复秒杀
		if(redisTemplate.opsForSet().isMember(userKey, uid)) {
			return false;
		}
		
		
		/*1.事务写法
		//开启事务
		
		redisTemplate.setEnableTransactionSupport(true);
		List<Object> result = redisTemplate.execute(new SessionCallback<List<Object>>() {
		    public List<Object> execute(RedisOperations operations) throws DataAccessException {
		    	operations.watch(secondKillKey);
		        operations.multi();
				//组队
				//库存-1
		        operations.opsForValue().decrement(secondKillKey);
				//加入用户
		        operations.opsForSet().add(userKey, uid);
		        return operations.exec();
		    }
		});

		//判断结果
		if(result == null || result.size() == 0) {
			return false;
		}
		*/
		//2.非事务
		if(redisTemplate.opsForValue().decrement(secondKillKey) < 0) {
			return false;
		}
		redisTemplate.opsForSet().add(userKey, uid);
		return true;
	}
}

五、持久化操作RDB与AOF

他们默认存储在同路径下。

5.1 RDB(默认开启)

在指定时间间隔内,将数据集快照写入硬盘之中。
在这里插入图片描述
Redis缓存会用临时空间,通过Fork子进程(写时备份技术),先把数据放进去。然后覆盖到dump.rdb文件中。

save是手动备份,bgsave是自动备份。注意,过了设定的时间后,会重新计算时间与key。

如果直接写入,宕机会导致备份文件也挂掉。

5.1.1 优势

  • 适合大规模的数据恢复
  • 对数据完整性和一致性要求不高可使用
  • 节省硬盘空间
  • 恢复快

5.1.2 劣势

  • 存储的时候会把数据拷贝一份,2倍膨胀
  • 虽然用了写时复制技术,但数据庞大时,较消耗性能
  • 备份周期在一定时间间隔后做备份,所以意外宕机,会导致丢失快照后的所有修改

5.2 AOF(默认不开启)

以日志的形式记录每一个写操作(增量保存),将Redis执行的过的所有指令都记录下来(读操作不记录),只允许追加文件但不可以改写文件,redis启动之初会重新构建数据。从前到后执行。
在这里插入图片描述
在这里插入图片描述

5.2.1 AOF重写/压缩(4.0以上)

省略过程,记录结果。
比如有很多操作,只记录结果操作。用来减少记录的操作量。

set a a1
set b b1
重写为:
set a a1 b b1

  1. 通过bgrewriteaof触发重写。判断是否有bgsave或者bgrewriteaof在进行。有则等待。
  2. 通过fork进程执行重写操作
  3. 同样写入临时文件
  4. 完成后会向主进程发送信号
  5. 覆盖旧文件。

重写时机:

auto-aof-rewrite-min-size设置重写基准值,最小文件64M。当达到时,进行重写。
AOF大小>=base_size+base_size*100%(默认)且大小>=64mb时,触发重写操作。

5.2.2 优势

  • 本分机制更稳健,丢失数据概率低
  • 刻度的日志文本,可以处理通过AOF处理错误操作

5.2.3 劣势

  • 比RDB占用更多空间
  • 备份恢复速度更慢
  • 每次写都同步,会有压力
  • 存在个别BUG

5.3 混合模式(4.0以上,默认不开启)

即,同时使用RDB和AOF。

如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF ⽂件开头。

  • 好处:
    以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据
  • 坏处:
    AOF ⾥⾯的RDB 部分是压缩格式不再是 AOF 格式,可读性较差。

六、主从复制

主机数据更新后,更具配置和策略,自动同步到备份机的master/slaver机制。Master机器以写为主,Slaver以读为主。

  1. 读写分离:降低压力。
  2. 容灾快速恢复:一台挂掉后,快速切换服务器。一般一主多从。
  3. 当主服务器
写入
读取
复制
复制
读取
应用
主服务器
从服务器1
从服务器2

6.1 在本地建立主从模式

我们先将文件下的

/etc/redis.conf

复制到一个文件夹中,可以叫做myredis
修改内容如下

daemonize yes
Appendonly (关掉或者换名字)

接下来,我们创建三个配置文件,做到一主两从(数字是端口号)

redis6379.conf
redis6380.conf
redis6381.conf

此处笔者放于同一文件下,然后对于每一个文件,我们在其中添加如下内容,同时需要注意改为对应数字。(配置文件也是外层覆盖内层,因此,include的配置项会被当前配置项覆盖)

include /opt/redis/redis.conf
pidfile “/var/run/redis_6379.pid”
port 6379
dbfilename “dump6379.rdb”

然后后台启动他们

redis-server redis6379.conf
redis-server redis6380.conf
redis-server redis6381.conf

通过命令,查看是否能够启动

ps -ef | grep redis

分别连接这几个redis

redis-cli -p 端口号

通过下面的命令,查看状态

info replication

可以看到

role:master#角色,主服务
connected_slaves:0#从服务数目
master_failover_state:no-failover#故障转移
master_replid:d362ca74278def073b29589c3cec03b9f9cdcbb9
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0

在redis上,加上下列命令,即可让设置当前redis为从服务

slaveof ip 端口号

可以尝试在从服务器上添加,会发现失败。

从机数目越多,同步速度越慢。

6.2 一主二从特点

6.2.1 宕机情况

  1. 当从服务器挂掉后,重启后,会变回主服务器
  2. 如果,这个时候我们在此把他变为从服务器,会从新从主服务器将数据同步过来(从头复制)
  3. 如果主服务器挂掉了,从服务器不会变化,仍然是从服务器。
  4. 主服务器重启,主服务器下仍旧会有两个从服务器

6.2.2 复制原理

从服务器主动发起

  1. 连接上主服务器后,从服务器向主服务器发送数据同步消息
  2. 主服务器街道从服务器发送过来的同步消息,把服务器数据进行持久化,rdb文件生成后,会被发送给从服务器,从服务器拿到rdb后,进行读取。
  3. 从服务器会抛弃原有数据

主服务器主动发起

  1. 每次主服务器进行写操作后,会与从服务器数同步

6.2.3 薪火相传

  1. 主服务器只会取同步一台从服务器,由从服务器之间自己取完成同步。
  2. 可以看成树结构,由主服务器开始向一台从服务器同步后向下,逐步覆盖到叶节点

通过设置从服务器的主机位另一台从服务器,即可完成该模式。

缺点:

  1. 第一台从服务器挂掉了,后面将无法同步
  2. 当层数过高时,数据延迟较大

6.2.4 反客为主

当它的主服务器挂掉后,手动输入一下命令即可,让从服务器作为主机。

slaveof no one

缺点:

  1. 需要手动启动

6.2.5 哨兵模式

反客为主的自动模式。

创建一个文件,作为哨兵的配置文件,有它来监视主机

vi sentinel.conf

并写入(mymaster 取的主机名;1 ,quorum ,表示至少有多少个从哨兵认为主服务器挂掉了)具体内容可以查看安装文件下的源文件说明。可以设置判断宕机的等待时长。

sentinel monitor mymaster 127.0.0.1 6379 1
protected-mode no
daemonize yes
port 26379

现在启动哨兵

redis-sentinel /opt/redis/sentinel.conf

  • 切换,由哨兵选择一个从服务器作为新的主服务器。
  • 此时,挂掉的主服务器,重启后作为从服务器。

切换逻辑(优先度从高到低):

  1. 根据优先级别slave-priority(新版本叫做replica-priority)决定。可以在每一个redis服务器的配置文件中配置。
  2. 偏移量最越大的,优先。即,与挂掉的主服务器同步率越高的优先。
  3. runid最小的从服务,runid是实例启动随机生成的40位ID。

业务层一般会连接哨兵,向哨兵获取redis连接。就不再是直接连接redis服务就好了。

6.2.6 多哨兵模式(一主二从三哨兵)

为了避免哨兵挂掉,因此可有多个哨兵相互监督,通过发布与订阅的方式,确认彼此状态。

  • 主观下线:根据定时任务3对没有有效回复的节点做主观下线处理。
  • 客观下线:若主观下线的是主节点,会联系其他哨兵对此主节点进行判断,一般为一半多的哨兵(依据设置的quorum 数目)达成一致意见才认为一个master客观上已经宕机掉。(因此导致哨兵数多为奇数,保证一半多)

过程:

  1. 主服务器宕机,哨兵1先检测到这个结果,但是系统并不会马上进行重新选举和故障转移过程。
  2. 当后面的哨兵也检测到主服务器连接不上了,并且数量达到quorum 个时。就会投票选举一个Leader哨兵(有一个任期,任期内继续当,过期了就会在下一次需要Leader时重选),由它开始故障转移操作,将从机切换成主机。以订阅的形式,通知下去。

任何一个想成为 Leader 的哨兵,要满足两个条件:

  • 拿到半数以上的赞成票

  • 拿到的票数同时还需要大于等于哨兵配置文件中的 quorum 值

同样的,我们只用让程序连接哨兵就好,具体可以不用关心。

相关内容详情可参考
Redis中哨兵选举算法
如果redis哨兵宕机了怎么办_Redis#哨兵机制(九)
Redis哨兵集群中哨兵挂了,主从库还能切换吗?

七、集群

7.1 简介

解决问题

  1. 当redis容量不足,扩容问题。
  2. 并发写,redis分摊问题

最开始,用的是代理的方式,由一个代理服务,来将请求送达各自redis区域。再为代理服务准备一个从机,以防挂掉。

会导致服务器需要很多,同时搭建较难,后期维护也会十分复杂。

因此,我们采用无中心化集群。

客户端
代理服务
用户
订单
商品

7.2 无中心化集群

各个服务器之间是连通状态,彼此可以互相访问。
我们把这些数据分别分散再集群中。通过计算,判断属于哪一个服务器,然后去其中查找。

客户端
订单
商品
用户

7.3 本地部署集群

我们按照主从时的做法,准备6个配置文件
不过,设置内容需要修改,内容如下

include /opt/redis/redis.conf
pidfile “/var/run/redis_6379.pid”
port 6379
dbfilename “dump6379.rdb”
cluster-enabled yes
cluster-config-file nodes-6379.conf
cluster-node-timeout 15000

在确保文件正常启动后(6个redis服务),我们需要转到安装文件下。使用ruby环境(旧版本需要装环境,但新版本已经装好,所以此处直接使用即可)

cd /opt/redis-7.0.2

执行下列命令,其中**–cluster-replicas**表示集群方式,1:一主机,一从机,三组(2*3)。

redis-cli --cluster create --cluster-replicas 1 真实IP地址:端口号1 [ 真实IP地址:端口号2 …

如果用不了,原因可能有:

  1. 端口未开放
  2. 尝试清空redis对应服务的值
  3. 尝试使用修复工具

以集群方式连接

redis-cli -c -p 端口号

连接任意一个端口,都可以访问目标内容。通过插槽来判断是否属于自己,不是就重定向给其他服务器。

查看集群节点

cluster nodes

7.4 slots插槽

一个redis集群中,包含有16384个插槽(hash slot),数据库中的每个键都属于这些插槽中的一个。

集群使用公式CRC16(key)%16384来计算key属于哪个插槽,其中CRC16(key)语句则是用于计算键的key的CRC16校验和。

集群中的每一个节点负责处理一部分插槽(相当于把所有数据,通过插槽分散到各个服务器中)。我们通过计算key,得到一个值,通过值来判断其属于哪一个redis服务器,然后去其中找。

不再同一个插槽下,没办法使用mget,mset,因为无法计算slot。我们只能通过组的形式

mset key{group} value key2{group}value2

获取键的插槽值

cluster keyslot <key>

同时,通过插槽值查看值,只能看到自己服务器当前负责的范围

cluster countkeysinslot <slotkey>

获取同一插槽值下的键,以及获取多少个

cluster getkeysinslot <slotkey> <count>

7.5 故障恢复

  1. 主机挂掉后,从机马上成为新的主机提供服务
  2. 挂掉的主机重启后,会成为从机
  3. 主机与从机都挂掉后,基于配置(cluster-require-full-coverage yes 代表整个集群挂掉)决定集群能否继续使用。

7.6 Jedis开发集群

public class RedisClusterDemo{
	public static void main(String[] args){
		HostAndPort hostAndPort = new HostAndPort("ip地址",端口号);
		JedisCluster jedisCluster = new JedisCluster(hostAndPort);
		jedisCluster.set("","");
		System.out.println("value:"+jedisCluster.get(""));
		jedisCluster.close();
	}
}

7.7 优缺点

  • 优点
  1. 实现扩容
  2. 分摊压力
  3. 无中心配置,相对简单
  • 缺点
  1. 多键操作不支持
  2. LUA脚本不支持
  3. 出现较晚, 很多公司采用其他集群方案

参考文献

[1]【尚硅谷】Redis6入门到精通 超详细教程
[2]菜鸟教程-Redis
[3]Redis-官方文档
[4]JavaGuide面试突击版

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值