自拟!Redis死知识!!!
自拟!Redis死知识!!!
Redis基础
使用概念
redis是基于内存存储的数据库,读写速度十分之快,所以多用于缓存方向。
但在分布式业务中,也可用于分布式锁,或作为消息队列存储,进行消耗和接受。
此外,redis可存储很多中数据类型,又因为key - value Hash对、set、zset、list,存储String的原因,使其数据结构很丰富。
指令
包括set、get、exists、decrease、setex等等
127.0.0.1:6379> set key value #设置 key-value 类型的值
OK
127.0.0.1:6379> get key # 根据 key 获得对应的 value
"value"
127.0.0.1:6379> exists key # 判断某个 key 是否存在
(integer) 1
127.0.0.1:6379> strlen key # 返回 key 所储存的字符串值的长度。
(integer) 5
127.0.0.1:6379> del key # 删除某个 key 对应的值
/////////////////////批量
127.0.0.1:6379> mset key1 value1 key2 value2 # 批量设置 key-value 类型的值
OK
127.0.0.1:6379> mget key1 key2 # 批量获取多个 key 对应的 value
/////////////////////计数
127.0.0.1:6379> set number 1
OK
127.0.0.1:6379> incr number # 将 key 中储存的数字值增一
(integer) 2
127.0.0.1:6379> get number
"2"
127.0.0.1:6379> decr number # 将 key 中储存的数字值减一
(integer) 1
127.0.0.1:6379> get number
"1"
/////////////////////设置过期
expire key 60
setex key 60 value
////////////////////list操作
rpush list value # 添加value
lpop list # 左边弹出
rpop list # 右边弹出
lrange list 0 1 # 查看下标 0 1 元素
//////////////////// map 操作
hmset key value # 设置
hexists key # 判断
hgetall key # 获取key下所有value
hget key # 获取
////////////////////set 操作
sadd set key value # 设置
smembers set key #查看
scard set key # 查看key长度
sismember set key # 检查存在
///////////////// sorted set操作
zadd ...
zcard...
///////////////// bitmap
setbit mykey 7 1 # 设置7位位置 1
setbit mykey 7 0 # 设置7为位置 0
bitcount mykey # 统计mekey 7位置 1的数目
单线程模型
Redis基于Reactor[反应堆模式],由文件事件处理器单线程运行。
通过IO多路复用[即时监听通知io请求的线程,监听多个套字节,不需要额外创建多余线程监听]程序监听大量客户端请求。
其文件事件处理器包含四部分监听请求:
1、创建多个socket
2、IO多路复用程序[接受多个套字节]
3、事件分派器
4、事件处理器
不使用多线程的原因
redis作为缓存组件,使用多线程容易造成死锁,上下文不兼容问题,极大影响效率。
单线程更加容易维护。
即使使用多线程也不会很明显的加大redis的性能,redis的性能靠机械的网络和内存支持。
6.0之后可以支持多线程,但是只针对大键的删除,而且这些删除命令在通过多线程向主线程请求后,依然会走单线程的删除操作。
删除策略
1、惰性删除,懒删除模式,只有当有操作指令到key时,才判断key是否过期,过期则删除
2、定时删除,每隔一段时间,抽取一批key进行判断删除。
1对cpu好,2对内存好,redis使用的1+2的结合方式。
内存淘汰机制
当key大量推荐,即将导致内存溢出,Out Of memory时,触发淘汰机制
1、设置过期时间,最少使用 的,淘汰
2、设置过期时间,将要过期的,淘汰
3、设置过期时间,随机挑选,淘汰
4、最少使用的,淘汰
5、随机挑选,淘汰
6、不淘汰,抛出内存溢出异常。
持久化机制
将数据写入硬盘中,即使redis挂掉,再次重启时也能恢复数据
快照持久化
快照,快速拍张照的意思。
当内存达到快照触发值时,Redis创建快照,获取存储在内存中的数据在某个时间点的副本,对副本进行备份。
默认方式,可在Redis.conf配置文件中,修改时间范围,内存达到多少时进行快照操作。
AOF持久化
目前主流的持久化方案,需要appendonly yes 手动开启。
当执行redis指令时,会将该命令写入内存缓存中,然后根据配置决定同步到硬盘的时间
在 Redis 的配置文件中存在三种不同的 AOF 持久化方式,它们分别是:
appendfsync always #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
appendfsync everysec #每秒钟同步一次,显式地将多个写命令同步到硬盘
appendfsync no #让操作系统决定何时进行同步
建议使用第二种,每秒同步,不会让性能感到不适,而且即使系统崩溃,也能获取到最后一秒的备份数据。
Redis事务
和数据库事务特性'ACID'不同
偶遇redis事务基于 multi [开始] Exec[执行] Discard[禁止执行] 控制,所以相当于预输入指令,然后统一执行的概念。
所以无法保证其事务的原子性,因为没有回滚操作。
总之redis事务是: Redis 事务提供了一种将多个命令请求打包的功能。然后,再按顺序执行打包的所有命令,并且不会被中途打断。
Redis作消息队列
Redis5.0之后新增数据结构 Steam
使用:
XADD key * user english msg Hello
创建一条key id为*[redis自动定义] user = english msg = hello 的队列
XREAD streams key 0
//读取key的第0条开始的所有消息[全部]
XREAD block 1000 streams key $
//阻塞队列读取模式 当有key 的消息时立马读取,否则阻塞
缓存穿透
缓存被穿透,大量请求到key上,但是这个key不存在,所有请求都穿透了缓存直接去请求数据库DB。
解决方法:
1、进行数据校验,让数据不能操作出不符合key设置范围的值
2、缓存一个无效的key,设置过期时间。如果出现无效key出现,则设置一个缓存值并设置过期时间。但是弊端就是如果大量请求,每次请求的key都不同,但都是无效key。则会消耗大量的内存。
3、布隆过滤器,特殊结构。将所有可能存在的请求值放在过滤器中,判断用户给的key是否存在。不存在,则直接返回参数错误信息。存在的话,再走缓存 - 数据库流程。
但是布隆过滤器可能存在误判情况,但是只要他返回错误信息,则说明这个key一定参数非法。
缓存雪崩
缓存大面积失效,像雪崩一下崩塌,导致redis无用,请求全都打在数据库DB上。多出现在并发量突然特别大的时候,缓存失效的一瞬间,并发起来吓死数据库,比如秒杀。
解决方法:
1、redis集群
2、减少并发,分流。
3、设置缓存永不失效
Redis读写策略
Cache Aside Pattern旁路缓存模式
平时使用的模式,向更新数据库,然后删除缓存。
读:有存储就读缓存,没有就读DB、顺便设置缓存。
缺点:在写操作很频繁的业务中,会频繁删除缓存,减低命中率.
优化:在更新DB后,删除缓存,设置一个时间较短的缓存。
Read/Write Through Pattern 读写穿透
很少见,将redis当前主要存储源,由redis完成对DB数据库的更新、删除、新增操作
Write Behind Pattern 异步缓存写入
同上
Redis与数据库
数据库与redis缓存一致性问题
更新缓存,更新数据库。
没用,两者之一有一环失败都会出现问题
更新数据库,更新缓存
同上,也无法保证数据的绝对一致性。
并且在并发的环境下,以上两种方案更是会存在丢失修改的情况发生。
删除缓存,更新数据库
不行,在读-写服务一起操作同一数据库时,会出现:
服务A删除缓存,服务B读缓存发现没有,去读数据库设置缓存值2,服务A更新数据库3 。此时数据库值为3,缓存为2 不一致。
更新数据库,删除缓存
不行,在读写中:
服务A读缓存,不存在读数据库值为1。 服务B写数据库,更新数据库值为2,删除缓存。 服务A写入缓存1. 此时数据库为2,缓存未1 。 但是理论上本例很难发生,除非更新数据库+删除缓存的服务B,比读数据库+写入缓存的服务A ,快很多很多。并且在并发上发生。而且因为数据库一般有加锁操作,写操作比读操作会慢很多。
这么看来【更新数据库,删除缓存】的方案,是可以在很大程度上保证数据一致性的。
消息队列
解决第一方案和第二方案,保证第二步成功
引入消息队列,完美的解决了,更新缓存失败的问题。
消息队列,在一致性问题上有几个特性。
一,保证消息存在,即使系统崩溃,消息成功消费前都不会丢失
二,保证消息成功被接受,如果接受失败则会一直等待消费者继续请求。
所以在数据库操作后,将缓存操作放入消息中发送队列。可以保证缓存
订阅数据库日志
在操作数据库后,不进行缓存操作。只在数据库变更日志BinLog中进行本次数据库操作的登记。
而程序内则订阅这个日志,当日志更新时,取里面的内容解析进行缓存的更新。
延迟双删
顾名思义,在数据库更新完数据库后,删除缓存,休眠一段时间,然后再次删除缓存。
这样做的目的是,防止在数据库更新完缓存后2,删除缓存后;有服务B进入读数据库,因为主从库同步的问题,且都是写操作,所以服务B很快的查询到了从库的旧值1,并且设置缓存。因为读操作很快,所以只需要休眠大于【主从复制】的时间和B线程在读到旧值写入缓存时间的时间,再一次删除缓存就行。
总结: 不管哪种方案,都无法做到数据库与缓存的强一致性的问题。因为在不牺牲性能的前提下,都很难去控制和判断DB操作和写缓存的时间。
如果消耗性能,则可以通过很多方面达到。比如设置分布式锁、读-写锁、甚至排他锁等等
分布式锁
分布式锁使用原因:
当一个服务被拆分成多个微服务,A\B\C\D时。
由于本地锁是加在本地单个服务,所以在分布式下,A服务修改数据库CODE=10,B服务不受本地锁影响,修改数据库CODE=9。
那么数据库CODE则在9和10之间飘忽不定,与期望结果不一致。
这时候就需要使用分布式锁,在各微服务之间共享同一锁场景。
Redisson
使用方法
redisson的使用都是通过RedissonClient对象进行具体操作,
所以在项目初始化时,需要注入一个装配好了的RedissonClient到ioc容器中。
@Bean(destroyMethod="shutdown") // 服务停止后调用 shutdown 方法。
public RedissonClient redisson() throws IOException {
// 1.创建配置
Config config = new Config();
// 集群模式
// config.useClusterServers().addNodeAddress("127.0.0.1:7004", "127.0.0.1:7001");
// 2.根据 Config 创建出 RedissonClient 示例。
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
return Redisson.create(config);
}
使用场景
创建可重入锁Lock
可重入锁,和ReentrantLock用法一样;属于用锁的线程阻塞其他线程,其他线程等待锁释放。
当拿到lock的线程A服务挂掉时,由看门狗机制释放锁。
看门狗: 当拿锁的线程挂掉了,默认设置中,是启动一个定时任务,每隔十秒重新给锁设置过期时间,过期时间为30秒。而因为锁的有效期为30秒,所以30秒后将自动解锁。
创建可重入读写锁RReadWriteLock
读写锁,指:读锁为共享锁,写锁为排他锁,所以读-读操作不受阻塞,其余和写相关操作均被阻塞。
RReadWriteLock rwlock = redisson.getReadWriteLock("anyRWLock");
// 最常见的使用方法
rwlock.readLock().lock(); //读锁
// 或
rwlock.writeLock().lock(); //写锁
```
信号量RSemaphore
信号量,停车位。
配合redis使用步骤:
定义一个停车场缓存,key:park value:3
指有3个车位,有三个线程能进入。
然后使用 park.release() 释放, **park.acquire()**站位
Redis分布式操作
使用方法
使用SETNX:
相当于,请求进入A服务,使用SETNX设置KEY.设置成功,进入业务,直到业务成功删除KEY。
请求进入B服务,一样使用SETNX设置KEY,设置失败,重复设置直到设置成功,进入业务。
缺点
服务A占据锁的时候,服务挂掉,就直接造成死锁了。
解决方案也就是,设置key的过期时间,但是这个时间根据业务来说非常难确定。
优化方案
既然不好确定过期时间,则可以在服务A获取到锁后,再进行对锁设置过期时间。
那么这个时间就可以完全根据业务需求进行判断。
但是因为两者都涉及到了设置过期时间问题,那么就有一个很大的漏洞。
当获取锁 与 设置过期时间之间发生异常时。
锁就无法正确的设置过期时间,那么一样会造成死锁问题。
那么要保证锁正确的设置过期时间,也只要将锁的获取与锁的过期时间设置,绑定为原子操作即可。
最优解决
以上不管那种形式,都是设置过期时间。
会发现都绕不开一个问题,这个过期时间与实际业务的时间长度无法衡量判断。
会造成A服务业务还没做完,锁就过期了,被服务B拿到锁,在服务B业务进行时,A就把这个锁强制删除了。
所以为了避免这种问题,则需要在每一把锁上顶上一个唯一记号。
A服务只能删除A服务的锁。
但是即使这样,也无法保证一个问题:
在A服务获取A服务锁值期间,B服务获取锁的值是否一定和A服务锁不同?
最最优解决
既然发现A服务查询A锁,和删除A锁不是一个原子操作。
那么只需要使用redis的脚本即可:
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);