中间件
redis
一、redis概述
redis与memcached的区别:
- Redis是一个开源的内存数据结构存储,用作数据库,缓存和消息代理;
- Memcached是一个免费的开源高性能分布式内存对象缓存系统,它通过减少数据库负载来加速动态Web应用程序。
memcached是高性能的分布式内存缓存服务器。一般使用目的是,通过缓存数据库查询结果,减少数据库访问次数,以提高动态web应用的速度、提高可扩展性。
redis是一个开源的内存数据库,它以键值对的形式存储数据。由于数据存储在内存中,因此Redis的速度很快,但是每次重启Redis服务时,其中的数据也会丢失,因此,Redis也提供了持久化存储机制,将数据以某种形式保存在文件中,每次重启时,可以自动从文件加载数据到内存当中。
二、redis安装
相关知识:
redis默认的端口号为6379,默认有16个数据库,类似数组小标从0开始,初始默认使用0号数据库。
使用select 命令来切换数据库。例如 select 2 切换到2号数据库。
使用dbsize命令来查看当前数据库的key的数量。
使用flushdb命令来情空当前数据库中所有数据。
使用flushall命令来删除所以数据库中的数据。
Redis是单线程+多路IO复用技术。多路复用是指使用一个线程来检查多个文件描述符(Socket)的就绪状态,比如调用select和poll函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞直到超时。得到就绪状态后进行真正的操作可以在同一个线程里执行,也可以启动线程执行(比如使用线程池)。
三、redis数据结构
四、redis数据类型
4-1 key操作
4-2 string
String 是 redis 最基本的类型,你可以理解成与 Memcached 一模一样的类型,一个 key 对应一个value。
String 类型是二进制安全的,意思是 redis 的 string 可以包含任何数据。比如jpg图片或者序列化的对象。
String 类型是 Redis 最基本的数据类型,String 类型的值最大能存储 512MB。
String类型一般用于缓存、限流、计数器、分布式锁、分布式Session。
结构图:
相关命令:
4-3 list
Redis列表是简单的字符串列表,按照插入顺序排序(后进先出)。你可以添加一个元素到列表的头部(左边)或者尾部(右边)一个列表最多可以包含 2^32 - 1 个元素 (4294967295, 每个列表超过40亿个元素)。
List类型一般用于关注人、简单队列等。
结构图:
相关命令:
4-4 set
Redis 的 Set 是 String 类型的无序集合。集合中成员是唯一的,这就意味着集合中不能出现重复的数据。Redis 中集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。 集合中最大的成员数为 2^32 - 1 (4294967295, 每个集合可存储40多亿个成员)。
Set类型一般用于赞、踩、标签、好友关系等。
结构图:
相关命令:
- cursor :游标
- MATCH pattern :查询 Key 的条件
- Count count :返回的条数,默认值为 10
SCAN 是一个基于游标的迭代器,需要基于上一次的游标延续之前的迭代过程。SCAN 以 0作为游标,开始一次新的迭代,直到命令返回游标 0 完成一次遍历。此命令并不保证每次执行都返回某个给定数量的元素,甚至会返回 0 个元素,但只要游标不是 0,程序都不会认为 SCAN 命令结束,但是返回的元素数量大概率符合 Count 参数。另外,SCAN 支持模糊查询。
例:
SSCAN names 0 MATCH test* COUNT 10
#每次返回10条以test为前缀的key。
4-5 zset
Redis 有序集合和集合一样也是string类型元素的集合且不允许重复的成员。不同的是每个元素都会关联一个 double类型的分数 。redis正是通过分数来为集合中的成员进行从小到大的排序。有序集合的成员是唯一的,但分数(score)却可以重复。集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。 集合中最大的成员数为2^32 - 1 (4294967295, 每个集合可存储40多亿个成员)。
Zset类型一般用于排行榜等。
结构图:
相关命令:
4-6 Hash
Redis hash 是一个 string 类型的 field(字段)和 value(值) 的映射表,hash 特别适合用于存储对象。Redis 中每个 hash 可以存储 2^32 - 1 键值对(40多亿)
Hash类型一般用于存储用户信息、用户主页访问量、组合查询等。
结构图:
相关命令:
4-7 Bitmaps
介绍:现代计算机用二进制(位) 作为信息的基础单位, 1个字节等于8位, 例如“abc”字符串是由3个字节组成, 但实际在计算机存储时将其用二进制表示, “abc”分别对应的ASCII码分别是97、 98、 99, 对应的二进制分别是01100001、 01100010和01100011,如下图:
合理地使用操作位能够有效地提高内存使用率和开发效率。
Redis 6 中提供了 Bitmaps 这个“数据类型”可以实现对位的操作:
1)Bitmaps本身不是一种数据类型,实际上它就是字符串(key-value),但是它可以对字符串的位进行操作。
2)Bitmaps单独提供了一套命令,所以在Redis中使用Bitmaps和使用字符串的方法不太相同。 可以把Bitmaps想象成一个以位为单位的数组, 数组的每个单元只能存储0和1, 数组的下标在Bitmaps中叫做偏移量。
相关命令:
1.setbit
这个命令用于设置Bitmaps中某个偏移量的值(0或1),offset偏移量从0开始。格式如下:
setbit <key> <offset> <value>
例如,把每个独立用户是否访问过网站存放在Bitmaps中, 将访问的用户记做1,没有访问的用户记做0,用偏移量作为用户的id。
设置键的第offset个位的值(从0算起) , 假设现在有20个用户,userid=1,6,11,15,19的用户对网站进行了访问, 那么当前Bitmaps初始化结果如图:
注意:
很多应用的用户id以一个指定数字(例如10000) 开头, 直接将用户id和Bitmaps的偏移量对应势必会造成一定的浪费, 通常的做法是每次做setbit操作时将用户id减去这个指定数字。
在第一次初始化Bitmaps时, 假如偏移量非常大, 那么整个初始化过程执行会比较慢, 可能会造成Redis的阻塞。
2.getbit
这个命令用于获取Bitmaps中某个偏移量的值。格式为:
getbit <key> <offset>
获取键的第offset位的值(从0开始算)。例如获取id=6的用户是否在2022-07-18这天访问过, 返 回0说明没有访问过:
3.bitcount
这个命令用于统计字符串被设置为1的bit数。一般情况下,给定的整个字符串都会被进行计数,通过指定额外的 start 或 end 参数,可以让计数只在特定的位上进行。start 和 end 参数的设置,都可以使用负数值:比如 -1 表示最后一个位,而 -2 表示倒数第二个位,start、end 是指bit组的字节的下标数,二者皆包含。格式如下:
bitcount <key> [start end]
用于统计字符串从start字节到end字节比特值为1的数量。例如,统计id在第1个字节到第3个字节之间的独立访问用户数, 对应的用户id是11, 15, 19。
4.bitop
这个命令是一个复合操作, 它可以做多个Bitmaps的and(交集) 、 or(并集) 、 not(非) 、 xor(异或) 操作并将结果保存在destkey中。格式如下:
bitop and(or/not/xor) <destkey> [key…]
4-8 HyperLogLog
简介:在工作当中,我们经常会遇到与统计相关的功能需求,比如统计网站PV(PageView 页面访问量),可以使用Redis的incr、incrby轻松实现。
但像UV(UniqueVisitor,独立访客)、独立IP数、搜索记录数等需要去重和计数的问题如何解决?这种求集合中不重复元素个数的问题称为基数问题。
解决基数问题有很多种方案:
1)数据存储在MySQL表中,使用distinct count计算不重复个数
2)使用Redis提供的hash、set、bitmaps等数据结构来处理
以上的方案结果精确,但随着数据不断增加,导致占用空间越来越大,对于非常大的数据集是不切实际的。
为了能够降低一定的精度来平衡存储空间,Redis推出了HyperLogLog。
HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是:在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。
在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以HyperLogLog 不能像集合那样,返回输入的各个元素。
什么是基数?
比如数据集 {1, 3, 5, 7, 5, 7, 8},那么这个数据集的基数集为 {1, 3, 5 ,7, 8},基数(不重复元素) 为5。 基数估计就是在误差可接受的范围内,快速计算基数。
相关命令:
4-9 Geospatial
Redis 3.2 中增加了对GEO类型的支持。**GEO,Geographic,地理信息的缩写。该类型,就是元素的2维坐标,在地图上就是经纬度。**redis基于该类型,提供了经纬度设置,查询,范围查询,距离查询,经纬度Hash等常见操作。
相关命令:
五、配置文件
5-1 Unit
配置大小单位,开头定义了一些基本的变量单位,只支持 byte,不支持 bit。大小不敏感
5-2 includes
它类似于 JSP 中的 include,大多数实例可以把公共部分用到的配置文件提取来,然后通过include 把这个公共部分包含进来。
5-3 network(网络)
(1) bind
默认情况下 bind=127.0.0.1 只能接受本机的访问请求。在不写的情况下,无限制接受任何 IP 地址的访问。
生产环境需要填写你应用服务器的地址。由于服务器是需要远程访问的,所以需要将其注释掉。
注意:如果开启了protected-mode,那么在没有设定bind ip且没有设密码的情况下,Redis只允许接受本机的响应。
我们把 bind 127.0.0.1 -::1 它注释掉。
(2)protected-mode
我们如果希望远程访问,那么需要把它设置为 no 。
(3)port
默认端口号为 6379,可以在这里对它进行设置。
(4)tcp-backlog
设置 tcp 的 backlog,backlog 其实是一个连接队列,backlog队列总和 = 未完成三次握手队列 + 已经完成三次握手队列。
在高并发环境下你需要一个高 backlog 值来避免慢客户端连接问题。
注意:
Linux内核会将这个值减小到/proc/sys/net/core/somaxconn的值(128),所以需要确认增大/proc/sys/net/core/somaxconn和/proc/sys/net/ipv4/tcp_max_syn_backlog(128)两个值来达到想要的效果
(5) timeout
一个空闲的客户端维持多少秒会关闭,0表示关闭该功能。即永不关闭
(6)tcp-keepalive
对访问客户端的一种心跳检测,每 n 秒检测一次。
单位为秒,如果设置为0,则不会进行 Keepalive 检测,建议设置成 60。
5-4 general(通用)
(1)daemonize
是否为后台进程,即守护进程,用于后台启动,设置为yes。
(2)pidfile
存放pid文件的位置,每个实例会产生一个不同的pid文件。
(3)loglevel
指定日志记录级别,Redis 总共支持四个级别:debug、verbose、notice、warning,默认为notice。
四个级别根据使用阶段来选择,生产环境选择 notice 或者warning。
(4)logfile
指定日志文件名称
(5)databases
设定库的数量,默认16,默认数据库为0,可以使用 SELECT 命令在连接上指定数据库id。
5-5 security
当设置好密码后(即把 requirepass foobared 注解解开),然后使用客户端连接服务器后,在执行 set 命令时,提示需要获取权限。
[root@redis-101 bin]# redis-cli 127.0.0.1:6379> set k1 v2 (error) NOAUTH Authentication required.
此时需要使用 auth 命令来输入密码:
127.0.0.1:6379> auth foobared OK127.0.0.1:6379> set k1 v2 OK
5-6 clients
(1)maxclients
设置redis同时可以与多少个客户端进行连接。默认情况下为10000个客户端。如果达到了此限制,redis则会拒绝新的连接请求,并且向这些连接请求方发出“max number of clients reached”以作回应。
(2)maxmemory
建议必须设置,否则可能导致将内存占满,造成服务器宕机。
设置redis可以使用的内存量。一旦到达内存使用上限,redis将会试图移除内部数据,移除规则可以通过maxmemory-policy来指定。
如果redis无法根据移除规则来移除内存中的数据,或者设置了“不允许移除”,那么redis则会针对那些需要申请内存的指令返回错误信息,比如SET、LPUSH等。
但是对于无内存申请的指令,仍然会正常响应,比如GET等。如果你的redis是主redis(说明你的redis有从redis),那么在设置内存使用上限时,需要在系统中留出一些内存空间给同步队列缓存,只有在你设置的是“不移除”的情况下,才不用考虑这个因素
(3) maxmemory-policy
用于设置内存达到使用上限后的移除规则。有以下参数可设置:
-
volatile-lru:使用LRU(最近最少使用算法)算法移除key,只对设置了过期时间的键;(最近最少使用)
-
allkeys-lru:在所有集合key中,使用LRU算法移除key
-
volatile-random:在过期集合中移除随机的key,只对设置了过期时间的键
-
allkeys-random:在所有集合key中,移除随机的key
-
volatile-ttl:移除那些TTL值最小的key,即那些最近要过期的key
-
noeviction:不进行移除。针对写操作,只是返回错误信息
(4)maxmemory-samples
用于设置样本数量,LRU算法和最小TTL算法都并非是精确的算法,而是估算值,所以你可以设置样本的大小,redis默认会检查这么多个key并选择其中LRU的那个。
一般设置3到7的数字,数值越小样本越不准确,但性能消耗越小。
六、Jedis使用
在使用 Jedis 连接 Redis 之前,服务器需要做如下操作:
1.关闭防火墙
systemctl stop firewalld.service
systemctl disable firewalld.service
2.redis开启远程访问
1)把 bind 值注释
2)把 protected-mode 的值设置为 no
七、整合srpingboot
7-1 创建工程
7-2 引入依赖
<dependencies>
<!--变成子工程-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.1</version>
</dependency>
<!--redis启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.7.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.11.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<version>2.7.1</version>
</dependency>
</dependencies>
7-3 配置连接信息
application.yml
spring:
redis:
host: 192.168.124.131
port: 6379
password:
database: 0
# 连接超时时间,单位为毫秒
timeout: 180000
lettuce:
pool:
enabled: true
# 最大的活动数
max-active: 20
# 最大的空闲数
max-idle: 5
# 最大阻塞等待时间,如果为-1表示没有限制
max-wait: -1
7-4 编写配置类
package com.openlab.redis.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
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 java.time.Duration;
/**
* redis配置类
*/
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 配置序列化对象
RedisSerializer<String> serializer = 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);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
template.setConnectionFactory(factory);
// key序列化方式
template.setKeySerializer(serializer);
// value序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
// value hashmap序列化
template.setHashValueSerializer(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.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
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;
}
}
7-5 编写入口类
package com.openlab.redis;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class RedisApplication {
public static void main(String[] args) {
SpringApplication.run(RedisApplication.class, args);
}
}
7-6 功能测试
package com.openlab.redis.test;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
/*** redisTemplate.opsForValue() 获取的是 string类型
* * redisTemplate.opsForHash() 获取hash类型
* * redisTemplate.opsForSet() 获取set类型
* * redisTemplate.opsForList() 获取list类型
* * redisTemplate.opsForZSet() 获取zset类型
* * redisTemplate.opsForHyperLogLog() 获取hyperLogLog类型
* * redisTemplate.opsForGeo() 获取geo类型 */
@SpringBootTest
public class RedisApiTest {
@Autowired
private RedisTemplate redisTemplate;
@Test
void testRedis() {
/*//添加数据
redisTemplate.opsForValue().set("hello","world");*/
//获取数据
Object hello = redisTemplate.opsForValue().get("hello");
System.out.println(hello);
}
}
八、发布和订阅(目前来说没有消息队列功能好)
8-1 简介
Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息。Redis 客户端可以订阅任意数量的频道。下图展示了频道 channel1 ,以及订阅这个频道的三个客户端 —— client1 、client2 和 client3 之间的关系(观察者模式):
当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端:
8-2 常用命令
8-3 命令演示
我们打开一个客户端,然后执行如下命令来订阅一个通道:
在另一个客户端中执行消息发布
发布完成后,在第一个客户端中就可以看到所订阅的频道所传过来的消息。
8-4 案例演示
(1)配置发布订阅
修改 RedisConfig 类,在类中增加如下的配置:
package com.openlab.redis.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.openlab.redis.subscribe.Receiver;
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.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
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 java.time.Duration;
import java.util.concurrent.CountDownLatch;
/**
* redis配置类
*/
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
// 配置 RedisTemplate 模板对象
@Bean
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 配置序列化对象
RedisSerializer<String> serializer = 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);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
template.setConnectionFactory(factory);
// key序列化方式
template.setKeySerializer(serializer);
// value序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
// value hashmap序列化
template.setHashValueSerializer(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.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
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;
}
/*-------------------- 下面的配置是用于发布订阅,如果不使用这个功能可以删除 ------------------------*/
// 创建连接工厂
@Bean
RedisMessageListenerContainer container(RedisConnectionFactory factory, MessageListenerAdapter adapter) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(factory);
// 指定了一个消息的主题,叫 myTopic
container.addMessageListener(adapter, new PatternTopic("myTopic"));
return container;
}
// 绑定消息监听者和接收监听的方法
@Bean
public MessageListenerAdapter listenerAdapter(Receiver receiver) {
return new MessageListenerAdapter(receiver, "receiveMessage");
}
// 注册订阅者
@Bean
public Receiver receiver(CountDownLatch latch) {
return new Receiver(latch);
}
// 计数器,用来控制线程
@Bean
public CountDownLatch latch() {
return new CountDownLatch(1); // 指定了计数的次数 1
}
}
(2)编写消息接收者类
package com.openlab.redis.subscribe;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.concurrent.CountDownLatch;
/*
* 消息接收者类
* */
@Slf4j
public class Receiver {
private CountDownLatch latch;
@Autowired
public Receiver(CountDownLatch latch) {
this.latch = latch;
}
//用于接收消息的方法
public void receiveMessage(String message) {
log.info("接收到的消息是:<" + message + ">");
latch.countDown();
}
}
注意:为了使用日志,我们添加了如下的依赖:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
(3)编写消息发送者
package com.openlab.redis.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.RestController;
@RestController
public class SenderController {
@Autowired
private RedisTemplate redisTemplate;
@GetMapping("/send")
public String send(String msg) {
redisTemplate.convertAndSend("myTopic",msg);
return "消息发送成功";
}
}
(4)运行测试
启动项目,在浏览器中输入 http://localhost:8080/send?msg=hello 后就可以在 IDEA 控制台中看到如下信息:
九、事务管理
9-1 事务定义
Redis 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行(相当于隔离起来,加了锁),执行完之后才会释放锁。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
Redis 事务的主要作用就是串联多个命令防止别的命令插队。
9-2 Multi、Exec、Discard
从输入 Multi 命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入 Exec 后,Redis 会将之前的命令队列中的命令依次执行。
组队的过程中可以通过 discard 来放弃组队。
(1)组队成功,提交成功的情况:
(2)组队阶段报错,提交失败的情况:
(3)组队成功,提交失败的情况:
9-3 事务冲突问题
(1)问题场景
有一账户余额为 10000 元,现在三个请求,第一个请求想给金额减 8000 元,第二个请求想给金额减 5000 元,第三个请求想给金额减 1000 元。如果没有事务,可能会发生如下情况。
如果没有事务来控制的话,那么账户就会出现透支的问题。解决这个问题有以下两种方式。
(2)悲观锁
悲观锁(Pessimistic Lock)顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库就用到了很多这种锁机制,比如行锁,表锁,读锁,写锁等,都是在做操作之前先上锁。
这种方式能够解决透支问题,但是性能不高。
(3)乐观锁
乐观锁(Optimistic Lock)顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。
Redis就是利用这种check-and- set机制实现事务的。
(4)watch
在执行 multi 之前,先执行 watch key1 [key2]
可以监视一个(或多个) key ,如果在事务执行之前这个/些 key被其他命令所改动,那么事务将被打断。
为了演示这个功能,我们先设置一个 balance 的初始值为 10000。然后打开一个客户端,在两个客户端中都去监视 balance,同时也开启事务。
首先设置值:
在客户端 A :
首先在客户端 A 中监听 balance ,然后开记事务操作。同时,在客户端 B 中也监听 balance,然后开启事务。
当客户端 A 提交了事务后,客户端B也提交事务,会发现,客户端B的事务没有成功。
事务是一个整体,里面有一个不成功,都不会执行。
9-4 Redis事务的特性
Redis 的事务有以下三个特性:
- 单独的隔离操作
事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
- 没有隔离级别的概念
队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行。
- 不保证原子性
事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚。
9-5 案例演示
在电商项目中,经常会有秒杀活动,我们使用 Redis 来模拟秒杀功能。
假设有 10 个商品库存,现有 200 个用户来抢购这 10 个商品。
(1)搭建工程
引入依赖:
<dependencies>
<!--变成子工程-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.1</version>
</dependency>
<!--redis启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.7.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.11.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
</dependencies>
配置application.yml
spring:
redis:
host: 192.168.124.131
port: 6379
lettuce:
pool:
enabled: true
max-wait: -1
max-active: 8
max-idle: 8
timeout: 5000
配置RedisTemplate
package com.openlab.redis.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
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.connection.lettuce.LettuceConnectionFactory;
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 java.time.Duration;
/**
* redis配置类
*/
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
// Lettuce 配置方式
@Bean
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory
factory) {
// key序列化器
RedisSerializer<String> keySerializer = new StringRedisSerializer();
// value序列化器
Jackson2JsonRedisSerializer<Object> valSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
valSerializer.setObjectMapper(om);
// 配置redisTemplate
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
template.setConnectionFactory(factory);
template.setKeySerializer(keySerializer);
// key序列化
template.setValueSerializer(valSerializer);
// value序列化
template.setHashKeySerializer(keySerializer);
// Hash key序列化
template.setHashValueSerializer(valSerializer);
// Hash value序列化
template.afterPropertiesSet(); 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.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
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.openlab.redis;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class RedisApplication {
public static void main(String[] args) {
SpringApplication.run(RedisApplication.class,args);
}
}
秒杀页面:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8">
<title>秒杀页面</title>
</head>
<body>
<h1>外星人笔记本1元秒杀!!!</h1>
<form id="form" action="/seckill">
<input type="hidden" name="productId" value="1001">
<input type="button" id="seckillBtn" value="点击秒杀"/>
</form>
</body>
<script src="/js/jquery-3.1.0.js"></script>
<script>
$(function(){
$("#seckillBtn").click(function(){
var url = $("#form").attr("action");
$.post(url, $("#form").serialize(), function(data){
if(data == "false"){
alert("你手速慢了,敬请期待下次光临!" );
$("#seckillBtn").attr("disabled", true);
}
});
})
})
</script>
</body>
</html>
编辑控制类
package com.openlab.redis.contronller;
import lombok.extern.slf4j.Slf4j;
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.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Random;
@RestController
@Slf4j
public class SeckillController {
@Autowired
private RedisTemplate redisTemplate;
@GetMapping("/init")
public String init() {
// 初始化数据,模拟数据库中库存为 10 个商品,写入到 Redis中
redisTemplate.opsForValue().set("sk:1001:stock", 10);
log.info("===>>>库存初始化成功,库存数为:" + 10);
return "初始化库存成功";
}
@PostMapping("/seckill")
public boolean seckKill(@RequestParam("productId") String productId) {
String userId = String.valueOf(new Random().nextInt(200));
return doSeckKill(userId, productId); }
private boolean doSeckKill(String userId, String productId) {
SessionCallback<List<Object>> sessionCallback = new SessionCallback<List<Object>>() {
@Override
public <String, Object> List<java.lang.Object> execute(RedisOperations<String, Object> operations) throws DataAccessException {
// 1. 判断商品 ID 和用户 ID 是否为空
if (userId == null || productId == null) {
return null;
}
// 2. 对商品 ID 和用户ID作为key进行拼接
// 2.1 拼接商品的Key
String stockKey = (String) ("sk:" + productId + ":stock");
// 2.2 拼接用户的Key
String userKey = (String) ("sk:" + userId + ":user");
// 2.3 监听商品Key
redisTemplate.watch(stockKey);
// 3. 判断Redis中指定的productId是否存在,如果不存在则秒杀没有开始
Integer stock = (Integer) redisTemplate.opsForValue().get(stockKey);
if (stock == null) {
System.out.println("秒杀还没有开始,请等待"); return null;
}
// 4. 判断如果商品数量——库存小于 1,则秒杀结束
if (stock <= 0) {
System.out.println("秒杀已经结束了!"); return null;
}
// 5. 判断用户是否重复操作
if (redisTemplate.opsForSet().isMember(userKey, userId)) {
System.out.println("已经秒杀成功了,不能重复秒杀!");
return null;
}
// 6. 开启事务
operations.multi();
// 7. 减少库存
operations.opsForValue().decrement(stockKey);
operations.opsForSet().add(userKey, (Object) userId);
// 8. 执行
List<java.lang.Object> exec = operations.exec();
return exec;
}
};
redisTemplate.multi();
// 9. 提交事务
List<Object> result = (List) redisTemplate.execute(sessionCallback);
if (result == null || result.size() == 0) {
System.out.println("秒杀失败....");
return false;
}
return true;
}
}
十、数据持久化
官网文档地址:https://redis.io/docs/manual/persistence/
Redis提供了主要提供了 2 种不同形式的持久化方式:
-
RDB(Redis数据库):RDB 持久性以指定的时间间隔执行数据集的时间点快照。
-
AOF(Append Only File):AOF 持久化记录服务器接收到的每个写操作,在服务器启动时再次播放,重建原始数据集。 命令使用与 Redis 协议本身相同的格式以仅附加方式记录。 当日志变得太大时,Redis 能够在后台重写日志。
10-1 RDB(快照)
(1)什么是RDB
在指定的时间间隔内将内存中的数据集快照写入磁盘, 也就是Snapshot快照,它恢复时是将快照文件直接读到内存里。(有触发条件)
(2)如何备份
Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到 一个临时文件中,待持久化过程都结束后,再用这个临时文件替换上次持久化好的文件。 整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。 (没有达到触发条件的话,数据会丢失)
(3)Fork
Fork 的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等) 数值都和原进程一致,但它是一个全新的进程,并作为原进程的子进程。
在 Linux 程序中,fork() 会产生一个和父进程完全相同的子进程,但子进程在此后多会 exec 系统调用,出于效率考虑,Linux 中引入了“写时复制技术”。
一般情况下父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。
(4)执行流程
启动:
- 检查是否存在子进程正在执行 AOF 或者 RDB 的持久化任务。如果有则返回 false。
- 调用 Redis 源码中的 rdbSaveBackground 方法,方法中执行 fork() 产生子进程执行 RDB 操作。
- 关于 fork() 中的 Copy-On-Write。
(5)相关配置
- 配置文件名称:
快照持久化是Redis中默认开启的持久化方案,根据redis.conf中的配置,快照将被写入dbfilename指定的文件中(默认是dump.rdb文件)
注意:RDB功能在 Redis中默认是开启的,而 dump.rdb 文件生成的目录是执行 redis-server命令所在的目录。
在这里设置 RDB 的文件名称。
- 设置文件路径:
根据redis.conf中的配置,快照将保存在dir选项指定的路径上,我们可以修改为指定目录:
dir ./
修改这个值即可。注意要以绝对路径来表示。
-
save(触发条件):
多少秒钟,写操作多少次,也就是多少备份一次,如果不在规定的备份周期中,最后的数据就会丢失。
格式:save 秒钟 写操作次数
save 3600 1 save 300 100 save 60 10000
(6)优势
RDB 方式适合大规模的数据恢复,并且对数据完整性和一致性要求不高更适合使用。它有以下几种优势:
-
节省磁盘空间
-
恢复速度快
(7)劣势
- Fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑
- 虽然Redis在fork时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能。
- 备份周期在一定间隔时间做一次备份,所以如果Redis意外down掉的话,就会丢失最后一次快照后的所有修改。
10-2 AOF(日志)
(1)什么是AOF
以日志的形式来记录每个写操作(增量保存,只加不减),将Redis执行过的所有写指令记录下来(读操作不记录), 只追加文件但不可以改写文件,Redis启动之初会读取该文件重新构建数据。简单说,Redis 重启时会根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。
在Redis的默认配置中AOF(Append Only File)持久化机制是没有开启的,要想使用AOF持久化需要先开启此功能。AOF持久化会将被执行的写命令写到AOF文件末尾,以此来记录数据发生的变化,因此只要Redis从头到尾执行一次AOF文件所包含的所有写命令,就可以恢复AOF文件的记录的数据集。
(2)持久化流程
-
1)客户端的请求写命令会被 append 追加到 AOF 缓冲区内。
-
2)AOF 缓冲区根据 AOF 持久化策略 [always,everysec,no] 将操作sync同步到磁盘的 AOF 文件中。
-
3)AOF 文件大小超过重写策略或手动重写时,会对 AOF 文件 rewrite 重写,压缩 AOF 文件容量。
-
4)Redis 服务重启时,会重新 load 加载 AOF 文件中的写操作达到数据恢复的目的。
(3)使用AOF
-
开启AOF:
修改 redis.conf 配置文件:
-
通过修改redis.conf配置中 appendonly yes 来开启AOF持久化
-
通过appendfilename指定日志文件名字(默认为appendonly.aof)
-
通过appendfsync指定日志记录频率
-
-
配置选项(操作sync同步):
默认是everysec
-
使用演示:
配置好后,连接到 redis中。
[root@redis-101 bin]# redis-server /etc/redis.conf [root@redis-101 bin]# redis-cli 127.0.0.1:6379> keys * (empty array)
当启动后,发现原来的 5 条数据没有了。什么原因?
原因是为当 AOF 和 RDB 同时存在时,以 AOF 持久化文件 appendonly.aof 文件中的内容为准。由于这个功能是刚开启,因此文件中内容为空。
appendonly.aof 生成所在的目录与 dump.rdb 文件生成所在的目录是一致的。(设置路径,也就是绝对路径,两个文件都会在这里路径里)
-
恢复:
如果 appendonly.aof 文件出问题了,可以使用
redis-check-aof
命令来进行恢复。[root@redis-101 bin]# redis-check-aof --fix appendonly.aof 0x d1: Expected prefix '*', got: 'h' AOF analyzed: size=216, ok_up_to=209, ok_up_to_line=48, diff=7 This will shrink the AOF from 216 bytes, with 7 bytes, to 209 bytes Continue? [y/N]: y Successfully truncated AOF
(4)优势
-
备份机制更稳健,丢失数据概率更低。
-
可读的日志文本,通过操作AOF稳健,可以处理误操作。
(5)劣势
-
比起RDB占用更多的磁盘空间。
-
恢复备份速度要慢。
-
每次读写都同步的话,有一定的性能压力。
-
存在个别Bug,造成恢复不能。
官方推荐两个持久化都开启。
十一、主从复制
随着项目访问量的增加,对Redis服务器的操作也越加频繁,虽然Redis读写速度都很快,但是一定程度上也会造成一定的延时,那么为了解决访问量大的问题,通常会采取的一种方式是主从架构Master/Slave,Master 以写为主,Slave 以读为主。
11-1 主从复制原理
Redis 一般是使用一个 Master 节点来进行写操作,而若干个 Slave 节点进行读操作,Master 和 Slave 分别代表了一个个不同的 Redis Server 实例。
另外定期的数据备份操作也是单独选择一个 Slave 去完成,这样可以最大程度发挥 Redis 的性能,为的是保证数据的弱一致性和最终一致性。
另外,Master 和 Slave 的数据不是一定要即时同步的,但是在一段时间后 Master 和 Slave 的数据是趋于同步的,这就是最终一致性。
全同步过程如下:
- Slave 发送 Sync 命令到 Master。
- Master 启动一个后台进程,将 Redis 中的数据快照保存到文件中。
- Master 将保存数据快照期间接收到的写命令缓存起来。
- Master 完成写文件操作后,将该文件发送给 Slave。
- 使用新的 RDB 或 AOF 文件替换掉旧的 RDB 或 AOF 文件。
- Master 将这期间收集的增量写命令发送给 Slave 端。
增量同步过程如下:
-
Master 接收到用户的操作指令,判断是否需要传播到 Slave。
-
将操作记录追加到 AOF 文件。
-
将操作传播到其他 Slave:对齐主从库;往响应缓存写入指令。
-
将缓存中的数据发送给 Slave。
11-2 主从复制配置
下面我们来搭建一主双从的 Redis 集群。
1.一主双从
准备三台虚拟机,配置好主机名、IP地址和Redis环境。本教程中为了演示方便,在一台虚拟机中配置三个Redis实例。
(1)创建目录
我们在根目录下创建 rediscluster 目录:
mkdir /rediscluster
(2)复制配置文件
把 redis.conf 复制到这个目录中
cp /etc/redis.conf /rediscluster/redis.conf
为了演示方便,把 redis.conf 中的 appendonly no,即关闭 AOF。
(3)创建三个文件
在rediscluster目录下分别创建三个文件: redis-6379.conf、redis-6380.conf 和 redis-6381.conf。
vim redis-6379.conf
文件内容如下:
include /rediscluster/redis.conf
# 修改端口号
port 6379
# 修改pid文件名
pidfile "/var/run/redis_6379.pid"
# 修改持久化文件名
dbfilename "dump_6379.rdb"
dir "/rediscluster"
同样的方式创建另外两个文件并做相应的修改。
(4)启动服务
分别启动三台 Redis 服务器:
(5)验证服务
rediscluster]# ps -ef | grep redis
(6)连接服务
分别使用 redis-cli 来连接这三台服务(三个窗口):
rediscluster]# redis-cli -p 6379
rediscluster]# redis-cli -p 6380
rediscluster]# redis-cli -p 6381
(7)进行复制
客户端连接上后,执行如下命令:
127.0.0.1:6379> info replication
127.0.0.1:6380> info replication
127.0.0.1:6381> info replication
(8)配置主从
假设我们希望 6379 是主服务器,而 6380 和 6381 是从服务器,则需要做如下配置。
命令格式: slaveof <ip> <port>
我们分别在 6380 和 6381 客户端中执行如下命令:
127.0.0.1:6380> slaveof 127.0.0.1 6379
OK
127.0.0.1:6381> slaveof 127.0.0.1 6379
OK
注意:
1.在 redis-cli 客户端中执行 slaveof 命令只会对当前环境生效,重启后失效。要想永久生效,需要在 redis.conf 配置文件中添加 slaveof <masterip> <masterport>
配置。
2.在 Redis 5.0 后,新增了 replicaof
命令,作用与slaveof
命令效果一致。
再次执行 info replication
命令查看,可以发现已经变为了从机了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YIycrkFn-1667740088796)(C:\Users\xxy\AppData\Roaming\Typora\typora-user-images\image-20220820171500796.png)]
(9)集群测试
我们在 6379 中添加如下数据:
然后在从机上查询:
发现数据已经同步了。
注意:
-
如果我们在从服务器中添加数据,则会报错。因为从服务器只能读。
-
如果从机宕机了,重启后会变为主服务器,需要重新执行
slaveof <ip> <port>
命令。 -
如果主机宕机了,重启后一切正常。
2.主从切换
当一个 master 宕机后,后面的 slave 可以立刻升为 master,其后面的 slave 不用做任何修改。用 slaveof no one
将从机变为主机。
假设主机 6379 宕机了,我们可以在 6380 上执行如下命令来切换为主机:
127.0.0.1:6380> slaveof no one
查看 6380,已经变为主机了。
但是这样会很麻烦,所以会有以下哨兵模式
11-3 哨兵模式
哨兵也叫 sentinel,它的作用是能够在后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库。
1.配置哨兵
首先停止三台 Redis 服务。
redis-cli shutdown
然后再重新启动三台 Redis 服务器,并实现一主双从。
最后在 /rediscluster 目录下新建 sentinel.conf 文件,文件名称不能写错,必须叫这个名称。
rediscluster]# vim sentinel.conf
内容如下:
sentinel monitor redismaster 127.0.0.1 6379 1
参数说明:
- monitor:监控
- redismaster:为监控对象起的服务名称
- 1:为至少有多少个哨兵同意迁移的数量
2.启动哨兵
先启动三个服务
再执行如下命令来启动哨兵:
rediscluster]# redis-sentinel sentinel.conf
3.验证哨兵
我们把主服务器停止:
127.0.0.1:6379> shutdown
这时,哨兵就会介入,并进行选举,然后把选举成功的从面切换为主机。
如果我们把 6379 重新启动,则它会变为从机。
127.0.0.1:6379> info replication
4.复制延时
由于所有的写操作都是先在Master上操作,然后同步更新到Slave上,所以从Master同步到Slave机器有一定的延迟,当系统很繁忙的时候,延迟问题会更加严重,Slave机器数量的增加也会使这个问题更加严重。
5.选举策略
- 选择优先级靠前的服务器。优先级的配置在 redis.conf 文件中的 replica-priority 配置,默认为 100,值越小优先级越高。
- **选择偏移量最大的。**偏移量是指获得原主机数据最全的。
- **选择 runid 最小的从服务器。**每个redis实例启动后都会随机生成一个40位的runid。
11-4 主从应用
我们在 SpringBoot 应用中使用哨兵模式。
1.配置Redis
spring:
redis:
sentinel:
master: redismaster
nodes: 127.0.0.1:26379
说明:
-
master:是哨兵的名字,即在 sentinel.conf 文件中配置的名称;
-
nodes:哨兵集群节点,如果有多个,用逗号分隔;
-
26379:是哨兵的端口号。
2.编写控制器
@RestController
public class TestController {
@Autowired
private RedisTemplate redisTemplate;
@GetMapping("/set/{key}/{value}") public HttpResult setValue(@PathVariable("key") String key, @PathVariable("value") String value) {
redisTemplate.opsForValue().set(key, value);
return HttpResult.success();
}
@GetMapping("/get/{key}") public HttpResult getValue(@PathVariable("key") String key) {
return HttpResult.success(redisTemplate.opsForValue().get(key));
}
}
十二、集群环境
12-1 集群结介绍
Redis 集群是一个提供在多个 Redis 间节点间共享数据的程序集。Redis集群并不支持处理多个keys的命令,因为这需要在不同的节点间移动数据,从而达不到像Redis那样的性能,在高负载的情况下可能会导致不可预料的错误。
Redis 集群通过分区来提供一定程度的可用性,在实际环境中当某个节点宕机或者不可达的情况下继续处理命令。Redis 集群的优势:
-
自动分割数据到不同的节点上。
-
整个集群的部分节点失败或者不可达的情况下能够继续处理命令。
12-2 数据分片
Redis 集群没有使用一致性hash,而是引入了**哈希槽(slots)**的概念。
Redis 集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽。集群的每个节点负责一部分hash槽,比如当前集群有3个节点,那么:
-
节点 A 包含 0 到 5500号哈希槽。
-
节点 B 包含5501 到 11000 号哈希槽。
-
节点 C 包含11001 到 16384号哈希槽。
这种结构很容易添加或者删除节点。比如想新添加个节点D,只需要从节点 A、B、C中得部分槽并分配到D上即可。 如果想移除节点A,需要将A中的槽移到B和C节点上,然后将没有任何槽的A节点从集群中移除即可。由于从一个节点将哈希槽移动到另一个节点并不会停止服务,所以无论添加删除或者改变某个节点的哈希槽的数量都不会造成集群不可用的状态。
12-3 主从复制模型
为了使在部分节点失败或者大部分节点无法通信的情况下集群仍然可用,所以集群使用了主从复制模型,每个节点都会有N-1个复制品。
假设具有A、B、C三个节点的集群,在没有复制模型的情况下,如果节点B失败了,那么整个集群就会以为缺少5501-11000这个范围的槽而不可用。
然而如果在集群创建的时候(或者过一段时间)为每个节点添加一个从节点A1、B1、C1,那么整个集群便有三个master节点和三个slave节点组成,这样在节点B失败后,集群便会选举B1为新的主节点继续服务,整个集群便不会因为槽找不到而不可用了。
不过当B和B1 都失败后,集群是不可用的。
12-4 一致性保证
Redis 并不能保证数据的强一致性,这意味着在实际集群中在特定的条件下可能会丢失写操作。
第一个原因是因为集群是用了异步复制,写操作过程:
-
客户端向主节点B写入一条命令
-
主节点B向客户端回复命令状态
-
主节点将写操作复制给他得从节点 B1, B2 和 B3
主节点对命令的复制工作发生在返回命令回复之后,因为如果每次处理命令请求都需要等待复制操作完成的话,那么主节点处理命令请求的速度将极大地降低 —— 我们必须在性能和一致性之间做出权衡。注意:Redis 集群可能会在将来提供同步写的方法。
Redis 集群另外一种可能会丢失命令的情况是集群出现了网络分区,并且一个客户端与至少包括一个主节点在内的少数实例被孤立。举个例子:假设集群包含 A 、B 、C 、A1 、B1 、C1 六个节点, 其中 A 、B 、C 为主节点, A1 、B1 、C1 为A,B,C的从节点, 还有一个客户端 Z1。假设集群中发生网络分区,那么集群可能会分为两方,大部分的一方包含节点 A 、C 、A1 、B1 和 C1,小部分的一方则包含节点 B 和客户端 Z1。
Z1仍然能够向主节点B中写入,如果网络分区发生时间较短,那么集群将会继续正常运作;如果分区的时间足够让大部分的一方将B1选举为新的master,那么Z1写入B中得数据便丢失了。
注意, 在网络分裂出现期间, 客户端 Z1 可以向主节点 B 发送写命令的最大时间是有限制的,这一时间限制称为节点超时时间(node timeout), 是 Redis 集群的一个重要的配置选项。
十三、高频面试题
13-1 缓存穿透
1.描述
用户想要查询某个数据,在 Redis 中查询不到,即没有缓存命中,这时就会直接访问数据库进行查询。当请求量超出数据库最大承载量时,就会导致数据库崩溃。这种情况一般发生在非正常 URL 访问,目的不是为了获取数据,而是进行恶意攻击。
2.现象
-
应用服务器压力变大
-
Redis缓存命中率降低
-
一直查询数据库
3.原因
一个不存在缓存及查询不到的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。
4.解决
① 对空值缓存:如果一个查询数据为空(不管数据是否存在),都对该空结果进行缓存,其过期时间会设置非常短。
② 设置可以访问名单:使用bitmaps类型定义一个可以访问名单,名单id作为bitmaps的偏移量,每次访问时与bitmaps中的id进行比较,如果访问id不在bitmaps中,则进行拦截,不允许访问。
③ 采用布隆过滤器:布隆过滤器可以判断元素是否存在集合中,他的优点是空间效率和查询时间都比一般算法快,缺点是有一定的误识别率和删除困难。
④ 进行实时监控:当发现 Redis 缓存命中率急速下降时,迅速排查访问对象和访问数据,将其设置为黑名单。
13-2 缓存击穿
1.描述
key中对应数据存在,当key中对应的数据在缓存中过期,而此时又有大量请求访问该数据,由于缓存中过期了,请求会直接访问数据库并回设到缓存中,高并发访问数据库会导致数据库崩溃。
2.现象
-
数据库访问压力瞬时增加
-
Redis中没有出现大量 Key 过期
-
Redis正常运行
-
数据库崩溃
3.原因
由于 Redis 中某个 Key 过期,而正好有大量访问使用这个 Key,此时缓存无法命中,因此就会直接访问数据层,导致数据库崩溃。
最常见的就是非常“热点”的数据访问。
4.解决
① 预先设置热门数据:在redis高峰访问时期,提前设置热门数据到缓存中,或适当延长缓存中key过期时间。
② 实时调整:实时监控哪些数据热门,实时调整key过期时间。
③ 对于热点key设置永不过期。
13-3 缓存雪崩
1.描述
key中对应数据存在,在某一时刻,缓存中大量key过期,而此时大量高并发请求访问,会直接访问后端数据库,导致数据库崩溃。
注意:缓存击穿是指一个key对应缓存数据过期,缓存雪崩是大部分key对应缓存数据过期。
正常情况下:
缓存失效瞬间:
2.现象
数据库压力变大导致数据库和 Redis 服务崩溃
3.原因
在极短时间内,查询大量 key 的集中过期数据。
4.解决
① 构建多级缓存机制:nginx缓存 + redis缓存 + 其他缓存。
② 设置过期标志更新缓存:记录缓存数据是否过期,如果过期会触发另外一个线程去在后台更新实时key的缓存。
③ 将缓存可以时间分散:如在原有缓存时间基础上增加一个随机值,这个值可以在1-5分钟随机,这样过期时间重复率就会降低,防止大量key同时过期。
④ 使用锁或队列机制:使用锁或队列保证不会有大量线程一次性对数据库进行读写,从而避免大量并发请求访问数据库,该方法不适用于高并发情况。