目录
redis是什么
NOSQL数据库,一般当作缓存使用。他将数据存储到==内存==中,redis使用blkio控制内存的读写,使用redis缓存时,需要注意内存的使用情况,避免内存不足的情况发生。
redis可视化工具:https://github.com/cc20110101/RedisView/releases
redis指令
redis通用指令
-
KEYS:查看符合模板的所有key
-
DEL:删除一个指定的key
-
EXISTS:判断key是否存在
-
EXPIRE:给一个key设置有效期,有效期到期时该key会被自动删除
-
TTL:查看一个KEY的剩余有效期
String类型指令
-
SET:添加或者修改已经存在的一个String类型的键值对
-
GET:根据key获取String类型的value
-
MSET:批量添加多个String类型的键值对
-
MGET:根据多个key获取多个String类型的value
-
INCR:让一个整型的key自增1
-
INCRBY:让一个整型的key自增并指定步长,例如:incrby num 2 让num值自增2
-
INCRBYFLOAT:让一个浮点类型的数字自增并指定步长
-
SETNX:添加一个String类型的键值对,前提是这个key不存在,否则不执行
-
SETEX:添加一个String类型的键值对,并且指定有效期
key结构
Redis的 key 允许有多个单词形成层级结构,多个单词之间用':'隔开,格式如下:
项目名:业务名:类型:id -----> demo:redis:test
如果 value 是一个对象,则可以将对象序列化为 JSON 字符串后保存。
例如:
key | value |
demo:redis:test:user | {"id":1,"name":"张三","age":21} |
Hash类型指令
String类型存储value是对象时存储的是该对象序列化后的字符串,要改变对象中的某一个属性不方便。而Hash类型可以将对象的每一个属性单独存储,可以针对单个字段做CRUD,修改起来方便。
Hash的常见命令有:
-
HSET key field 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
-
HINCRBY:让一个hash类型key的字段值自增并指定步长
-
HSETNX:添加一个hash类型的key的field值,前提是这个field不存在,否则不执行
hset user name houhu
List类型指令
Redis中的List类型与Java中的LinkedList类似,可以看做是一个双向链表结构。既可以支持正向检索和也可以支持反向检索。
特征也与LinkedList类似:
-
有序
-
元素可以重复
-
插入和删除快
-
查询速度一般
常用来存储一个有序数据,例如:朋友圈点赞列表,评论列表等。
List的常见命令有:
-
LPUSH key element ... :向列表左侧插入一个或多个元素
-
LPOP key:移除并返回列表左侧的第一个元素,没有则返回nil
-
RPUSH key element ... :向列表右侧插入一个或多个元素
-
RPOP key:移除并返回列表右侧的第一个元素
-
LRANGE key star end:返回一段角标范围内的所有元素
-
BLPOP和BRPOP:与LPOP和RPOP类似,只不过在没有元素时等待指定时间,而不是直接返回nil
Set类型指令
Redis的Set结构与Java中的HashSet类似,可以看做是一个value为null的HashMap。因为也是一个hash表,因此具备与HashSet类似的特征:
-
无序
-
元素不可重复
-
查找快
-
支持交集、并集、差集等功能
Set的常见命令有:
-
SADD key member ... :向set中添加一个或多个元素
-
SREM key member ... : 移除set中的指定元素
-
SCARD key: 返回set中元素的个数
-
SISMEMBER key member:判断一个元素是否存在于set中
-
SMEMBERS:获取set中的所有元素
-
SINTER key1 key2 ... :求key1与key2的交集
SortedSet类型指令
Redis的SortedSet是一个可排序的set集合,与Java中的TreeSet有些类似,但底层数据结构却差别很大。SortedSet中的每一个元素都带有一个score属性,可以基于score属性对元素排序,底层的实现是一个跳表(SkipList)加 hash表。
SortedSet具备下列特性:
-
可排序
-
元素不重复
-
查询速度快
因为SortedSet的可排序特性,经常被用来实现排行榜这样的功能。
SortedSet的常见命令有:
-
ZADD key score member:添加一个或多个元素到sorted set ,如果已经存在则更新其score值
-
ZREM key member:删除sorted set中的一个指定元素
-
ZSCORE key member : 获取sorted set中的指定元素的score值
-
ZRANK key member:获取sorted set 中的指定元素的排名
-
ZCARD key:获取sorted set中的元素个数
-
ZCOUNT key min max:统计score值在给定范围内的所有元素的个数
-
ZINCRBY key increment member:让sorted set中的指定元素自增,步长为指定的increment值
-
ZRANGE key min max:按照score排序后,获取指定排名范围内的元素
-
ZRANGEBYSCORE key min max:按照score排序后,获取指定score范围内的元素
-
ZDIFF、ZINTER、ZUNION:求差集、交集、并集
注意:所有的排名默认都是升序,如果要降序则在命令的Z后面添加REV即可,例如:
-
升序获取sorted set 中的指定元素的排名:ZRANK key member
-
降序获取sorted set 中的指定元素的排名:ZREVRANK key memeber
redis缓存更新策略
缓存更新是redis为了节约内存而设计出来的一个东西,主要是因为内存数据宝贵,当我们向redis插入太多数据,此时就可能会导致缓存中的数据过多,所以redis会对部分数据进行更新,或者把他叫为淘汰更合适。
内存淘汰:redis自动进行,当redis内存达到咱们设定的max-memery的时候,会自动触发淘汰机制,淘汰掉一些不重要的数据(可以自己设置策略方式)
超时剔除:当我们给redis设置了过期时间ttl之后,redis会将超时的数据进行删除,方便咱们继续使用缓存
主动更新:我们可以手动调用方法把缓存删掉,通常用于解决缓存和数据库不一致问题
数据库和缓存保持一致性
总结:由缓存的调用者在更新数据库的同时删除并添加缓存,并设置超时时间 。
先操作数据库还是先操作缓存?
先操作数据库,redis缓存是存储在内存中的,操作快,并发问题小
更新缓存是先删缓存?
我们每次操作数据库后,都操作缓存,但是中间如果没有人查询,那么这个更新动作实际上只有最后一次生效,中间的更新动作意义并不大,我们可以把缓存删除,等待再次查询时,将缓存中的数据加载出来
为什么要加过期时间
-
防止缓存雪崩: 如果大量缓存在同一时间失效,可能导致数据库负载激增,造成缓存雪崩。通过设置过期时间,可以分散缓存的过期时间,避免同时失效。
-
定期刷新: 设置过期时间可以确保缓存定期刷新,从而保持缓存数据的新鲜性。这对于那些需要及时反映数据库变化的场景可能很重要。
-
资源释放: 设置过期时间有助于及时释放缓存占用的资源,确保不会因为长时间未使用的缓存而占用过多内存。
缓存穿透及解决方案
缓存雪崩及解决方案
缓存击穿及解决方案
问题:redis客户端关闭重启后或者电脑重启后,原先保存的key是否存在。目前发现现象是存在的。
分布式锁
背景:分布式情境下,java锁会失效。
失效原因:分布式环境下,每个Tomcat实例都有自己的JVM,导致同一份代码在不同Tomcat实例中的线程无法共享同一个锁对象。这样,通过普通的Java同步机制(如synchronized
关键字)来实现互斥就会失效,因为不同JVM之间无法共享锁。
java同步机制:
synchronized 关键字:
- 原理:
synchronized
是 Java 内置的关键字,用于实现对象级别的锁。当一个线程进入 synchronized 块时,它会尝试获取对象的锁。如果锁已经被其他线程持有,那么线程将被阻塞,直到锁被释放。 -
synchronized (lockObject) { // 同步代码块 }
ReentrantLock 类:
- 原理:
ReentrantLock
是java.util.concurrent
包中提供的锁实现,它提供了与 synchronized 相似的锁定机制,并且支持可重入、超时等特性。与 synchronized 不同,ReentrantLock
提供了显式的锁定和解锁方法。
ReentrantLock lock = new ReentrantLock();
lock.lock(); // 获取锁
try {
// 同步代码块
} finally {
lock.unlock(); // 释放锁
}
Lock 接口:
- 原理:
Lock
是java.util.concurrent.locks
包中定义的接口,它提供了通用的锁定和解锁操作,允许更灵活的锁定机制。 - 用法: 除了
ReentrantLock
外,Lock
接口的其他实现还包括ReadWriteLock
、StampedLock
等。
volatile 关键字:
- 原理:
volatile
关键字用于修饰变量,保证变量的可见性。当一个线程对volatile
变量进行写操作时,该变量的新值对所有线程都是立即可见的,避免了线程本地缓存的问题。 -
private volatile boolean flag = false;
分布式锁实现原理
分布式锁是在分布式系统中用于实现多个节点之间协调共享资源访问的一种机制。分布式锁的实现需要解决多个节点之间的并发控制问题,确保同一时刻只有一个节点能够持有锁。
-
基于数据库的实现:
- 使用数据库表中的一条记录作为锁,通过事务来实现加锁和释放锁的操作。当一个节点要获取锁时,尝试插入一条记录,如果插入成功则获取锁,否则说明锁已经被其他节点持有。
-
基于ZooKeeper的实现:
- 使用ZooKeeper分布式协调服务,其中一个节点创建一个顺序临时节点作为锁。所有节点按照顺序尝试创建这个节点,创建成功的节点获得锁。当节点释放锁时,删除对应的节点。
-
基于Redis的实现:
- 利用Redis的原子性操作,通过SETNX(set if not exists)实现锁的获取,通过DEL实现锁的释放。通过设置锁的超时时间或使用Redis的自动过期特性来防止死锁。
-
基于分布式一致性算法的实现:
- 使用一些分布式一致性算法,如Paxos、Raft等,来确保在多个节点之间达成一致的锁状态。这类算法通常用于构建高可用和一致性的分布式系统,其实现分布式锁只是其中的一种应用。
分布式锁的特性和注意事项:
-
原子性: 分布式锁的获取和释放操作需要是原子的,确保在多个节点之间不会出现竞争条件。
-
可重入性: 锁应该支持可重入,即同一个节点可以多次获取同一个锁而不会产生死锁。
-
锁超时: 为了防止节点崩溃或其他原因导致锁一直被持有,分布式锁通常需要支持锁超时机制。
-
高可用性: 分布式锁的实现应该考虑节点的高可用性,防止单点故障。
-
避免死锁: 锁的获取和释放操作应该设计得足够健壮,以避免死锁的发生。
基于Redis实现分布式锁
思路:利用redis 的setNx 方法,当有多个线程进入时,我们就利用该方法,第一个线程进入时,redis 中就有这个key 了,返回了1,如果结果是1,则表示他抢到了锁,那么他去执行业务,然后再删除锁,退出锁逻辑,没有抢到锁的哥们,等待一定时间后重试即可
实现:利用setnx方法进行加锁,同时增加过期时间,防止死锁,此方法可以保证加锁和增加过期时间具有原子性。为防止锁误删的情况,给当前锁增加name标识,通过name只删是自己获取的锁。
// 获取锁
private static final String KEY_PREFIX="lock:"
@Override
public boolean tryLock(long timeoutSec) {
// 获取线程标示
String threadId = Thread.currentThread().getId()
// 获取锁
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
// setIfAbsent 对应 Redis 命令 SETNX
return Boolean.TRUE.equals(success);
}
// 释放锁
public void unlock() {
// 获取线程标示
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 获取锁中的标示
String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
// 判断标示是否一致
if(threadId.equals(id)) {
// 释放锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
name属性:
Long userId = UserHolder.getUser().getId();
//创建锁对象(新增代码) 构造方法参数name值,redis模版
SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
以上代码存在问题:不同的name会有不同的锁,加锁失败。
使用Redission实现分布式锁
Redission借助于底层的一个voaltile的一个state变量来记录重入的状态的,比如当前没有人持有这把锁,那么state=0,假如有人持有这把锁,那么state=1,如果持有这把锁的人再次持有这把锁,那么state就会+1。所以Redission是可重入锁。
// 依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
//配置Redisson客户端:
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient(){
// 配置
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.150.101:6379")
.setPassword("123321");
// 创建RedissonClient对象
return Redisson.create(config);
}
}
@Test
void testRedisson() throws Exception{
//获取锁(可重入),指定锁的名称
RLock lock = redissonClient.getLock("anyLock");
//尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
boolean isLock = lock.tryLock(1,10,TimeUnit.SECONDS);
//判断获取锁成功
if(isLock){
try{
System.out.println("执行业务");
}finally{
//释放锁
lock.unlock();
}
}
}
分布式缓存
背景:单机redis存在以下问题:
-
并发能力弱:
- 原因: 单机 Redis 使用单线程处理命令,这可能在高并发负载下成为性能瓶颈。
- 解决方案: 在一些读多写少的场景中,单线程 Redis 的性能已经足够。如果需要提高并发能力,可以通过多实例、分片、或者使用其他更适合多线程的数据库来解决。
-
存储能力弱:
- 原因: 单机 Redis 的存储能力受限于单个节点的内存大小,无法直接扩展存储容量。
- 解决方案: 使用集群方案(Redis Cluster)、分片(Sharding)或者选择其他支持水平扩展的存储方案,如分布式缓存或数据库。
-
故障恢复问题:
- 原因: 单机 Redis 在发生故障(如服务器宕机)时,可能需要较长时间来进行故障恢复。
- 解决方案: 针对故障恢复问题,可以考虑使用主从复制(Replication)来提高可用性。主从复制可以保证在主节点故障时,从节点可以接替主节点的工作。
redis的持久化
redis有2种持久化方案:RDB持久化和AOF持久化
RDB持久化
RDB全称Redis Database Backup file(Redis数据备份文件),也被叫做Redis数据快照。简单来说就是把内存中的所有数据都记录到磁盘中。当Redis实例故障重启后,从磁盘读取快照文件,恢复数据。快照文件称为RDB文件,默认是保存在当前运行目录。
1.执行时机
RDB持久化在四种情况下会执行:
-
执行save命令
-
执行bgsave命令
-
Redis停机时
-
触发RDB条件时
1)save命令
执行下面的命令,可以立即执行一次RDB:
save命令会导致主进程执行RDB,这个过程中其它所有命令都会被阻塞。只有在数据迁移时可能用到。
save
2)bgsave命令
下面的命令可以异步执行RDB:
这个命令执行后会开启独立进程完成RDB,主进程可以持续处理用户请求,不受影响。
bgsave
3)停机时
Redis停机时会执行一次save命令,实现RDB持久化。
4)触发RDB条件
Redis内部有触发RDB的机制,可以在redis.conf文件中找到,格式如下:
# 900秒内,如果至少有1个key被修改,则执行bgsave , 如果是save "" 则表示禁用RDB save 900 1 save 300 10 save 60 10000
RDB的其它配置也可以在redis.conf文件中设置:
# 是否压缩 ,建议不开启,压缩也会消耗cpu,磁盘的话不值钱
rdbcompression yes
# RDB文件名称
dbfilename dump.rdb
# 文件保存的路径目录
dir ./
RDB原理
bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据。完成fork后读取内存数据并写入 RDB 文件。
fork采用的是copy-on-write技术:
-
当主进程执行读操作时,访问共享内存;
-
当主进程执行写操作时,则会拷贝一份数据,执行写操作。
AOF持久化
AOF全称为Append Only File(追加文件)。Redis处理的每一个写命令都会记录在AOF文件,可以看做是命令日志文件。
AOF默认是关闭的,需要修改redis.conf配置文件来开启AOF:
# 是否开启AOF功能,默认是no
appendonly yes
# AOF文件的名称
appendfilename "appendonly.aof"
AOF的命令记录的频率也可以通过redis.conf文件来配:
# 表示每执行一次写命令,立即记录到AOF文件
appendfsync always
# 写命令执行完先放入AOF缓冲区,然后表示每隔1秒将缓冲区数据写到AOF文件,是默认方案
appendfsync everysec
# 写命令执行完先放入AOF缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
appendfsync no
因为是记录命令,AOF文件会比RDB文件大的多。而且AOF会记录对同一个key的多次写操作,但只有最后一次写操作才有意义。通过执行bgrewriteaof命令,可以让AOF文件执行重写功能,用最少的命令达到相同效果。
两种方式各有优缺点,实际开发中会两者结合使用。
主从模式
主从数据同步原理
全量同步
主从第一次建立连接时,会执行全量同步,将master节点的所有数据都拷贝给slave节点。怎么判断是第一次建立连接?
每一个节点都有自己的数据集唯一标识(Replication Id)。
1、建立连接后,从节点会将自己的Replication Id 传为主节点,主节点判断与自己的Replication Id 是否一致,如果不一致就执行全量同步,否则拒绝全量同步。
2、满足执行全量同步后,master会将自己的Replication Id 连同offset 传给子节点让他继承。
3、master执行bgsave,生成RDB文件,并发送给子节点,并且会记录生成RDB过程中执行的所有命令,生成 repl_backlog
4、子节点清空数据,加载RDB文件
5、master发送生成RDB过程中的命令给子节点
6、子节点执行master发送过来的命令
增量同步
全量同步需要先做RDB,然后将RDB文件通过网络传输给slave,成本太高了。因此除了第一次做全量同步,其它大多数时候slave与master都是做增量同步。
什么是增量同步?就是只更新slave与master存在差异的部分数据
1、子节点发送replication id 和offset
2、主节点判断不是第一次 恢复continue
3、主节点去repl_backlog中去取offset后面的数据,并发送给子节点
repl_backlog:是一个固定大小环形数组。offset是数组的角坐标。
repl_baklog中会记录Redis处理过的命令日志及offset。
注意:repl_backlog是有大小限制的环形数组,如果长时间没有做增量同步,会导致数据丢失,这时候只能做全量同步。
可以从以下几个方面来优化Redis主从就集群:
-
在master中配置repl-diskless-sync yes启用无磁盘复制,避免全量同步时的磁盘IO。
-
Redis单节点上的内存占用不要太大,减少RDB导致的过多磁盘IO
-
适当提高repl_baklog的大小,发现slave宕机时尽快实现故障恢复,尽可能避免全量同步
-
限制一个master上的slave节点数量,如果实在是太多slave,则可以采用主-从-从链式结构,减少master压力
主从架构图:
简述全量同步和增量同步区别?
-
全量同步:master将完整内存数据生成RDB,发送RDB到slave。后续命令则记录在repl_baklog,逐个发送给slave。
-
增量同步:slave提交自己的offset到master,master获取repl_baklog中从offset之后的命令给slave
什么时候执行全量同步?
-
slave节点第一次连接master节点时
-
slave节点断开时间太久,repl_baklog中的offset已经被覆盖时
什么时候执行增量同步?
-
slave节点断开又恢复,并且在repl_baklog中能找到offset时
Redis哨兵
作用:哨兵机制实现故障修复
哨兵的作用如下:
-
监控:Sentinel 会不断检查您的master和slave是否按预期工作
-
自动故障恢复:如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主
-
通知:Sentinel充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端
集群监控原理
Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令:
•主观下线:如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线。
•客观下线:若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线。quorum值最好超过Sentinel实例数量的一半。
集群故障恢复原理
一旦发现master故障,sentinel需要在salve中选择一个作为新的master,选择依据是这样的:
-
首先会判断slave节点与master节点断开时间长短,如果超过指定值(down-after-milliseconds * 10)则会排除该slave节点
-
然后判断slave节点的slave-priority值,越小优先级越高,如果是0则永不参与选举
-
如果slave-prority一样,则判断slave节点的offset值,越大说明数据越新,优先级越高
-
最后是判断slave节点的运行id大小,越小优先级越高。
当选出一个新的master后,该如何实现切换呢?
流程如下:
-
sentinel给备选的slave1节点发送slaveof no one命令,让该节点成为master
-
sentinel给所有其它slave发送slaveof 192.168.150.101 7002 命令,让这些slave成为新master的从节点,开始从新的master上同步数据。
-
最后,sentinel将故障节点标记为slave,当故障节点恢复后会自动成为新的master的slave节点