概览:
1.中小公司Redis缓存架构以及线上问题分析
2.大厂线上大规模商品缓存数据冷热分离实战
3.实战解决大规模缓存击穿导致线上数据库压力暴增
4.黑客攻击导致缓存穿透线上数据库宕机bug
5.一行代码解决线上缓存穿透问题
6.一次大V直播带货导致线上商品系统崩溃原因分析
7.突发性热点缓存重建导致系统压力暴增问题分析
8.基于DCL机制解决热点缓存并发重建问题实战
9.Redis分布式锁解决缓存和数据库双写不一致问题
10.大促压力暴增导致分布式锁串行争用问题优化实战
11.一次微博明星热点事件导致系统崩溃原因分析
12.利用多级缓存架构解决Redis线上集群缓存雪崩问题
13.基于分布式实时计算机制实现JVM热点缓存存储
14.基于Sentine限流降级熔断机制解决Redis缓存雪崩问题
1.基础的Redis缓存架构
秒杀场景种关于Redis缓存使用中的一些问题及解决。
常规操作:
- 首先在插入对象的同时,要在Redis中缓存,以减轻数据库的访问压力
- 当数据发生更新时,也要同步更新Redis里的数据。此时有两种方式:(1)删除,重新插入(2)修改原值。
改进一:
- 一个数据放缓存里面时,要考虑数据量,如果数据量过大最好设置一个缓存时间。防止其一直占用内存空间,对Redis来说也有压力。
package com.example.service;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.example.dao.ProductDao;
import com.example.domin.Product;
import com.example.util.RedisKeyPrefixConst;
import com.example.util.RedisUtil;
import jodd.io.StreamUtil;
import org.redisson.Redisson;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
public class ProductService {
@Autowired
private RedisUtil redisUtil;
@Autowired
private ProductDao productDao;
@Autowired
private Redisson redisson;
public static final Integer PRODUCT_CACHE_TIMEOUT = 60*60*24;
@Transactional
public Product create(){
Product productResult = productDao.create();
redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE+productResult.getId(), JSON.toJSONString(productResult));
return productResult;
}
@Transactional
public Product update(Product product){
Product productResult = productDao.update(product);
redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE+productResult.getId(), JSON.toJSONString(productResult));
return productResult;
}
public Product get(Long productId){
Product product=null;
String producyCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE+productId;
String productStr= redisUtil.get(producyCacheKey);
if(!StringUtils.isEmpty(productStr)){
product=JSONObject.parseObject(productStr,Product.class);
return product;
}
return product;
}
}
当前实现存在的问题:在高并发场景下,redis可能发生的缓存击穿,缓存穿透、缓存雪崩等问题。
2.Redis框架高并发下问题分析
(1)缓存击穿
场景介绍:
比如有一批商品数据信息需要更新,通过批量导入来更新。这时Redis之前缓存的数据同时大量失效。这时请求进入数据库,数据库面临大访问量的压力,甚至可能崩掉。这种情况叫做缓存击穿。
即大量访问没有在redis内获取到信息,同时涌入数据库,导致数据库访问压力骤增的现象。
解决方案:超时时间相同,所以可能存在同时失效问题。对于由于缓存超时时间同时失效导致的缓存击穿问题,可以通过给超时时间加所随机数,来防止同一时间缓存失效问题。
public Product get(Long productId){
Product product=null;
String producyCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE+productId;
String productStr= redisUtil.get(producyCacheKey);
if(!StringUtils.isEmpty(productStr)){
product=JSONObject.parseObject(productStr,Product.class);
return product;
}
product = productDao.findProductById(productId);
if(product!=null){
redisUtil.set(producyCacheKey,JSON.toJSONString(product),PRODUCT_CACHE_TIMEOUT, TimeUnit.SECONDS);
}
return product;
}
private Integer getProductCacheTimeout(){
return PRODUCT_CACHE_TIMEOUT+new Random().nextInt(30)*60;
}
(2)缓存穿透
场景介绍:
后天人员把一个商品数据删除了,大量的访问查redis没查到,又查数据库也没查到。这就导致大量的访问即给到Redis,又给到数据库。Redis和数据库都面临访问压力骤增甚至瘫痪的问题。这种场景称为:缓存穿透。
即大量请求访问redis无法获取数据,转而访问数据库,造成redis和数据库都面临访问压力的情况,称为缓存穿透。(redis和数据库都面临大数据量查询压力,把缓存和数据库全部打穿,打崩)
解决方案:当查到数据库内没有这个数据时,在redis设置一个null值,下次就不查数据库了,来解决数据库的访问压力问题。
public Product get(Long productId){
Product product=null;
String producyCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE+productId;
String productStr= redisUtil.get(producyCacheKey);
if(!StringUtils.isEmpty(productStr)){
product=JSONObject.parseObject(productStr,Product.class);
return product;
}
product = productDao.findProductById(productId);
if(product!=null){
redisUtil.set(producyCacheKey,JSON.toJSONString(product),PRODUCT_CACHE_TIMEOUT, TimeUnit.SECONDS);
}else{
redisUtil.set(producyCacheKey,EMPTY_CACHE);
}
return product;
}
(3)突发性热点重构
场景介绍:
比如一个大V带货一个冷门商品(商品信息不在redis内),导致这个冷门商品突然变成热门商品(突发性热点|大量访问请求),导致页面崩掉(大量请求同一时间访问redis,没有获得信息,又同时访问数据库,压力给到数据库; 如果数据库能够支撑,此时又同时修改redis缓存,导致redis访问压力)。称之为突发性热点重构问题,通常导致访问压力暴增。
即:不在redis内的数据,突然受到大量访问。先大量访问压力给到数据库,后大量缓存重构请求同时执行,压力给到redis的现象,成为突发性热点重构问题。
解决方案:
1.提前缓存,只能解决已知的突发性热点重构问题,对于未知的突发性热点重构来说没用。
2.双重检测锁-单力模式,当大量请求访问数据库加锁,让一个请求取查数据库,并将值重构到redis内,其余请求在redis内获取数据,不在进入数据库。就不会发生上述大量请求同时访问数据库、同时设置redis的问题。
TipS: 重复代码随时重构,保证代码简洁性,保持良好编码习惯。
DCL:双重校验锁
public Product get(Long productId){
Product product=null;
String producyCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE+productId;
//DCL:DCL:double-checked locking 中文一般译为“双重检查锁
synchronized (this){
product =getProductFormCVache(producyCacheKey);
if(product!=null){
return product;
}
product = productDao.findProductById(productId);
if(product!=null){
redisUtil.set(producyCacheKey,JSON.toJSONString(product),PRODUCT_CACHE_TIMEOUT, TimeUnit.SECONDS);
}else{
redisUtil.set(producyCacheKey,EMPTY_CACHE);
}
}
return product;
}
private Integer getProductCacheTimeout(){
return PRODUCT_CACHE_TIMEOUT+new Random().nextInt(30)*60;
}
private Product getProductFormCVache(String productCacheKey){
Product product=null;
String productStr= redisUtil.get(productCacheKey);
if(!StringUtils.isEmpty(productStr)){
if(EMPTY_CACHE.equals(productStr)){
return null;
}
product=JSON.parseObject(productStr,Product.class);
}
return product;
}
改进:加锁可能导致的一些问题:当应对高并发场景时:
(1)当前使用的锁是synchronized,它是单机进程级别的锁
解决方式:分布式锁,在集群环境下也可以保证功能实现
(2)应该锁加到具体资源,不然访问商品101加锁时,1066也得排序,锁粒度要尽可能的小,不然会影响性能。
解决方式:全局对象池:用对象加锁,要维护对象池——麻烦
分布式锁:解决上述问题
SETNX:
setnx key value
将key的值设置为value, 当且仅当key不存在
若给定key已经存在,则SETNX不做任何动作。
SETNX是set if not esists的简写
可用版本: >=1.0.0
- 对于锁加一个前缀有利于后续维护工作,其次常量长一点没关系,信息更清楚。
- try catch finally要加锁,还要释放
public Product get(Long productId){
Product product=null;
String producyCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE+productId;
//DCL:DCL:double-checked locking 中文一般译为“双重检查锁
RLock hotCacheLock=redisson.getLock(LOCK_PRODUCT_HOT_CACHE_CREATE_PREFIX+productId);
hotCacheLock.lock();
try{
product =getProductFormCVache(producyCacheKey);
if(product!=null){
return product;
}
product = productDao.findProductById(productId);
if(product!=null){
redisUtil.set(producyCacheKey,JSON.toJSONString(product),PRODUCT_CACHE_TIMEOUT, TimeUnit.SECONDS);
}else{
redisUtil.set(producyCacheKey,EMPTY_CACHE);
}
}finally {
hotCacheLock.unlock();
}
return product;
}
(4)双写不一致
场景介绍:
线程3查了数据stock=10, 此时线程2写入stock=6,且先更新缓存stock=6.
此时线程3再去更新缓存使用的是旧值stock=10.这就是双写不一致问题。
update|delete缓存都有这个问题
https://blog.csdn.net/weixin_42201180/article/details/129244404
当前的代码也存在读写不一致 问题。
3-4种方案可以来解决
1.分布式锁:
同一个商品,一次一个人来改。锁粒度要尽可能小,锁太大会影响性能
public static final Integer PRODUCT_CACHE_TIMEOUT = 60*60*24;
public static final String EMPTY_CACHE = "{}";
public static final String LOCK_PRODUCT_HOT_CACHE_CREATE_PREFIX = "lock:product:hot_cache_create";
public static final String LOCK_PRODUCT_UPDATE_PREFIX = "lock:product:update";
@Transactional
public Product update(Product product){
Product productResult=null;
RLock updateProductLock = redisson.getLock(LOCK_PRODUCT_UPDATE_PREFIX+product.getId());
updateProductLock.lock();
try{
productResult = productDao.update(product);
redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE+productResult.getId(), JSON.toJSONString(productResult));
}finally {
updateProductLock.unlock();
}
return productResult;
}
改进一:
查询和修改串行进行:同一个商品排队执行,对于性能有损害
public Product get2(Long productId){
Product product=null;
String producyCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE+productId;
//DCL:DCL:double-checked locking 中文一般译为“双重检查锁
RLock hotCacheLock=redisson.getLock(LOCK_PRODUCT_HOT_CACHE_CREATE_PREFIX+productId);
hotCacheLock.lock();
//查
try{
product =getProductFormCVache(producyCacheKey);
if(product!=null){
return product;
}
//改
RLock updateProductLock=redisson.getLock(LOCK_PRODUCT_UPDATE_PREFIX+productId);
updateProductLock.lock();
try{
if(product!=null){
redisUtil.set(producyCacheKey,JSON.toJSONString(product), getProductCacheTimeout(), TimeUnit.SECONDS);
}else{
redisUtil.set(producyCacheKey,EMPTY_CACHE, getProductCacheTimeout(), TimeUnit.SECONDS);
}
product = productDao.findProductById(productId);
if(product!=null){
redisUtil.set(producyCacheKey,JSON.toJSONString(product),PRODUCT_CACHE_TIMEOUT, TimeUnit.SECONDS);
}else{
redisUtil.set(producyCacheKey,EMPTY_CACHE);
}
}finally {
updateProductLock.unlock();
}
}finally {
hotCacheLock.unlock();
}
return product;
}
分段锁:用不了,同一个资源没法分
2.读写锁:读多写少;读写互斥,读锁不互斥并行
RReadWriteLock readWriteLock=redisson.getReadWriteLock(LOCK_PRODUCT_UPDATE_PREFIX+productId);
RLock rLock=readWriteLock.readLock();
rLock.lock();
try{
if(product!=null){
redisUtil.set(producyCacheKey,JSON.toJSONString(product), getProductCacheTimeout(), TimeUnit.SECONDS);
}else{
redisUtil.set(producyCacheKey,EMPTY_CACHE, getProductCacheTimeout(), TimeUnit.SECONDS);
}
product = productDao.findProductById(productId);
if(product!=null){
redisUtil.set(producyCacheKey,JSON.toJSONString(product),PRODUCT_CACHE_TIMEOUT, TimeUnit.SECONDS);
}else{
redisUtil.set(producyCacheKey,EMPTY_CACHE);
}
}finally {
rLock.unlock();
}
写锁:
@Transactional
public Product update(Product product){
Product productResult=null;
//RLock updateProductLock = redisson.getLock(LOCK_PRODUCT_UPDATE_PREFIX+product.getId());
//updateProductLock.lock();
RReadWriteLock readWriteLock=redisson.getReadWriteLock(LOCK_PRODUCT_UPDATE_PREFIX+product.getId());
RLock writeLock=readWriteLock.writeLock();
writeLock.lock();
try{
productResult = productDao.update(product);
redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE+productResult.getId(), JSON.toJSONString(productResult));
}finally {
writeLock.unlock();
}
return productResult;
}
改进二:
锁一:突发性热点问题DCL_缓存重建,第一个人缓存,其他人不需要缓存重建
串行转并并发
改进三:
锁一和锁二能不能合并
(5)缓存雪崩
场景介绍:
有个粉丝量很大的大V账号,发了一个微博消息之后,微博挂了。
原因是:大V发的一条消息,只会落在分布式系统的一个节点上,当大量的粉丝访问这个节点时,就会造成缓存雪崩问题。
【Redis篇】Redis缓存之缓存雪崩_redis缓存雪崩-CSDN博客
Redis--缓存雪崩及解决方案_redis缓存雪崩-CSDN博客
Redis 面试常见问题:缓存雪崩、缓存击穿以及缓存穿透-腾讯云开发者社区-腾讯云
解决方法:多级缓存策略
private Map<String,Product> productMap=new ConcurrentHashMap<>();
public Product get(Long productId){
Product product=null;
String producyCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE+productId;
//DCL:DCL:double-checked locking 中文一般译为“双重检查锁
RLock hotCacheLock=redisson.getLock(LOCK_PRODUCT_HOT_CACHE_CREATE_PREFIX+productId);
hotCacheLock.lock();
try{
product =getProductFormCVache(producyCacheKey);
if(product!=null){
return product;
}
product = productDao.findProductById(productId);
if(product!=null){
redisUtil.set(producyCacheKey,JSON.toJSONString(product),PRODUCT_CACHE_TIMEOUT, TimeUnit.SECONDS);
productMap.put(producyCacheKey,product);
}else{
redisUtil.set(producyCacheKey,EMPTY_CACHE);
}
}finally {
hotCacheLock.unlock();
}
return product;
}
@Transactional
public Product update(Product product){
Product productResult=null;
//RLock updateProductLock = redisson.getLock(LOCK_PRODUCT_UPDATE_PREFIX+product.getId());
//updateProductLock.lock();
RReadWriteLock readWriteLock=redisson.getReadWriteLock(LOCK_PRODUCT_UPDATE_PREFIX+product.getId());
RLock writeLock=readWriteLock.writeLock();
writeLock.lock();
try{
productResult = productDao.update(product);
redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE+productResult.getId(), JSON.toJSONString(productResult));
productMap.put(RedisKeyPrefixConst.PRODUCT_CACHE+productResult.getId(),productResult);
}finally {
writeLock.unlock();
}
return productResult;
}
private Product getProductFormCVache(String productCacheKey){
Product product=productMap.get(productCacheKey);
if(product!=null){
return product;
}
String productStr= redisUtil.get(productCacheKey);
if(!StringUtils.isEmpty(productStr)){
if(EMPTY_CACHE.equals(productStr)){
return null;
}
product=JSON.parseObject(productStr,Product.class);
}
return product;
}
改进:
多级缓存还有可能存在的问题:内存比redis性能更高,问题是只更新一台服务器,其他服务器怎么办,还是有数据不一致问题。
多级缓存还可能存在的问题
1.缓存不够放,内存溢出问题,堆内存
缓存框架,使用淘汰策略,淘汰不用的缓存
2.单机map,其他服务器怎么同步
借助mq更新其他缓存,让其他人监听,然后更新自己的数据,
3.思考
耦合太多插件,虽然解决了问题,但不优雅
解决数据不一致问题:如何优雅呢?让热点缓存框架,实现更加优雅?