Redis原理简介及集群搭建以及springboot集成使用

  • 1. 简介
  • 2. 原理
  • 2.1 Redis的单线程和高性能
  • 2.2 Redis持久化
  • 3. 集群
  • 3.1 主从模式
  • 3.2 哨兵模式
  • 3.3 集群模式
  • 4. 集群搭建
  • 4.1 安装环境
  • 4.2 单机安装
  • 4.3 安装ruby
  • 4.4 集群安装
  • 5. 客户端集成
  • 5.1 springboot 集成 jedis
  • 5.2 springboot集成lettuce
  • 6. 参考文献


最近参与项目中使用Redis,其一是作为服务缓存的作用,其二结合websocket做发布订阅消息使用;本文结合项目对Redis集群搭建以及实际使用情况以及结合相关资料文献,对Redis的信息介绍、相关原理、集群搭建、springboot客户端集成等方面进行汇总总结整理,以便后续持续深入的学习和为读者提供使用思路。文中不免疏漏与不足之处,望读者于评论处给予指正,不胜感激!

1. 简介

(1)介绍

Redis:Remote Dictionary Server(远程字典服务),是一款内存高速缓存数据库,是完全开源免费的,
	  用C语言编写的、遵守BSD协议,高性能的(key/value)分布式内存数据库,基于内存运行并支持持久化的NoSQL数据库。
Redis的出现很大程度补偿了memcached这类key/value存储的不足,在部分场合可以对关系数据库起到很好的补充作用。
Redis提供了Java,C/C++,C#,PHP,JavaScript,Perl,Object-C,Python,Ruby,Erlang等客户端,使用很方便。

(2)优势特点(与memcache对比)
存储方式上:

Memecache把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小。
Redis有部份存在硬盘上,这样能保证数据的持久性。

数据支持类型上:

Memcache对数据类型支持相对简单。

Redis有复杂的数据类型:包括string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)和hash(哈希类型)。
这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。
在此基础上,redis支持各种不同方式的排序。与memcached一样,为了保证效率,数据都是缓存在内存中。
区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。

使用底层模型上:

它们之间底层实现方式 以及与客户端之间通信的应用协议不一样。Redis直接自己构建了VM机制 ,
因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。

value大小上:

redis最大可以达到1GB,而memcache只有1MB

(3)适用场景
1)会话缓存(Session Cache)

最常用的一种使用Redis的情景是会话缓存(session cache)。
	用Redis缓存会话比其他存储(如Memcached)的优势在于:Redis提供持久化。

2)全页缓存(FPC)

Redis还提供很简便的FPC平台。即使重启了Redis实例,
因为有磁盘的持久化,用户也不会看到页面加载速度的下降。

3)队列

Reids在内存存储引擎领域的一大优点是提供 list 和 set 操作,
这使得Redis能作为一个很好的消息队列平台来使用。

4)排行榜/计数器

Redis在内存中对数字进行递增或递减的操作实现的非常好。集合(Set)和有序集合(Sorted Set)。

5)发布/订阅

Redis自带的list类型(lpush和rpop或者brpop,rpush和lpop或者blpop)---blpop和brpop是阻塞读取。
发布/订阅模式(publish channel message 和 subscribe channel [channel ...] 
        或者 psubscribe pattern [pattern ...] 通配符订阅多个频道)

2. 原理

2.1 Redis的单线程和高性能

Redis是单线程,避免了多线程的切换(上下文切换)性能损耗问题,所有的数据都在内存中,
所有的运算都是内存级别的运算(纳秒),对于那些耗时的指令(比如keys),一定要谨慎使用,一不小心就可能会导致 Redis 卡顿;

单线程的Redis连接多并发的客户端

Redis的IO多路复用:redis利用epoll来实现IO多路复用,将连接信息和事件放到队列中,
依次放到文件事件分派器,事件分派器将事件分发给事件处理器。

2.2 Redis持久化

安装了redis之后,所有的配置都是在redis.conf文件中,里面保存了RDB和AOF两种持久化机制的各种配置。

RDB快照(snapshot)

RDB是把当前内存中的数据集快照写入磁盘,也就是 Snapshot 快照(数据库中所有键值对数据)。
恢复时是将快照文件直接读到内存里。RDB 有两种触发方式,分别是自动触发和手动触发。

