redis缓存
redis 除了可以作分布式锁,同时可以作缓存来使用。
假如让你使用redis设计一个缓存。
代码如下:
get(Long productId){
String product_key = "前缀"+objectId;
Object object = redis.get(product_key);
if(ObjectUtils.isNull(object)){
redis.expire(product_key,24*60*60,TimeUnit.SECONDS);
return object;
}
object = productDao.get(productId);
if(object !=null){
redis.set(product_key,object,24*60*60,TimeUnit.SECONDS);
}
return object;
}
private Integer genProductCacheTimeOut(){
return ”基本“
}
这段代码会有问题:缓存击穿、穿透和雪崩的问题
缓存击穿解决:重点在穿,穿过redis访问数据库,同一时间热点数据过期导致大量请求访问DB (热点数据缓存过期)
解决思路:互斥锁,
缓存雪崩:重点在崩,如果缓存集中在一段时间内失效或者redis故障,引发大量缓存击穿,查询都落在数据库上,造成缓存雪崩。
解决思路:
a. 大量数据过期:
均匀设置过期时间:new Random().nextInt(10)
互斥锁(重点):如果发现访问数据不在Redis里,就加个互斥锁,保证同一时间只有一个请求来构建缓存,当缓存构建完成后,再释放锁。实现互斥锁的啥时候,最好设置超时时间。可以同时解决这种情况:突发性热点缓存重建导致系统压力暴增。
代码如下:
Lock lock = redission.getLock("key");
lock.lock();
try{
product = getProductFromCache(key);
if(product!=null) return product;
product = productDao.get(productId);
if(product!=null){
redisUtil.set(key,value,Timeout,...);
}
}
双key策略:
缓存数据可以使用两个key,一个是主key,会设置过期时间,一个是备key,不会设置过期时间,它们只是key不一样,但是value值是一样的,相当于给缓 存做了个副本。 当业务线程访问不到主key的时候,就会直接返回副key的缓存数据,同时更新主key和备key的数据。
后台更新缓存:
业务线程不再更新缓存,缓存也不设置有效期,而是让缓存永久有效,并将更新缓存的工作交由后台线程定时更新。
缓存数据不设置有效期,并不是意味着一直在内存,因为当系统内存紧张的时候,有些缓存数据会被淘汰,而在缓存被淘汰到下一次后台定时更新缓存这段时间内,业务线程读取缓存失败就返回空值,业务视角就以为数据丢失了。
解决上面问题的方式有两种。
第一种:后台线程不仅负责定时更新缓存,也负责频繁检测缓存是否有效,检测到缓存失效了,马上从数据库读取数据,并更新缓存。
第二种:业务线程发现缓存数据失效后(缓存数据被淘汰),通过消息队列发送给一条消息通知后台线程更新缓存,后台线程收到消息后,在更新缓存前
可以判断缓存是否存在,存在就不执行更新缓存操作;不存在就读取数据库数据,并将数据加载到缓存。这种方式相比第一种方式缓存的更新会更及时,
用户体验也比较好。
b. redis故障宕机:
服务熔断或请求限流: 因为Redis故障宕机而导致缓存雪崩问题时,可以启动服务熔断机制,暂停业务应用对缓存服务的访问,直接返回错误。
为了减少对业务的影响,可以启用请求限流机制,只将少部分请求发送到数据库进行处理,再多请求在入口直接拒绝服务,等到Redis恢复正常并把缓存预热完后,再解除。
构建Redis缓存高可用集群:最好通过主从节点方式构建Redis缓存高可靠集群。如果Redis缓存的主节点故障宕机,从节点可以切换成为主节点,继续提供缓存服务,避免宕机。
缓存穿透:重点在透上,查空的数据,缓存没有,再查数据库,数据库也没有,透了。
解决思路:
1. 默认空值或者默认值
String productStr = redisUtil.get(key);
product = product.get(productId)
if(product!=null){
//设置缓存啥啥啥的
}else{
//将空缓存设置进缓存,包括过期时间
}
2. 使用布隆过滤器快速判断数据是否存在。
布隆过滤器由初始值都为0的位图数组和N个哈希函数两部分组成。当我们在写入数据库数据时,布隆过滤器里做个标记,下次查询数据是否在数据
库时,只需要查询布隆过滤器,如果查询到数据没有被标记,说明不在数据库中。
布隆过滤器会通过3个操作完成标记:
第一步:使用N个哈希函数分别对数据做哈希计算,得到N个哈希值。
第二步:对位图长度取模,得到每个哈希值在位图数组的对应位置。
第三步:将每个哈希值在位图数组对应位置的值设置为1。
redis还会出现缓存不一致的情况,
线程1 ---------------------------------------------------------------> 写数据库 stock =6 ------->更新或删除缓存
线程2 ----->查缓存(空) ------------->查询数据库stock =10----------------------------------------------------------------->更新缓存为stock
线程3 ----->写数据库stock=10 --->更新缓存stock =10
redis在写数据会有并发写的时候,此时后一个写的结果覆盖前一个写的结果,而查询数据的时候却只查询到前一个就开始更新缓存。
这个问题是并发的读写导致的。
因此可以通过加锁,无论是读或者写都需要加分布式锁排队执行,但是串行执行又会出现性能问题,此时redission提供了读写锁来解决这个问题
读锁与读锁之间是不互斥的,写锁与写锁,读锁与写锁是互斥的。
Lock firstlock = Redission.getLock(“前缀”+key);
firstlock.lock();
try{
product = getresult();
if(product!=null){
return product;
}
RReadWriteLock readWriterLock = redission.getReadWriteLock(key);
Lock secondlock = readWriterLock .readLock();
secondlock.lock() //加读锁
try{
product = productDao.get(key);
if(product !=null){
redis.set(key,value,timeout,seconds);
}else{
//空啥的
}
}fially{
secondlock.unlock();
}finallu{
firstlock.unlock();
}
此时总结一下,在get的时候会上两把锁:
第一把锁 redission.getLock(key) 是为了解决并发重建问题,避免缓存击穿问题,每次只允许单个进行访问,此时的key是加了并发重建的前缀的,用做区分。
其是为了保证每个读请求只能有一个进入代码块
第二把锁是读写锁,主要解决的是缓存不一致的问题 其key加了读写锁的前缀。
其目的是为了解决读写时并发的问题。
你以为上面的代码就完美了嘛?
盲生发现了一个华点,第一把锁阻塞的线程每次都要抢锁,然后每次就放进来一个,然后解锁再进来一个,这个有必要?
没有必要。
firstlock.trylock(time,seconds); 等待三秒,直接去查缓存。 第一个是串行,其余是并行。但是有个前提是,time一定要大于重建缓存的时间。
但是这样子就真的真的完美了嘛?如果高并发来了以后,你的redis缓存都扛不住了怎么办嘞。

浙公网安备 33010602011771号