Redis基础篇
入门
初始Redis
认识NoSql
-
非关系型数据库
Sql和NoSql对比
SQL | NoSql | |
---|---|---|
数据结构 | 机构化 | 非结构化 |
数据关联 | 关联的 | 无关联的 |
查询方式 | SQL查询 | 非SQL查询 |
事务特性 | ACID | BASE |
储存方式 | 磁盘 | 内存 |
扩展性 | 垂直 | 水平 |
使用场景 | 1.数据结构固定 2.相关业务对数据安全性、一致性要求较高 | 1.数据结构不固定 2.对一致性、安全性要求不高 3.对性能要求高 |
安装
-
Redis是基于Linux系统设计的,所以只能安装到Linux系统上。企业开发也是基于Linux的。
1.1安装Redis依赖
yum install -y gcc tcl
1.2上传安装包
-
将安装包上传到虚拟机目录并解压
解压:
tar -xzf 安装包
进入目录:
cd 包名
运行预编译命令:
make && make install
没有意外应该就安装成功了
1.3启动
redis的启动有多种方式:
-
默认启动
-
指定配置启动
-
开机启动
默认启动
在任意命令行输入命令即可启动
redis-server
这种启动属于前台启动,他启动后的会话窗口不能关闭,否则redis则会关闭。不推荐使用
指定配置启动
-
如果想让redis后台启动则需要修改redis的配置文件。在安装包的目录下有一个redis.conf的文件
我们先将文件备份一份
cp redis.conf redis.conf.bck
然后修改一些配置
# 允许访问的地址,默认是127.0.0.1,会导致只能在本地访问。修改为0.0.0.0则可以任意ip访问,生产环境下不要设置为0.0.0.0 bind 0.0.0.0 # 守护进程,修改为yes后即可进入后台运行 daemonize yes # 密码,设置后访问redi必须要密码 requirepass 123456
启动redis:
# 进入redis的安装目录 cd /url/local/src/redis-6.2.6 # 启动 redis-server redis.conf
开机自启
首先,我们需要新建一个服务文件:
vi /etc/systemd/system/redis.service
文件内容如下:
[Unit] Description=redis-server After=network.target [service] Type=forking ExecStart=/ulr/local/bin/redis-server /usr/local/src/redis-6.2.6/redis.conf PrivateTmp=true [Instal] WantedBy-multi-user.target
然后重载系统服务:
systemctl daemon-reload
通过命令
systemctl enable redis
就可以让redis开机自启了
Redis客户端
这里操作redis需要用到redis客户端,包括;
-
命令行客户端
-
图形化桌面客户端
-
编程客户端
命令行客户端
Redis安装好了之后就自带命令行客户端:
redis-cli [options] [commonds]
其中常见的options有:
-
-h 127.0.0.1:指定要链接的redis节点的ip地址,默认是127.0.0.1
-
-p 6379:指定要链接的端口,默认是6379
-
-a 123321:指定访问密码
其中的commonds就是redis的操作命令:
-
ping:与redis做服务端心跳测试,服务端正常返回pong
不指定commonds时,会进入redis-cli的交互控制台
图形化客户端
-
进入到下面这个仓库可以下载相应的安装包,
https://github.com/lework/RedisDesktopManager-Windows/releases
安装完成之后,进入到安装目录里,找到rdm.exe文件,双击即可运行
常用数据结构
介绍
-
Redis是一个key-value的数据库,key一般是string类型的,value的类型却有很多种:
string | hello world |
---|---|
Hash | {name: "Jack", age: 21} |
List | [A->B->C->C] |
set | {A,B,C} |
SortedSet | {A: 1, B: 2,C: 3} |
GEO | {A: (120.3, 30.5)} |
BitMap | 01110100101 |
HyperLog | 01101010101 |
其中前五种我们称为redis的基本数据类型,后三种为特殊类型
操作命令
-
Redis官网对操作命令进行了详细的分组和示例,我们查看查看官方文档即可查询到对应的操作命令
通用命令
-
部分数据类型都可以使用的命令,常见的有:
KEYS:查看符合模板的所有key(类似于模糊查询,查询效率不高。redis是单线程的,当数据量达到千万时,redis的查询效率会很慢,就相当于阻塞了,所以不建议使用) | KEYS pattern |
---|---|
DEL:删除键,可以同时删除多个 | DEL key [key ...] |
EXISTS:判断一个key是否存在 | EXISTS key |
EXPIPE:给一个key设置有效期,有效期到期时该key会被自动删除 | EXPIPE key seconds |
TTL:查询一个key的剩余有效期 | TTL key |
String类型
-
是Redis当中最简单的存储类型。其中value为字符串,不过根据字符串的格式不同,又可以分为3类:
-
string:普通字符串
-
int:整数类型,可以做自增、自减操作
-
float:浮点类型,可以做自增自减操作
常见命令
-
SET:添加或者修改已经存在的一个String类型的键值对
-
GET:根据key获取String类型的value
-
MSET:批量添加多个String类型的键值对
-
MGET:根据多个key获取多个String类型的value
-
INCR:让一个整型的key自增
-
INCRBY:让一个整型的key自增并指定步长,例如:incrby num 2 让num值自增2
-
INCRBYFLOAT:让一个浮点类型的数字自增并指定步长
-
SETNX:添加一个String类型的键值对,前提是这个key不存在,否则不执行
-
SETEX:添加一个String类型的键值对,并且指定有效期
key的结构
-
Redis的key允许多个单词形成层级结构,多个单词之间用‘:’隔开,格式如下:
项目名:业务名:类型:id
这个格式并非固定,可以根据自己的需求来添加,例如:
heima:user:1
heima:product:1
Hash类型
-
Hash类型,也叫散列,其value是一个无序字典,类似于java中的HashMap结构
-
String结构是将对象转化为string类型的字符串,当需要修改其中一个字段是无法进行的。而Hash结构是将value中的字符串在进行一次key-value类型的分别
常用命令
-
HSETkeyfield value:添加或者修改hash类型key的field的值
-
HGET key field:获取一个hash类型key的field的值
-
HMsET:批量添加多个hash类型key的field的值
-
HMGET:批量获取多个hash类型key的field的值
-
HGETALL:获取一个hash类型的key中的所有的field和value
-
HKEYs:获取一个hash类型的key中的所有的field
-
HvALS:获取一个hash类型的key中的所有的value
-
HINCRBY:让一个hash类型key的字段值自增并指定步长
-
HsETrx:添加一个hash类型的key的field值,前提是这个field不存在,否则不执行
List类型
-
Redis中的List类型与java中的LinkedList类似,可以看作是一个双向链表结构。既可以支持正向检索也可以支持反向检索
常用命令
-
LPUsHkeyelement...:向列表左侧插入一个或多个元素
-
LPOPkey:移除并返回列表左侧的第一个元素,没有则返回nit
-
RPUSHkeyelement...:向列表右侧插入一个或多个元素
-
RPOPkey:移除并返回列表右侧的第一个元素
-
LRANGE key star end:返回一段角标范围内的所有元素
-
BLPOP和BRPOP:与LPOP和RPOP类似,只不过在没有元素时等待指定时间,而不是直接返回nil
Set类型
-
和java的HashSet类似,可以看作是一个value为null的HashMap。因为是一个表,因此也具备和HashSet类似的特征
-
无需,元素不重复,查找快,支持交集、并集、差集等功能
常用命令
-
SADD key member .:向set中添加一个或多个元素
-
SREM key member...:移除set中的指定元素
-
SCARD key:返回set中元素的个数
-
SISMEMBER key member:判断一个元素是否存在于set中
-
SMEMBERS:获取set中的所有元素
-
SINTER key1 key2...:求key1与key2的交集
-
SDIFF key1 key2...:求key1与key2的差集
-
SuNION key1 key2..:求key1和key2的并集
SortedSet类型
-
Redis的Sorted是一个可排序的Set集合,与java中的treeSet类似,但底层的数据结构差别却很大。
-
SortedSet中的每一个元素都带有一个score属性,可以基于score属性对元素进行排序。
-
可排序、元素不重复、查询速度快
-
因为可排序性,经常被用来实现排行榜这样的功能
常用命令
-
ZADD key score member:添加一个或多个元素到sorted set,如果已经存在则更新其score值
-
ZREM key member:翻除sorted set中的一个指定元素
-
ZSCORE key member:获取sorted set中的指定元素的score值
-
ZRANKkey member:获取sorted set中的指定元素的排名
-
ZCARD key:获取sorted set中的元素个数
-
ZCOUNNT key min max:统计score值在给定范围内的所有元素的个数
-
ZINCRBY keyincrement member:让sorted set中的指定元素自增,步长为指定的increment值
-
ZRANGE key min max:按照score排序后,获取指定排名范围内的元素
-
ZRANGEBYSCORE key min max:按照score排序后,获取指定score范围内的元素
-
ZDIEF、ZINTER、ZUNION:求差集,交集并集
Redsi的java客户端
-
Redis官网提供了很多的客户端
Jedis
-
它提供了与Redis命令一一对应的方法,并支持使用管道和事务来提高和保证数据的一致性
实现步骤
1.引入依赖
<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>3.7.0</version> </dependency>
2.建立连接
private Jedis jedis; @BeforeEach void setUp() { /建立连接 jedis=new Jedis("192.168.150.101",6379); //设置密码 jedis.auth("123321"); //选择库 jedis.select(@);
3.测试
@Test void teststring() { //插入数据,方法名称就是redis命令名称,非常简单 String result = jedis.set("name", "张三"); System.out.println("result = " + result); //获取数据 String name = jedis.get("name"); System.out.println("name = " + name); }
4.释放资源
@AfterEach void tearDown() { //释放资源 if (jedis != null) { jedis.close(); }
Jedis连接池
-
Jedis本身是线程不安全的,并且频繁的和销毁连接会有性能损耗,因此我们建议大家使用jedis连接池代替jedis直连方式
public class JedisConnectionFactory { private static final jedisPool jedisPool; static { JedisPaalConfig jedisPoolconfig =now JedisPaoiconfig(); //最大连接 jedisPclconfig.setMaxTotol(8); //最大空闲连接 jedisPoolconfig.setMaxIdle(8); //最小空闲连接 jedisPoalConfig.setMinIdle(0); //设置最长等待时间,ms jedisPoolconfig.setMaxWaitHillis(200); jedisPoot = new JedisPool(jedisPoolConfig. "192.168.150.101", 6379,1000,"123321"); /获族Dedis对象 pubiic static Jedis getJedis(){ return jedispool.getResource(); }
SpringDataRedis
-
SpringData是Spring中数据操作的模块,包含对各种数据库的集成,其中对redis的集成叫做SpringDataRedis
作用
-
提供了对不同Redis客户端的整合(Lettuce和jedis)
-
提供了RedisTemplate统一API来操作Redis
-
支持Redis的发布订阅模型
-
支持Redis哨兵和Redis集群
-
支持基于Lettuce的响应式编程
-
支持基于JDK、JSON、字符串、Spring对象的数据序列化及反序列化
-
支持基于Redis的JDKCollection实现
操作命令
-
SpringDataRedis中提供了RedisTemplate工具类,其中封装了各种对Redis的操作。
API | 返回值 | 说明 |
---|---|---|
redisTemplate.opsForValue() | ValueOperations | 操作String类型的数据 |
redisTemplate.opsForHash() | HashOperations | 操作Hash类型数据 |
redisTemplate.opsForList() | ListOperations | 操作List类型数据 |
redisTemplate.opsForSet() | SetOperations | 操作Set类型数据 |
redisTemplate.opsForZSet() | ZSetOperations | 操作SortedSet类型数据 |
redisTemplate | 通用命令 |
使用步骤
1.引入依赖
<!--Redis依赖--> <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>
2.配置文件
spring: redis: host:192.168.150.101 port:6379 password: 123321 lettuce: pool: max-active:8 # 最大连接 max-idle:8#最大空闲连接 min-idle:0 # 最小空闲连接 max-wait:100 #连接等待时间
3.注入RedisTemplate
@Autowired private RedisTemplate redisTemplate;
4.编写测试
@SpringBootTest pubtic class RedisTest { @Autawired private RedisTemplate redisTemplate; @Test void testString() { //插入一条string类型数据 redisTemplate.opsForvalue().set("name", "李四"); //读取一条string类型政据 Object name = redisTemplate.opsForValue().get("name"); System.out.println("name = " + name); } }
序列化问题
-
RedisTemplate可以接收任意的object作为值写入redis,只不过写入前会把object序列化为字节形式,默认是采用jdk序列化,而我们看到的就是序列化后的字节
-
这样就导致可读性差,内存占用较大
解决方法
-
改变RedisTemplate中的序列化方式
示例:
public RedisTemplate<string,object> redisTemplate(RedisConnectionFactory redisConoectionFoctory) throws UnknownHostException { //创建Template RedisTemplate<string, Object> redlsTemplate = new RedisTemplate<>(); //设置连接工厂 redisTemplate.setConnectionFactory(redisConnectionFactory); //设置序列化工具 GenericJackson2JsonRedisSerialTizer jsonRedisSeriaizer = new GenericJackson2JsonRedisSerializer(); //key和 hashKey采用 string序列化 redisTemplate.setkeySeriatizer(Redisserializer.string()); redisTemplate.setHashKeySerializer(RedisSerializer.string()); //value和 hashValue采用JsoN序列化 redisTemplate.setValueSerializer(jsonRedisSerializer); redisTemplate.setHashValueSerializer(jsonRedisSerializer); return redlsTemplate; }
存入对象数据存在的问题
-
当存入一个对象数据时,redis中会额外带有对应的对象包的信息,会带来额外的开销
解决方法
-
Spring提供了一个StringRedlsTemplate类,它的key和value的序列化方式默认是string方式。省去了我们自定义redlsTemplate的过程
@Autowired private StringRedisTemplate stringRedisTemplate; //JSON工具 private static final ObjectMapper mapper = new ObjectMapper(); @Test void testStringTemplate() throws JsonProcessingException { //准务对象 User user =new User("虎哥",18); //手动序列化 String json = mapper.writevalueAsString(user); //写入一条数据到redis stringRedisTemplate.opsForValue().set("user:200", json); //读取数据 String val = stringRedisTemplate.opsForValue().get("user:200"); //反序列化 User user1 = mapper.readvatue(vat, User.class); System.out.println("user1 = " + user1); }
Redis缓存
概念
-
缓存就是数据交换的缓冲区(cache),是存储数据的临时地方,一般读写性能较高。
优缺点
优点
-
降低了后端负载
-
提高了读写效率,降低响应时间
缺点
-
数据一致性问题。将数据缓存到redis后,如果数据库的数据更新,那么到时候用户得到的数据就是就的数据,会造成一些错误
-
为了解决数据一致性的问题,这就要求我们的代码更加复杂,导致代码的维护成本增加
缓存更新策略
内存淘汰 | 超时剔除 | 主动更新 | |
---|---|---|---|
说明 | 不用自己维护,利用redis的内存淘汰机制,当内存不足时自动淘汰部分数据。下次查询时更新 | 给缓存添加ttl时间,到期后自动删除缓存。下次查询时更新 | 编写业务逻辑,在修改数据库的同时,更新缓存 |
一致性 | 差 | 一般 | 好 |
维护成本 | 无 | 低 | 高 |
主动更新策略
Cache Aside Pattern
-
由缓存的调用者,在更新数据库的同时更新缓存(使用最多)
存在的问题
缓存更新问题
当我们更新缓存时,我们需要考虑一个问题,是更新缓存还是删除缓存
-
更新缓存:每次更新数据库都更新缓存,无效写操作过多。具体一点就是我现在有一个程序,数据库的数据有多次更新,但是在这期间没有请求来访问这些数据,但是redis一直在进行更新操作,这就造成了大量的无效操作
-
删除缓存:更新数据库时,让缓存失效,用户查询时再更新缓存。所以我们推荐先删除缓存
操作同时成功问题
-
如何保证缓存和数据库同时成功或失败
-
单体系统:将缓存和数据库操作放在同一个事务当中。
-
分布式系统:利用TCC等分布式事务方案
先后问题
先更新数据库还是先更新缓存?
-
两者即可,看具体情况。两者都可能发生问题
Read/Write Through Patten
-
缓存与数据库整合为一个服务,又服务来维护一致性。调用者调用该服务,无需关心缓存一致性的问题
Write Behind Caching Patten
-
调用者只操作缓存,由其他线程异步的将缓存数据持久化到数据库,保证一致性
缓存穿透
概念
-
缓存穿透是指客户端请求的数据在缓存中和数据库都不存在,但是用户还在不断的发起请求,这些请求都会打到数据库,导致数据库直接被压垮
解决方案
缓存空对象
-
缓存一个null
优点
-
实现简单,维护方便
缺点
-
造成额外的消耗(如果每一个不存在的请求都要缓存一个,那么就造成了很多浪费)
-
可能造成短期的不一致
布隆过滤
-
在客户端和缓存之间增加一个过滤器,每一个请求先被过滤器拦截,然后根据条件选择性的放行
优点
-
内存占用少,没有多余的key
缺点
-
实现复炸
-
存在误判可能
缓存雪崩
概念
-
缓存雪崩是指在同一时间段大量的缓存key同时失效或者redis服务宕机,导致大量的请求到达了数据库,带来了巨大的压力
解决方案
-
给不同的key的TTL添加一个随机值
-
利用redis集群提高服务的可用性
-
给缓存业务添加降级限流策略
-
给业务添加多级缓存
缓存击穿
概念
-
缓存击穿问题也叫热点Key问题,就是一个被高并发访间并且缓存重建业务较复杂的key突然失效了,无数的请求访何会在瞬间给数据库带来巨大的冲击
解决方案
互斥锁
简单来说就是在Redis中根据key获得的value值为空时,先锁上,然后从数据库加载,加载完毕,释放锁。若其他线程也在请求该key时,发现获取锁失败,则睡眠一段时间(比如100ms)后重试
优点
-
没有额外的消耗
-
保证一致性
-
实现简单
缺点
-
线程需要等待,性能可能受影响
-
可能有锁死风险
逻辑过期
优点
-
线程无需等待,性能较好
缺点
-
不保证一致性
-
有额外的消耗
-
实现复炸
Redis实战篇
优惠券秒杀
存在问题
每个店铺都可以发布优惠券,当用户抢购时就会生成订单并保存到订单表中,订单表中有一个字段表示的是使用了那些优惠券。而订单表如果使用数据库自增长ID就会存在一些问题
-
id的规律性太明显(用户可能根据id推测出一天的实际订单数)
-
受单表数据量的限制
解决方法
全局ID生成器
全局ID生成器,是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足一下特性:
-
唯一性
-
高可用
-
高性能(生成的速度快)
-
递增性
-
安全性
为了增加ID的安全性,我们可以不直接使用redis的自增数值,而是拼接一些信息:
ID的组成:
符号位:1bit,永远为0
时间戳:31bit,以秒为单位,可以使用69年
序列号:32bit,以秒为计数器,支持每秒生产2的32次方个不同的ID
分布式锁
概念
-
满足分布式系统或集群模式下多进程可见且互斥的锁
实现
实现方式
MySQL | Redis | Zookeeper | |
---|---|---|---|
互斥 | 利用mysql本身的互斥锁机制 | 利用setnx这样的互斥命令 | 利用节点的唯一性和有序性 |
高可用 | 好 | 好 | 好 |
高性能 | 一般 | 好 | 一般 |
安全性 | 断开连接,自动释放锁 | 利用锁超时时间,到期自动释放 | 临时节点,断开连接自动释放 |
基于Redis实现
实现分布式锁需要实现两个基本方法:
-
获取锁:
-
而获取锁其中最重要的一个就是互斥:确保只有一个线程获取锁,比如:
SETNX lock thread1 NX EX 10# 利用setnx的互斥特性,NX互斥,EX存活时间
-
-
释放锁:
-
手动释放:
-
超时释放:手动添加一个存活时间
DEL key # 释放锁,删除即可
-
在释放锁时,为了防止误删,我们可以在获取锁时添加一个线程的标识,释放时进行比较,一致在删除,以防止误删的情况.
但仍旧存在一些问题,当判断结束后要进行释放操作时线程发生了堵塞,也会出现误删问题。原因是因为判断和释放不是同一个操作,中间有间隔。
而解决方法就是让两个操作具有原子性
如何让radis命令具有原子性
-
Redis提供了Lua脚本功能,在一个脚本中编写多条redis命令,确保多条命令的原子性。
Redis调用函数
# 执行redis命令 redis.call('命令名称','key','其他参数'...)
例如我们要执行set name jack命令,则脚本为:
# 执行set name jack redis.call('set','name','jack')
如果我们要执行set name Rose,在执行get name,则脚本如下:
# 先执行set name jack redis.call('set','name','jack') # 再执行get name local name = redis.call('get','name') # 返回 return name
写好脚本后,需要redis命令来执行脚本,调用脚本命令如下:
EVAL script numkeys key
例如,我们要执行redis.call('set','name','jack')这个脚本,语法如下:
# 调用脚本 EVAL "return redis.call('set','name','jack')" 0
然后进行改进:基于Lua改进分布式锁
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT; static { UNLOCK_SCRIPT = new DefauttRedisscript<>(); UNLOCK_SCRIPT.setLocation(new classPathResource("unlock.lua")); UNLOCK_SCRIPT.setResultType(Long.class);//设置返回值 } @Override pubtic void unlock() //调用Lua脚本 stringRedisTemplate.execute(){ UNLOCK_SCRIPT, collections.singletonList(KEY_PREFIX + name), ID_PREFIX + Thread.currentThread().getId()); }
分布式锁优化
Redisson
-
Redsson是一个在redis基础上实现的java驻内存数据网格。它不仅提供了一系列的分布式java对象,还提供了许多分布式服务,其中就包含了分布式锁
Redisson入门
1.引入依赖
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.13.6</version> </dependency>
2.配置客户端
@configuration pabtic class RedisConfig ( @Bean pubtic RedissonClient redissonClient() { //配置类 Config config = new confng(); //添加redis地址,这里添加了单点的地址,也可以使用config.useClusterServers()添加集群地址 config.useSingieServer().setAddress("redis//192.168.150.101:6379").setPassword("123321"); //创建客户端 return Redisson.crecte(config); }
3.使用锁
@Resource private RedissonClient redissonClient; @Test void testRedisson() throws InterruptedException { //获取锁(可重入),指定锁的名称 RLock lock = redissonClient.getLock("anyLock"); //尝试获取锁,参数分放是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时网单位 boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS); //判断释放获取成功 if(isLock)( try { System.out.printin("执行业务“); }finally { //释放锁 lock.unlock(); } } }
Redis高级篇
单点Redis存在的问题
数据丢失问题
-
Redis是内存存储,服务重启后可能会丢失数据
解决方法
-
redis的数据持久化,将数据写入磁盘
并发能力问题
-
单节点redis并发能力虽然不错,但也无法满足618这样的高并发场景
解决方法
-
redis主从集群,实现读写分离
故障恢复问题
-
如果redis宕机,则服务不可用,需要一种自动的故障恢复手段
解决方法
-
redis哨兵机制,实现健康检测和自动恢复
存储问题
-
redis的数据是存储在内存的,单节存储的数据难以满足海量的数据需求
解决方法
-
搭建分片集群,利用插槽机制实现动态扩容
Redis持久化
RDB持久化
概念
-
RDB全redis database backup file(数据备份文件),也被叫做redis数据快照。简单来说就是将内存中的数据记录到磁盘中,当redis出现故障重启后,从磁盘读取快照文件,恢复数据
实现
自动实现
-
redis默认在关闭时就会自动的将数据备份到当前目录中。但这是关闭后才备份的
主动实现
-
redis内部由RDB触发机制,可以在redis.conf文件中找到,格式如下:
# 900秒内,如果至少有1个key被修改。则执行bgsave,如果是save "" 则表示禁用RDB save 900 1 save 30B 10 save 60 10000
还有其他设置
# 是否压缩 ,建议不开启,压缩也会消耗cpu,磁盘的话不值钱 rdbcompression yes # RDB文件名称 dbfilename dump.rdb # 文件保存的路径目录,保存在当前路径 dir ./
bgsave
-
bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据。完成fork后读取内存数据并写入RDB文件
fork采用的是copy-on-write技术:
-
当主进程执行读操作时,访问共享内存
-
当主进程执行写操作时,则会拷贝一份数据,执行写操作。
AOF持久化
概念
-
AOF称为追加文件。redis处理的每一个命令都会记录在AOF文件中,可以看作是命令日志文件
AOF默认是关闭的,需要修改配置文件来开启AOF:
# 是否开启AOF功能,默认是no appendonly yes # AOF文件的名称 appendfilename "appendonly.aof"
AOF的命令记录频率也可以通过配置文件来配:
# 表示每执行一次写命令,立即记录到A0F文件 appendfsync always # 写命令执行完先放入AOF级冲区,然后表示每隔1秒将缓冲区数据写到ADF文件,是默认方案 appendfsync everysec # 写命令执行完先放入A0F缓冲区,由操作系统决定何时将缓冲区内容写回磁盘 appendfsync no
因为是记录命令。AOF文件会比RDB文件大的多。而且AOF会记录对同一个key的多次写操作。但只有最后一次写操作才有意义,通过执行bgrewriteaof命令,可以让AOF文件执行重写功能,用最少的命令达到相同的效果
Redis也会在出发阈值时,自动重写AOF文件。也是在配置文件中配置的:
# A0F文件比上次文件 增长超过多少百分比则触发重写 auto-aof-rewrite-percentage 100 # AOF文件体积最小多大以上才触发重写 auto-aof-rewrite-min-size 64mb
主从架构
-
单节点的redis的并发能力是有限的,要进一步提高redis的并发能力,就需要搭建主从集群,实现读写分离
-
比如实际操作中读操作更多,那么我们可以将读操作分给主节点。写操作就可以分给从节点
如何搭建
-
开启3台redis,准备3个不同的配置文件,然后运行。最后进行连接。
数据同步原理
-
主从第一次同步是全量同步
-
当主节点的数据更新后,执行bgsave生成RDB文件,然后将RDB文件发送给从节点,从节点扫描文件然后更新数据,这样就能基本保证数据的一致性
哨兵机制
-
slave节点宕机后恢复后可以找master节点同步数据,那master节点宕机后怎么办呢?
作用
监控
-
Sentionel会不断检查你的master和slave是否按预期工作
自动故障恢复
-
如果master发生了故障宕机,sentinel会将一个slave提升为master。当故障恢复后也以新的master为主
通知
-
Sentinel充当redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给redis客户端
服务状态监控
如何实现监控
-
Sentinel基于心跳机制监测服务状态,每个1秒向集群的每个实例发送ping命令
-
主观下线:如果Sentinel发现某个实例没有按照规定时间响应,则认为该实例主观下线
-
客观下线:若超过指定数量的哨兵都认为该实例下线,则该实例客观下线。这个值最好是一超过哨兵数量的一半
-
如何实现故障转移
当一个slave转化为新的master后,故障转移步骤如下:
-
sentinel给备选的slave1节点发送slaveof no one命令,让该节点成为master
-
sentinel给所有其它slave发送slaveof192.168.150.1017002命令,让这些slave成为新master的从节点,开始从新的master上同步数据。
-
最后,sentinel将故障节点标记为slave,当故障节点恢复后会自动成为新的master的slave节点
搭建步骤
Java客户端操作哨兵
1.在pom文件中引入redis的starter依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
2.在配置文件中指定sentienl的信息
spring: redis: sentinel: master:mymaster # 指定master名称 nodes:# 指定redis-sentinel集群信息 - 192.168.150.101:27001 - 192.168.150.101:27002 - 192.168.150.101:27003
3.配置主从读写分离
@Bean pubtic LettuceClientConfigurationBuilderCustomizer contigurationBuilderCustomizer(){ return configBuilder -> config8uilder.reodFrom(ReadFrom.REPLICA_PREFERRED); }
Redis分片集群
作用
主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决:
-
海量数据存储问题
-
高并发写的问题
而使用分片集群可以解决上述问题,分片集群特征:
-
集群中有多个master,每个master保存不同的数据
-
每个master都可以有多个slave节点
-
master之间通过ping检测彼此健康状况
-
客户端请求可以访问集群任意节点,最总都会被转发到正确节点
搭建步骤
准备配置和实例
-
创建新的节点目录
mkdir 7001 7002 7003 8001 8002 8003
-
准备新的配置文件
port 6379 # 开启集群功能 cluster-enabled yes # 集群的配置文件名称,不需要我们创建,由redis自己维护 cluster-config-file /tmp/6379/nodes.conf # 节点心跳失败的超时时间 cluster-node-timeout 5000 # 持久化文件存放目录 dir /tmp/6379 # 绑定地址 bind 0.0.0.0 # 让redis后台运行 daemonize yes # 注册的实例p replica-announce-ip 192.168.150.101 # 保护模式 protected-mode no # 数据库数量 databases 1 # 日志 logfile /tmp/6379/run.log
-
将文件拷贝到各个目录中
echo 7001 7002 7003 8001 8002 8003 | xargs -t-n 1 cp redis.conf
-
修改文件信息,将端口号修改为所在目录一致:
printf '%s\n' 7001 7002 7003 8001 8002 8003 | xargs -I{} -t sed -i 's/6379/{}/g' {}/redis.conf
-
启动
printf '%s\n' 7001 7002 7003 8001 8002 8003 | xargs -I{} -t redis-server {}/redis.conf
-
设定关系
redis-cli --cluster create --ctuster-replicas 1 192.168.150.101:7001 192.168.150.101:7002 192.168.150.101:7003 192.168.150.101:8001 192.168.150.101:8002 192.168.150.101:8003
查询集群状态
redis-cli -p 7001 cluster nodes
散列插槽
-
redis会把每一个master节点映射到0-16383共16384个插槽上(hash slot)上,查看集群信息时就可以看到每一个master节点有一个slots信息
-
数据根据哈希函数被映射到不同的插槽中存储。每个Redis节点负责管理一部分插槽,这样就实现了数据的分布式存储和负载均衡。
-
当对一个散列键执行操作时,Redis会通过哈希函数计算出该键应该属于哪个插槽,并将其路由到负责该插槽的节点上
集群伸缩
-
集群最重要的一点就是能够动态的添加节点和删除节点
添加节点
-
add-node命令
故障转移
自动转移
当一个集群中有一个master宕机以后会发生什么呢?
-
1.首先是该实例与其他实例失去连接
-
2.然后疑是宕机
-
3.最后确定已经下线,自动提升一个新的slave为master
这是自动的转移
手动转移
-
例如设备的升级
数据迁移
-
利用cluster failover命令可以手动让集群中的某个master宕机,切换到执行cluster failover命令的这个slave节点,实现无感知的数据迁移
Java操作
-
redistemplate底层通过基于lettuce实现了分片集群的支持,而使用步骤与哨兵模式基本一致
-
引入redis的starter依赖,配置分片集群地址,配置读写分离模式
只不过配置当时略有差异:
spring: redis: cLuster: nodes: #指定分片集群的每一个节点信息 - 192.168.150.101:7001 - 192.168.150.101:7002 - 192.168.150.101:7003 - 192.168.150.101:8001 - 192.168.150.101:8002 - 192.168.150.101:8003