自动触发:在 redis.conf 配置文件中的 SNAPSHOTTING 模块下配置:
save 900 1:表示900 秒内如果至少有 1 个 key 的值变化,则保存
save 300 10:表示300 秒内如果至少有 10 个 key 的值变化,则保存
save 60 10000:表示60 秒内如果至少有 10000 个 key 的值变化,则保存

手动触发:手动触发Redis进行RDB持久化的命令有两种:
(1)save:该命令会阻塞当前Redis服务器,执行save命令期间,Redis不能处理其他命令,直到RDB过程完成为止。显然该命令对于内存比较大的实例会造成长时间阻塞,这是致命的缺陷,为了解决此问题,Redis提供了第二种方式。
(2)bgsave:执行该命令时,Redis会在后台异步进行快照操作,快照同时还可以响应客户端请求。具体操作是Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短。
基本上 Redis 内部所有的RDB操作都是采用 bgsave 命令。
ps:执行执行 flushall 命令,也会产生dump.rdb文件,但里面是空的.

AOF(append-only file)

Append Only File,AOF会保存服务器执行的所有写操作到日志文件中,在服务重启以后,会执行这些命令来恢复数据。
日志文件默认为appendonly.aof,Redis以Redis协议格式将命令保存至aof日志文件末尾,aof文件还会被重写,使aof文件的体积不会大于保存数据集状态所需要的实际大小
默认情况下,aof没有被开启。需要在redis.conf开启
appendonly yes
日志文件名
appendfilename "appendonly.aof"
日志文件所在目录(RDB日志文件也是在这里)
dir ./
fsync持久化策略
appendfsync everysec
策略:always:命令写入aof_buf后立即调用系统fsync操作同步到AOF文件,fsync完成后线程返回。这种情况下,每次有写命令都要同步到AOF文件,硬盘IO成为性能瓶颈,Redis只能支持大约几百TPS写入,严重降低了Redis的性能;即便是使用固态硬盘(SSD),每秒大约也只能处理几万个命令,而且会大大降低SSD的寿命。
no:命令写入aof_buf后调用系统write操作,不对AOF文件做fsync同步;同步由操作系统负责,通常同步周期为30秒。这种情况下,文件同步的时间不可控,且缓冲区中堆积的数据会很多,数据安全性无法保证。
everysec:命令写入aof_buf后调用系统write操作,write完成后线程返回;fsync同步文件操作由专门的线程每秒调用一次。everysec是前述两种策略的折中,是性能和数据安全性的平衡,因此是Redis的默认配置,也是我们推荐的配置。

3. 集群

Redis集群模式有三种:主从模式、哨兵模式、集群模式

3.1 主从模式

Redis 提供了复制(replication)功能,可以实现当一台数据库中的数据更新后,自动将更新的数据同步到其他数据库上。
在复制的概念中,数据库分为两类,一类是主数据库(master),另一类是从数据库(slave)。
主数据库可以进行读写操作,当写操作导致数据变化时会自动将数据同步给从数据库。而从数据库一般是只读的,并接受主数据库同步过来的数据。
一个主数据库可以拥有多个从数据库,而一个从数据库只能拥有一个主数据库。

3.2 哨兵模式

在主从模式中,当主数据库遇到异常中断服务后,开发者可以通过手动的方式选择一个从数据库来升格为主数据库,以使得系统能够继续提供服务。
然而整个过程相对麻烦且需要人工介入,难以实现自动化。 为此,Redis 2.8中提供了哨兵工具来实现自动化的系统监控和故障恢复功能。
哨兵的作用就是监控redis主、从数据库是否正常运行,主出现故障自动将从数据库转换为主数据库。

3.3 集群模式

