安装 redis
使用 redis
1、导入依赖
1 | <dependency> |
2、配置 redis
1 | spring: |
3、自动注入 StringRedisTemplate
1 | ValueOperations<String, String> ops = stringRedisTemplate.opsForValue(); |
Redis 堆外内存溢出
当进行压力测试时后期后出现堆外内存溢出OutOfDirectMemoryError
产生原因:
1)、springboot2.0以后默认使用lettuce作为操作redis的客户端,它使用netty进行网络通信
2)、lettuce的bug导致netty堆外内存溢出。netty如果没有指定堆外内存,默认使用Xms的值,可以使用-Dio.netty.maxDirectMemory
进行设置
解决方案:由于是lettuce的bug造成,不能直接使用-Dio.netty.maxDirectMemory去调大虚拟机堆外内存,治标不治本。
1)、升级lettuce客户端。但是没有解决的
2)、切换使用jedis
1 | <dependency> |
lettuce和jedis是操作redis的底层客户端,RedisTemplate是再次封装
缓存失效
缓存穿透
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。
解决:缓存空对象、布隆过滤器
缓存雪崩
缓存雪崩是指在我们设置缓存时key采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。
解决方案:
》缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
》如果缓存数据库是分布式部署,将热点数据均匀分布在不同缓存数据库中。
》设置热点数据永远不过期。
》出现雪崩:降级 熔断
事前:尽量保证整个 redis 集群的高可用性,发现机器宕机尽快补上。选择合适的内存淘汰策略。
事中:本地ehcache缓存 + hystrix限流&降级,避免MySQL崩掉
事后:利用 redis 持久化机制保存的数据尽快恢复缓存
缓存击穿
雪崩、击穿不同的是:
》击穿:并发查同一条数据。缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力
》雪崩:不同数据都过期了,很多数据都查不到从而查数据库。
解决方案:
》设置热点数据永远不过期。
》加互斥锁:业界比较常用的做法,是使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db去数据库加载,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。
解决方案
1)、雪崩:加入随机时间。避免同时失效
2)、穿透:加入空对象+失效时间。
3)、击穿:加排它锁。
本地锁
可以使用,synchronized,或 JUC 下的 lock。
锁时序问题
下面黑色的锁。结果放入缓存
也要放入锁中,保证原子性。否则,可能造成多次查询。
本地锁在分布式下的问题
关于本地锁,在分布式情况下会出现查询多次的情况。每个微服务都要有缓存服务、数据更新时只更新自己的缓存,造成缓存数据不一致
解决方案:分布式缓存,微服务共用 缓存中间件:redis,zookeeper
分布式锁,基本原理
相当于厕所占坑。多个人去厕所,一个人占坑,其他人只能排队。等待它蹲完,释放坑位的时候,其他人才能再次占坑。
分布式锁,阶段1:SET NX
分布式锁,阶段2:EX
分布式锁,阶段3:SET NX EX
分布式锁,阶段4:UUID
分布式锁,阶段5:LUA
参考:使用Redis的分布式锁
1 | if redis.call("get",KEYS[1]) == ARGV[1] then |
Redisson 简介
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。其中包括(BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service)
Redisson提供了使用Redis的最简单和最便捷的方法。
Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。
Redisson 使用
1、导入依赖
1 | <dependency> |
2、配置 redisson
1 |
|
可重入锁(Reentrant Lock)
A调用B。AB都需要同一锁,此时可重入锁就可以重入,A就可以调用B。不可重入锁时,A调用B将死锁。
基于Redis的Redisson分布式可重入锁RLock Java对象实现了java.util.concurrent.locks.Lock
接口。
1 | // 参数为锁名字 |
大家都知道,如果负责储存这个分布式锁的Redisson节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。
默认情况下,看门狗的检查锁的超时时间是30秒(每到20s就会自动续借成30s,是1/3的关系),也可以通过修改Config.lockWatchdogTimeout来另行指定。
1 | // 加锁以后10秒钟自动解锁,看门狗不续命 |
Redisson同时还为分布式锁提供了异步执行的相关方法:
1 | RLock lock = redisson.getLock("anyLock"); |
RLock对象完全符合Java的Lock规范。也就是说只有拥有锁的进程才能解锁,其他进程解锁则会抛出IllegalMonitorStateException
错误。
1 | public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisson() { |
读写锁(ReadWriteLock)
基于Redis的Redisson分布式可重入读写锁RReadWriteLock
Java对象实现了java.util.concurrent.locks.ReadWriteLock
接口。其中读锁和写锁都继承了RLock接口。
分布式可重入读写锁允许同时有多个读锁和一个写锁处于加锁状态。
1 | RReadWriteLock rwlock = redisson.getReadWriteLock("anyRWLock"); |
1 | // 10秒钟以后自动解锁 |
总结:读锁(共享锁)、写锁(排它锁)。
信号量(Semaphore)
闭锁(CountDownLatch)
缓存一致性
缓存和数据库一致性。
双写模式
DB更新,缓存也更新。
失效模式
DB更新,删除缓存。
解决方案
我们系统的一致性解决方案:
1、缓存的所有数据都有过期时间,数据过期下一次查询主动更新。
2、读写数据的时候,加上分布式的读写锁。
Spring Cache
穿透:缓存空对象。spring.cache.redis.cache-null-values=true
击穿:读缓存,加锁。@Cacheable(sync=true)
雪崩:指定过期时间 1h。spring.cache.redis.time-to-live=3600000