Redis学习笔记
前言
Redis是现在最受欢迎的NoSQL数据库之一,Redis是一个使用ANSI C编写的开源、包含多种数据结构、支持网络、基于内存、可选持久性的KV键值对存储数据库
具备如下特征
- 基于内存运行,性能高效
- 支持分布式、理论上可以无限扩展
- key-value存储系统
- 开源的使用ANSI C语言编写,科技与内存亦可持久化的日志型、可基于内存一颗持久化的日志型、Key-value数据库,并提供多种语言的API
基本类型:字符串(string),散列(hash), 列表(lists), 集合(sets) 有序集合(sorted sets)
redis官网: https://redis.io/
集群模式
模式一、主从(master-salver) 2.8之前
redis同mysql一样,虽然读写都很快,但是也会产生读写压力大的情况,为了分担读的压力,redis支持主从复制,master进行写操作,slave进行读操作,Redis的主从结构可以采用一主多从或者级联结构,Redis主从复制可以根据是否全量分为 全量同步 和 增量同步
模式二、哨兵模式(2.8之后)
由于redis单一的主从复制模式下,容灾性较差,当集群中master由于故障下线了,那么slaver因为没有master二同步终端,因此需要人工进行故障转移工作,Redis在2.8之后提供了一种该可用的方案-哨兵模式(Sentinel
),由一个或多个哨兵节点组成Sentinel系统用于监听一个或多个redis集群
,监听主节点和从节点的运行状态,并当redis的master下线后,从master的从头节点中选举出新的master并维护新的主从关系
注:哨兵监视redis集群的同时,哨兵节点也会相互监听
哨兵模式过程
1、监听集群(master-slave)
sentinel在监听redis的集群过程中会周期性的对redis集群发送指令进行状态监控
Ⅰ:通过info指令定期更新当前节点最新的节点信息
Ⅱ:发现新加入的slave节点,确认其主从关系
2、master宕机过程
主管下线和客观下线
sentinnel集群在监听redis集群的过程中,每个哨兵会对master发送心跳PING来确认master的存活,如果master在一定时间范围内不回应PONG,或者回应了一个错误的信息,该sentinel会认为当前集群的master已经无法使用(主观下线),并同时向sentinel集群中的其他节点发送sentinel ismaster-down-by-addr
命令询问其他节点对主机的状态判断,当超过一定的sentinel确认master已经无法使用,这时候master下线的判定就认为是客观的(客观下线)
3、领导者(sentinel)选举
Ⅰ、每个在线的sentinel节点都有资格成为领导者,当它确认主节点主管下线的时候,会向其他Sentinel节点发送 sentinel is-master-down-by-addr命令,要求将自己设置为领导者
Ⅱ、收到命令的Sentinel节点,如果没有同意锅其他Sentinel节点的Sentinel is-master-down-by-addr命令,将同意该请求,否则拒绝
Ⅲ、如果该Sentinel节点发现自己的票数大于等于max(quorum,num(sentinels)/2 + 1) (过半),那么它将成为领导者
Ⅳ、如果此过程中没有选举出领导者,将进入下一次选举
4、故障转移
故障转移就是当master宕机,sentinel集群会在redis集群中,自动选择一个合适的slave节点来升级为master节点的操作,不需要人工故障转移
1、筛选slave成为master
(1) 过滤掉无法使用的slave(主观下线、断线)
(2) 选择slave-priority最大的从节点(可能存在多个相同大小),如果只存在一个,则完成选择,否则继续
slave-priority值在redis启动文件中配置,用于决定故障转移优先级,以及数据备份时的备份顺序
(3) 选择复制偏移量最大的从节点(复制最完整的) 如果存在,则完成选择,否则继续
最完整: master和slave同步进度通过参数master_repl_offset 参数来记录主从同步的程度,每完成一次同步此值会进行累加,从多个slave中选择偏移量最大的slave则能选出复制master最完整的主机
(4) 选择runid(服务器运行的唯一ID)最小的从节点
2、对新master发送slaveof no one指令,停止其主从复制
3、修改程序段连接到新的master
4、向集群中其他slave发送指令修改为新的master的从机
5、原master重启后修改为新的master的slave
哨兵模式的缺点
1、如果是从节点下线了,sentinel是不会对其进行故障转移的,连接从节点的客户端也无法获取到新的可用从节点
2、较难支持在线扩容,在集群容量达到上限时会变得很复杂
3、集群中只有一个主节点,当写操作并发量特别大的时候,并无法缓解写的操作的压力
模式三、集群(clasuer) 3.0之后
为了解决Redis高可用模式下集群动态扩容困难,写操作并发瓶颈问题,在3.0之后redis推出了Redis-Cluster集群模式
redis-cluster采用务中心结构,每个节点保存各自的数据和整个集群的状态,每个节点都和其他的所有节点连接,客户端连接任意主节点(master)可以对整个集群中数据进行读写,所有slave节点仅用于数据备份与故障转移
分布式存储
Redis-Cluster 集群采用分布式存储的机制,每个master以及其slave只存储自己节点下的数据,客户端与任意master进行读写操作,会通过cluster的集群算法路由到对应的机器上【一致性hash算法】
一致性hash算法:redis-cluster把所有的redis节点映射到[0-16383]solt上(不一定是平均分配,图中的物理机的哈希值为5461,10822,16383),每一次读写操作集群会计算key的hash值,然后根据哈希值选择对应机器进行读写
将[0-16383]slot进行首位相连,形成哈希环,对于每个redis节点会分配到一个值,该节点就负责自己节点值到上一节点值的所有slot值数据的存储
当集群要对一个key进行读写的时候,将key值计算出来的hash值向16384进行取模,将模值放入hash环,并向后寻找第一个redis的slot值,然后将key值存入redis上
通过这种方式,能够保证集群中存在多个master同时写操作,极大的降低了单节点高并发读写的压力
动态扩容
redis-cluster 集群的一致性哈希算法支持动态扩容。动态扩容在一致性算法中涉及到两个问题。slot桶的重新分配、数据转移
上图所示是一个三节点redis-cluster集群里,redis节点与key在哈希环上的映射关系,可以看出key1(16121)
,key2(2200)
两个key存储在redis1
上,key 3 4 5 6
四个key会存储在redis2
上,而redis3
只存储了key7
,当我们需要往集群中新增一台redis,如果改变了全部redis分配的slot,那么数据的转移会涉及到整个集群,那将是灾难性的
在一致性哈希算法下,会将新的redis节点计算出哈希值,放入哈希环中,这时,redis2中H(key) <= 7866 的所有key值会进行转移,转移到redis4中,而当我们需要在新集群中删除其中一台redis2,redis中的所有数据会根据算法迁移到redis3上存储
一致性哈希算法在保持了单调性的同时,还将数据的迁移达到了最小,这样的算法对分布式集群来说是非常合适的,避免了大量数据迁移,减少了服务器的压力
故障发现与转移
Cluster集权在运行时所有的redis节点之间会通过ping/pong消息实现节点通信,消息不但传输节点槽信息,也能传播节点状态,主从状态,节点故障等。
当集群中某一节点出现问题,集群会通过消息进行发现,与sentinel模式相同,节点故障在集群中也会经过主观下线,客观下线的过程,但是cluster集群中,并不需要sentinel来进行节点监控与故障转移,而是由集群中的master们来处理的
redis集群安装
1、在/uar/local/创建redis-cluster目录
mkdir redis-cluster
2、在redis-cluster目录下创建节点目录
mkdir 7001 7002 7003 7004 7005 7006
3、复制一份redis.conf文件到7001节点目录
cp redis.conf ../redis-cluster/7001
4、修改redis-cluster/7001/redis.conf
bind 0.0.0.0
daemonize yes (设置后台运行redis)
cluster-enabled yes (开启集群)
cluster-node-timeout 15000 (设置请求超时时间,默认为15秒,可以自行修改)
appendonly yes (aop日志开启,会每次进行写操作都记录一条日志)
port 7001(和目录端口一致)
pidfile /var/run/redis_7001.pid
dbfilename dump_7001.rdb
appendfilename "appendonly_7001.aof"
cluster-config-file nodes_7001.conf
# 设置密码
masterauth 123456
requirepass 123456
5、修改完成后,把redis.conf 文件复制到其他节点中,并修改不同端口部分:
# 当前目录:redis-cluster/7001/
cp redis.conf ../7002/
cp redis.conf ../7003/
cp redis.conf ../7004/
cp redis.conf ../7005/
cp redis.conf ../7006/
6、编辑群起脚本 start-all.sh
#! /bin/bash
REDIS_HOME=/usr/local/redis-cluster
cd $REDIS_HOME/7001 && ./redis-server redis.conf
cd $REDIS_HOME/7002 && ./redis-server redis.conf
cd $REDIS_HOME/7003 && ./redis-server redis.conf
cd $REDIS_HOME/7004 && ./redis-server redis.conf
cd $REDIS_HOME/7005 && ./redis-server redis.conf
cd $REDIS_HOME/7006 && ./redis-server redis.conf
7、赋予权限
chmod 755 start-all.sh
注:查看进程是否启动,并确定端口:
ps -ef|grep redis|grep cluster
8、创建集群
./redis-cli -a 123456 --cluster create 192.168.136.152:7001 192.168.136.152:7002 192.168.136.152:7003 192.168.136.152:7004 192.168.136.152:7005 192.168.136.152:7006 --cluster-replicas 1
--cluster-replicas 1 :表示一个master挂载几个slave,至少需要3个master,此处设置为1 至少需要 6节点,如果设置为2 至少需要9个节点。
9、关闭集群
#! /bin/bash
REDIS_HOME=/usr/local/redis-cluster
cd $REDIS_HOME/7001 && ./redis-cli -c -h 192.168.136.152 -p 7001 shutdown
cd $REDIS_HOME/7002 && ./redis-cli -c -h 192.168.136.152 -p 7002 shutdown
cd $REDIS_HOME/7003 && ./redis-cli -c -h 192.168.136.152 -p 7003 shutdown
cd $REDIS_HOME/7004 && ./redis-cli -c -h 192.168.136.152 -p 7004 shutdown
cd $REDIS_HOME/7005 && ./redis-cli -c -h 192.168.136.152 -p 7005 shutdown
cd $REDIS_HOME/7006 && ./redis-cli -c -h 192.168.136.152 -p 7006 shutdown
cd /usr/local/redis-cluster/7001 && rm -f `ls |grep _7001`
cd /usr/local/redis-cluster/7002 && rm -f `ls |grep _7002`
cd /usr/local/redis-cluster/7003 && rm -f `ls |grep _7003`
cd /usr/local/redis-cluster/7004 && rm -f `ls |grep _7004`
cd /usr/local/redis-cluster/7005 && rm -f `ls |grep _7005`
cd /usr/local/redis-cluster/7006 && rm -f `ls |grep _7006`
操作代码
JedisCluster
说明:在redis中一共有16384个Slot,每个节点负责一部分Slot,当对Key进行操作时,redis会通过CRC16计算出key对应的Slot,将Key映射到Slot所在节点上执行操作,相当于一个Map<Integer,Jedis>,根据slot值去找个Map中获取Jedis,然后使用传统Jedis方式查询数据
弊端:大量数据查询是会大大增加tcp连接中交互往返的开销,每次查询会创建一次jedis连接
JedisCluster jedisCluster = null; // Redis 集群模式下
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
//设置过大,当客户端或服务端出现抖动,可能造成服务端连接过多
jedisPoolConfig.setMaxIdle(100);
jedisPoolConfig.setMaxTotal(100);
jedisPoolConfig.setMaxWaitMillis(100);
//Redis Cluster模式下,设置为True,每次操作多进行一次PING,影响性能
jedisPoolConfig.setTestOnReturn(false);
//同上
jedisPoolConfig.setTestOnBorrow(false);
Set<HostAndPort> jedisClusterNodes = new HashSet<>();
jedisClusterNodes.add(192.168.23.114,6379);
jedisClusterNodes.add(192.168.23.115,6379);
jedisClusterNodes.add(192.168.23.116,6379);
// timeout,password,maxAttempts 都是整数,根据需要配置
jedisCluster = new JedisCluster(jedisClusterNodes, timeout, timeout, maxAttempts, password, jedisPoolConfig);
//string
String value = jedisCluster.get(s);
//hash
Map<String, String> stringStringMap = jedisCluster.hgetAll(s);
//set
Set<String> sm = jedisCluster.smembers(s);
//zset
Set<Tuple> zr = jedisCluster.zrangeWithScores(s, 0, -1);
//list
List<String> lrange = jedisCluster.lrange(((String) v), 0, -1);
JedisCluster PipeLime
说明:pipeline对比redisCluster操作,先将要查询的数据进行收集,类似一个Map<String,List>,将同一个槽内的查询进行合并,相同槽位的key,使用同一个jedis.pipeline去执行命令,同时维护一个Map<String,JedisClient>的连接池,数据查询结束后,将client释放返回到池中
弊端:pipeline不是一个事务,不能保证里面的一组命令一起成功或者失败。
// 1、获取Jedis集群连接
JedisCluster jedisCluster = null; // Redis 集群模式下
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
//设置过大,当客户端或服务端出现抖动,可能造成服务端连接过多
jedisPoolConfig.setMaxIdle(100);
jedisPoolConfig.setMaxTotal(100);
jedisPoolConfig.setMaxWaitMillis(100);
//Redis Cluster模式下,设置为True,每次操作多进行一次PING,影响性能
jedisPoolConfig.setTestOnReturn(false);
//同上
jedisPoolConfig.setTestOnBorrow(false);
Set<HostAndPort> jedisClusterNodes = new HashSet<>();
jedisClusterNodes.add(192.168.23.114,6379);
jedisClusterNodes.add(192.168.23.115,6379);
jedisClusterNodes.add(192.168.23.116,6379);
// timeout,password,maxAttempts 都是整数,根据需要配置
jedisCluster = new JedisCluster(jedisClusterNodes, timeout, timeout, maxAttempts, password, jedisPoolConfig);
// 2、
JedisClusterPipeLineUtil2 pipelined = JedisClusterPipeLineUtil2.pipelined(jedisCluster);
// 刷新集群信息
pipelined.refreshNodesInfo();
//string
pipelined.get(s);
//hash
pipelined.hgetAll(s);
//set
pipelined.smembers(s);
//zset
pipelined.zrangeWithScores(s, 0, -1);
//list
pipelined.lrange(s, 0, -1);
// 执行pipelined代码
List<Map<String, Object>> result = pipelined.syncAndReturnAll();
JedisClusterPipeLineUtil2.java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.*;
import redis.clients.jedis.exceptions.JedisMovedDataException;
import redis.clients.jedis.exceptions.JedisRedirectionException;
import redis.clients.util.JedisClusterCRC16;
import redis.clients.util.SafeEncoder;
public class JedisClusterPipeLineUtil2 extends PipelineBase implements Closeable {
private static Logger log = LoggerFactory.getLogger(JedisClusterPipeLineUtil2.class);
private final Queue<Client> orderedClients = new LinkedList<Client>();
// 顺序调用的所有key
private List<String> keys = new ArrayList<>();
/**
* 一次pipeline过程中使用到的jedis缓存
*/
private final Map<JedisPool, Jedis> poolToJedisMap = new HashMap<>();
private boolean hashDataInBuff = false; // 查看数据是否再缓存区
private final JedisSlotBasedConnectionHandler connectionHandler;
private final JedisClusterInfoCache clusterInfoCache;
private static final Field SLOT_BASED_CONNECTION_HANDLER_FIELD;
private static final Field CLUSTER_INFO_CACHE_FIELD;
// 是否因为集群信息更改导致重启
private boolean errorCountReset = false;
static {
SLOT_BASED_CONNECTION_HANDLER_FIELD = getField(BinaryJedisCluster.class, "connectionHandler");
CLUSTER_INFO_CACHE_FIELD = getField(JedisClusterConnectionHandler.class, "cache");
}
public JedisClusterPipeLineUtil2(JedisCluster jedisCluster) {
this.connectionHandler = getValue(jedisCluster, SLOT_BASED_CONNECTION_HANDLER_FIELD);
this.clusterInfoCache = getValue(connectionHandler, CLUSTER_INFO_CACHE_FIELD);
}
private static Field getField(Class<?> cls, String fieldName) {
log.info("调用 getField 方法");
try {
Field field = cls.getDeclaredField(fieldName);
field.setAccessible(true);
return field;
} catch (NoSuchFieldException | SecurityException e) {
throw new CommonException(BaseEnum.QUERY_SQL_ERROR);
}
}
private static <T> T getValue(Object obj, Field field) {
log.info("调用 getValue 方法");
try {
return (T) field.get(obj);
} catch (IllegalArgumentException | IllegalAccessException e) {
// throw new RuntimeException(e);
throw new CommonException(BaseEnum.QUERY_SQL_ERROR);
}
}
@Override
protected Client getClient(String key) {
keys.add(key);
log.info("调用 getClient方法");
Client client = getClient(SafeEncoder.encode(key));
orderedClients.add(client);
return client;
}
@Override
protected Client getClient(byte[] key) {
Client client;int slot = JedisClusterCRC16.getSlot(key);
JedisPool pool = clusterInfoCache.getSlotPool(slot);
Jedis borrowedJedis = poolToJedisMap.get(pool);
if (null == borrowedJedis) {
// 获取客户端连接 存储到容器中去
borrowedJedis = pool.getResource();
poolToJedisMap.put(pool, borrowedJedis);
}
client = borrowedJedis.getClient();
// 如果已经存在直接返回
hashDataInBuff = true;
return client;
}
@Override
public void close() {
clean();
// 清除客户端队列
orderedClients.clear();
// 关闭缓存的jedis连接
for (Jedis jedis : poolToJedisMap.values()) {
if (hashDataInBuff){
flushCacheData(jedis);
}
jedis.close();
}
// 清除jedis连接缓存数据
poolToJedisMap.clear();
// 还原初始状态
hashDataInBuff = false;
}
private void flushCacheData(Jedis jedis){
try{
jedis.getClient().getAll();
}catch (RuntimeException ex){
// 保证代码高可用
ex.printStackTrace();
log.error("刷新jedis连接信息超时");
}
}
public List<Map<String,Object>> syncAndReturnAll() {
log.info("调用 syncAndReturnAll 方法");
List<Object> formatted = new ArrayList<>();
innerSync(formatted);
List<Map<String,Object>> objects = new ArrayList<>();
if (formatted.size() == keys.size()){
for (int i = 0; i < keys.size(); i++) {
HashMap<String, Object> objectObjectHashMap = new HashMap<>();
objectObjectHashMap.put(keys.get(i),formatted.get(i));
objects.add(objectObjectHashMap);
}
}
return objects;
}
private void innerSync(List<Object> formatted) {
HashSet<Client> clientSet = new HashSet<>();
try{
for (Client client : orderedClients) {
Object response = generateResponse(client.getOne()).get();
formatted.add(response);
// size相同说明所有的client都已经添加了,不用再调用add方法了 注:此方法在刷新集群信息之后
if (clientSet.size() != poolToJedisMap.size()){
clientSet.add(client);
}
}
}catch (JedisRedirectionException ex){
// 捕获因为集群信息更改导致查询数据失败的问题
if (ex instanceof JedisMovedDataException){
refreshNodesInfo();
if (!errorCountReset){
innerSync(formatted);
errorCountReset = true;
}
}
} finally {
if (clientSet.size()!= poolToJedisMap.size()){
// 所有还没有执行过的client要保证执行刷新,防止放回线程池后面被污染
for (Jedis jedis : poolToJedisMap.values()) {
if (clientSet.contains(jedis.getClient())) {
continue;
}
flushCacheData(jedis);
}
}
hashDataInBuff = false;
close();
}
}
/**
* 根据jedisCluster实例生产对于的JedisClusterPipeline
*/
public static JedisClusterPipeLineUtil2 pipelined(JedisCluster jedisCluster) {
log.info("创建 JedisClusterPipLineUtils对象");
// 清除请求队列
JedisClusterPipeLineUtil2 pipeline = new JedisClusterPipeLineUtil2(jedisCluster);
return pipeline;
}
/**
* 由于集群存在节点的动态添加删除 client不能实时感知 这个命令就是手动刷新solt槽的集群信息
*/
public void refreshNodesInfo() {
connectionHandler.renewSlotCache();
}
}
LettucePipeLine
介绍:Lettuce是一个高性能基于
Java编写的
Redis驱动框架,底层集成了
Project Reactor提供天然的反应式编程,通信框架集成了
Netty使用了非阻塞
IO,
5.x版本之后融合了
JDK1.8的异步编程特性,在保证高性能的同时提供了十分丰富易用的
API
Set<RedisURI> uris = new HashSet<>();
RedisURI.Builder builder = RedisURI.builder()
.withHost(192.168.136.131)
.withPassword("xxxx01")
.withPort(7890);
uris.add(builder.build());
// 设置redis连接池
ClientResources resources = DefaultClientResources.builder()
.ioThreadPoolSize(4) //IO线程数
.computationThreadPoolSize(4) // 任务线程数
.build();
// 创建连接
RedisClusterClient client = RedisClusterClient.create(resources,uris);
// 开启 自适应集群拓扑刷新和周期拓扑刷新
ClusterTopologyRefreshOptions clusterTopologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
.enableAdaptiveRefreshTrigger(
ClusterTopologyRefreshOptions.RefreshTrigger.MOVED_REDIRECT,
ClusterTopologyRefreshOptions.RefreshTrigger.PERSISTENT_RECONNECTS,
ClusterTopologyRefreshOptions.RefreshTrigger.UNKNOWN_NODE,
ClusterTopologyRefreshOptions.RefreshTrigger.ASK_REDIRECT
)
.adaptiveRefreshTriggersTimeout(Duration.ofSeconds(3))
.build();
// 配置集群选项,自动重连,最多重连次数
ClusterClientOptions build = ClusterClientOptions.builder()
.topologyRefreshOptions(clusterTopologyRefreshOptions)
.maxRedirects(maxAttempts)
.build();
client.setOptions(build);
client.setDefaultTimeout(Duration.ofSeconds(timeout));
StatefulRedisClusterConnection<String, String> connection = client.connect();
RedisAdvancedClusterAsyncCommands<String, String> async = connection.async();
List<Map<String, RedisFuture>> list = new ArrayList<>();
// 关闭自动提交刷新
async.setAutoFlushCommands(false);
//string
list.add(async.get(s));
//hash
list.add(async.hgetAll(s);
//set
list.add(async.smembers(s));
//zset
list.add(async.zrangeWithScores(s, 0, -1));
//list
list.add(async.lrange(s, 0, -1));
// 执行pipelined代码
async.flushCommands();
for(li:list){
System.out.println(li.getValue().get());
}