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缓存都扛不住了怎么办嘞。

 

posted @ 2022-06-16 01:20  小罗咯  阅读(221)  评论(0)    收藏  举报