使用哨兵模式,redis每个实例也是全量存储,每个redis存储的内容都是完整的数据,浪费内存且有木桶效应。
为了最大化利用内存,可以采用集群,就是分布式存储。即每台redis存储不同的内容。
使用集群,只需要将每个数据库节点的cluster-enable配置打开即可。
每个集群中至少需要三个主数据库才能正常运行。每个实例使用不同的配置文件,主从不用配置,集群会自己选。
(1)redis集群采用P2P模式,是完全去中心化的,不存在中心节点或者代理节点;
(2)redis集群是没有统一的入口的,客户端(client)连接集群的时候连接集群中的任意节点(node)即可,集群内部的节点是相互通信的(PING-PONG机制),每个节点都是一个redis实例;
(3)为了实现集群的高可用,即判断节点是否健康(能否正常使用),redis-cluster有这么一个投票容错机制:如果集群中超过半数的节点投票认为某个节点挂了,那么这个节点就挂了(fail)。
(4)判断集群是否挂了的方法:如果集群中任意一个节点挂了,而且该节点没有从节点(备份节点),那么这个集群就挂了。
(5)任意一个节点挂了(没有从节点)这个集群就挂了:因为集群内置了16384个slot(哈希槽),并且把所有的物理节点映射到了这16384[0-16383]个slot上,或者说把这些slot均等的分配给了各个节点。
当需要在Redis集群存放一个数据(key-value)时,redis会先对这个key进行crc16算法,然后得到一个结果。再把这个结果对16384进行求余,这个余数会对应[0-16383]其中一个槽,进而决定key-value存储到哪个节点中。
所以一旦某个节点挂了,该节点对应的slot就无法使用,那么就会导致集群无法正常工作。
(6)综上所述,每个Redis集群理论上最多可以有16384个节点。

4. 集群搭建

4.1 安装环境

操作系统: ubuntu-20.04.2-live-server-amd64
Redis版本: redis-6.2.4

4.2 单机安装

(1)下载redis安装包,解压文件;https://redis.io/download
(2)编译redis源文件
		1)安装gcc
		判断是否安装gcc: 	gcc --version
		安装gcc:			sudo apt-get install build-essential	
		2)编译
		进入到解压缩后的redis文件目录(此时可以看到Makefile文件);
		执行命令: make    
		编译成功提示:Hint: It's a good idea to run 'make test' ;)
		3)把编译好的redis源文件安装到/usr/local/redis目录下,如果/local目录下没有redis目录,会自动新建redis目录;
		执行命令: make install PREFIX=/usr/local/redis
		如下提示成功:Hint: It's a good idea to run 'make test' ;
		
(3)启动redis
		进入/usr/local/redis/bin目录,
		执行: ./redis-server
		
(4)调整配置,后台启动
		把解压缩的redis文件下的redis.conf文件复制到/usr/local/redis/bin目录下,
		后端启动配置
		daemonize yes;
		允许远程连接
		# bind 127.0.0.1
		protected-mode no
		启动:在/bin目录下通过./redis-server redis.conf启动redis
		
(5)验证启动是否成功
		ps -ef | grep redis

4.3 安装ruby

执行命令: sudo apt install ruby

4.4 集群安装

(0)redis单机实例调整配置
	cluster-enabled yes
(1)ruby安装redis
	gem install redis
(2)创建集群
	./redis-cli --cluster create 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 127.0.0.1:7006 --cluster-replicas 1
(3)使用 info 命令指定集群上任一节点的地址便可以查看集群状态
	./redis-cli --cluster info 127.0.0.1:7001
	查看集群信息
	./redis-cli -c -h 127.0.0.1 -p 7001 cluster info
	查看集群节点信息
	./redis-cli -c -h 127.0.0.1 -p 7001 cluster nodes
(4)redis集群密码设置
	redis.conf配置文件增加配置项
	masterauth passwd123 
	requirepass passwd123 
	客户端访问增加 -a 密码 选项,例如
	./redis-cli --cluster info 127.0.0.1:7001 -a passwd123

5. 客户端集成

Redis官方推荐的Java客户端有Jedis、Lettuce和Redisson三种;

概念:

Jedis:是Redis的Java实现客户端,提供了比较全面的Redis命令的支持,
Redisson:实现了分布式和可扩展的Java数据结构。
Lettuce:高级Redis客户端,用于线程安全同步,异步和响应使用,支持集群,Sentinel,管道和编码器。

优点:

Jedis:比较全面的提供了Redis的操作特性
Redisson:促使使用者对Redis的关注分离,提供很多分布式相关操作服务,例如,分布式锁,分布式集合,可通过Redis支持延迟队列
Lettuce:主要在一些分布式缓存框架上使用比较多

可伸缩:

Jedis:使用阻塞的I/O,且其方法调用都是同步的,程序流需要等到sockets处理完I/O才能执行,不支持异步。Jedis客户端实例不是线程安全的,所以需要通过连接池来使用Jedis。
Redisson:基于Netty框架的事件驱动的通信层,其方法调用是异步的。Redisson的API是线程安全的,所以可以操作单个Redisson连接来完成各种操作
Lettuce:基于Netty框架的事件驱动的通信层,其方法调用是异步的。Lettuce的API是线程安全的,所以可以操作单个Lettuce连接来完成各种操作

目前项目中主要使用了Jedis和Lettuce,本文以下主要介绍springboot项目集成使用Jedis和Lettuce客户端的相关方法。后续出文章详细对比三种客户端的特征以及在java项目中相关的集成使用方案。

5.1 springboot 集成 jedis

引入jar包

compile 'redis.clients:jedis:3.4.0'

配置信息

#####redis 集群的配置  ########
spring.redis.cluster.nodes=192.168.65.150:7001, 192.168.65.150:7002,192.168.65.150:7003, 192.168.65.150:7004, 192.168.65.150:7005, 192.168.65.150:7006
spring.redis.cluster.max-redirects=6
redis.password=12345
redis.timeout=10000
redis.maxIdle=300
#控制一个pool可分配多少个jedis实例,用来替换上面的redis.maxActive,如果是jedis 2.4以后用该属性
redis.maxTotal=1000
#最大建立连接等待时间。如果超过此时间将接到异常。设为-1表示无限制。
redis.maxWaitMillis=1000
#连接的最小空闲时间 默认1800000毫秒(30分钟)
redis.minEvictableIdleTimeMillis=300000
#每次释放连接的最大数目,默认3
redis.numTestsPerEvictionRun=1024
#逐出扫描的时间间隔(毫秒) 如果为负数,则不运行逐出线程, 默认-1
redis.timeBetweenEvictionRunsMillis=30000
#是否在从池中取出连接前进行检验,如果检验失败,则从池中去除连接并尝试取出另一个
redis.testOnBorrow=true
#在空闲时检查有效性, 默认false
redis.testWhileIdle=true

配置信息实例化RedisTemplate

@Configuration
public class RedisClusterConfig {

@Value("${spring.redis.cluster.nodes}")
private String clusterNodes;

@Value("${spring.redis.cluster.max-redirects}")
private int maxRedirects;

@Value("${redis.password}")
private String password;

@Value("${redis.timeout}")
private int timeout;

@Value("${redis.maxIdle}")
private int maxIdle;

@Value("${redis.maxTotal}")
private int maxTotal;

@Value("${redis.maxWaitMillis}")
private int maxWaitMillis;

@Value("${redis.minEvictableIdleTimeMillis}")
private int minEvictableIdleTimeMillis;

@Value("${redis.numTestsPerEvictionRun}")
private int numTestsPerEvictionRun;

@Value("${redis.timeBetweenEvictionRunsMillis}")
private int timeBetweenEvictionRunsMillis;

@Value("${redis.testOnBorrow}")
private boolean testOnBorrow;

@Value("${redis.testWhileIdle}")
private boolean testWhileIdle;

@Bean
public JedisPoolConfig getJedisPoolConfig() {
    JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
    jedisPoolConfig.setMaxIdle(maxIdle);
    jedisPoolConfig.setMaxTotal(maxTotal);
    jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
    jedisPoolConfig.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
    jedisPoolConfig.setNumTestsPerEvictionRun(numTestsPerEvictionRun);
    jedisPoolConfig.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
    jedisPoolConfig.setTestOnBorrow(testOnBorrow);
    jedisPoolConfig.setTestWhileIdle(testWhileIdle);
    return jedisPoolConfig;
}

/**
 * Redis集群的配置
 *
 * @return RedisClusterConfiguration
 * @throws
 */
@Bean
public RedisClusterConfiguration redisClusterConfiguration() {
    RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration();
    String[] serverArray = clusterNodes.split(",");
    Set<RedisNode> nodes = new HashSet<>();
    for (String ipPort : serverArray) {
        String[] ipAndPort = ipPort.split(":");
        nodes.add(new RedisNode(ipAndPort[0].trim(), Integer.parseInt(ipAndPort[1])));
    }
    redisClusterConfiguration.setClusterNodes(nodes);
    redisClusterConfiguration.setMaxRedirects(maxRedirects);
    redisClusterConfiguration.setPassword(RedisPassword.of(password));
    return redisClusterConfiguration;
}

/**
 * @Description:redis连接工厂类
 */
@Bean
public JedisConnectionFactory jedisConnectionFactory() {
    //集群模式
    JedisConnectionFactory factory = new JedisConnectionFactory(redisClusterConfiguration(), getJedisPoolConfig());
    factory.setDatabase(0);
    factory.setTimeout(timeout);
    factory.setUsePool(true);
    return factory;
}


/**
 * 实例化 RedisTemplate 对象
 */
@Bean
public RedisTemplate<String, Object> redisTemplate() {
    RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
    initDomainRedisTemplate(redisTemplate);
    return redisTemplate;
}

/**
 * 设置数据存入 redis 的序列化方式,并开启事务
 * 使用默认的序列化会导致key乱码
 */
private void initDomainRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
    redisTemplate.setKeySerializer(new StringRedisSerializer());
    Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
    objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
    redisTemplate.setDefaultSerializer(jackson2JsonRedisSerializer);
    redisTemplate.setEnableTransactionSupport(false);
    redisTemplate.setConnectionFactory(jedisConnectionFactory());
}
}

5.2 springboot集成lettuce

引入jar包

compile ('org.springframework.boot:spring-boot-starter-data-redis')

配置信息

#各redis节点信息
spring.redis.cluster.nodes=192.168.65.150:7001, 192.168.65.150:7002,192.168.65.150:7003, 192.168.65.150:7004, 192.168.65.150:7005, 192.168.65.150:7006
#密码
spring.redis.password=12345
spring.redis.cluster.max-redirects=6
spring.redis.lettuce.pool.max-active=600
spring.redis.lettuce.pool.max-idle=300
spring.redis.lettuce.pool.min-idle=50
spring.redis.lettuce.pool.max-wait=1000

配置信息实例化RedisTemplate

@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class RedisClusterConfig {

    @Autowired
    private RedisProperties redisProperties;

    @Bean(destroyMethod = "destroy")
    public LettuceConnectionFactory lettuceConnectionFactory(){
        if(null == redisProperties.getCluster() || null == redisProperties.getCluster().getNodes()){
            RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration(redisProperties.getHost(),redisProperties.getPort());
            configuration.setPassword(redisProperties.getPassword());
            return new LettuceConnectionFactory(configuration);
        }

        RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration(redisProperties.getCluster().getNodes());
        redisClusterConfiguration.setPassword(redisProperties.getPassword());
        redisClusterConfiguration.setMaxRedirects(redisProperties.getCluster().getMaxRedirects());
        GenericObjectPoolConfig genericObjectPoolConfig = new GenericObjectPoolConfig();
        genericObjectPoolConfig.setMaxTotal(redisProperties.getLettuce().getPool().getMaxActive());
        genericObjectPoolConfig.setMaxIdle(redisProperties.getLettuce().getPool().getMaxIdle());
        genericObjectPoolConfig.setMinIdle(redisProperties.getLettuce().getPool().getMinIdle());
        genericObjectPoolConfig.setMaxWaitMillis(redisProperties.getLettuce().getPool().getMaxWait().getSeconds());
        
        //  使用ClusterTopologyRefreshOptions来设置拓扑自动刷新,定时刷新
        ClusterTopologyRefreshOptions clusterTopologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
                .enableAllAdaptiveRefreshTriggers()
                .enableAdaptiveRefreshTrigger()
                .enablePeriodicRefresh(Duration.ofSeconds(10))
                .build();

        ClusterClientOptions clusterClientOptions = ClusterClientOptions.builder()
                .topologyRefreshOptions(clusterTopologyRefreshOptions).build();

        LettuceClientConfiguration lettuceClientConfiguration = LettucePoolingClientConfiguration.builder()
                .poolConfig(genericObjectPoolConfig)
                .readFrom(ReadFrom.REPLICA_PREFERRED)
                .clientOptions(clusterClientOptions).build();
        LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(redisClusterConfiguration,lettuceClientConfiguration);
        lettuceConnectionFactory.setShareNativeConnection(false);
        lettuceConnectionFactory.resetConnection();
        return lettuceConnectionFactory;
    }

    @Bean
    public RedisTemplate redisTemplate(LettuceConnectionFactory lettuceConnectionFactory){
        RedisTemplate<String,Object> template = new RedisTemplate<String,Object>();
        template.setConnectionFactory(lettuceConnectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        template.setDefaultSerializer(jackson2JsonRedisSerializer);
        template.setEnableTransactionSupport(false);
        return template;
    }
}

springboot集成jedis客户端连接使用redis集群,在redis节点掉线的情况下能够刷新连接节点;集成lettuce客户端时,需要设置拓扑自动刷新。

6. 参考文献

[1]. https://www.jianshu.com/p/4e6b7809e10a