Redis高并发缓存架构实战

示例代码:

@Service
public class ProductService {

    @Autowired
    private ProductDao productDao;

    @Autowired
    private RedisUtil redisUtil;

    @Autowired
    private Redisson redisson;

    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:";
    public static Map<String, Product> productMap = new HashMap<>();

	// 创建商品,把商品加入redis
    @Transactional
    public Product create(Product product) {
        Product productResult = productDao.create(product);
        redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), JSON.toJSONString(productResult));
        return productResult;
    }

	// 更新商品,从redis中更新
    @Transactional
    public Product update(Product product) {
        Product productResult = productDao.update(product);
        redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), JSON.toJSONString(productResult));
        return productResult;
    }

	// 获取商品,从redis中获取
    public Product get(Long productId) {
        Product product = null;
        String productCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE + productId;

        //从缓存里查数据
        String productStr = redisUtil.get(productCacheKey);
        if (!StringUtils.isEmpty(productStr)) {
            product = JSON.parseObject(productStr, Product.class);
            return product;
        }

        product = productDao.get(productId);
        if (product != null) {
            redisUtil.set(productCacheKey, JSON.toJSONString(product));
        }
        return product;
    }
}

1、中小公司Redis缓存架构以及线上问题分析

上面示例代码就是一般公司缓存数据的增删改查的操作。

新增的时候把数据放入缓存,更新的时候更新缓存,删除就删除缓存,查询的时候就先判断是否在缓存中,如果在就直接返回,没在就先从DB中查询,然后放入缓存,在返回。

如果公司线上并发量和流量不大的话,这样使用是没有什么问题的,但是并发量一旦上去了,数据量也很大的话,就会产生各种各样的问题。

冷门数据浪费redis资源

如果是淘宝,京东这种数据量很大的电商网站,他们的商品页可能就上亿级别,如果全部存储到redis的话,对redis的存储容量有很大的浪费,因为每天高频访问的数据其实是很少的,占比可能不到1%。其余99%的冷门商品也没有必要放到redis中了。

放入缓存的目的其实就是为了让热门的数据查询更快,效率更高,减少对DB的查询。

解决方案:缓存数据冷热分离

维护数据的时候给每个数据加上一个过期时间,冷门数据到期过后就会自动剔除了。热门数据在查询的时候给他做一个缓存延期(如果是从缓存中查询到的,就把它的过期时间重新刷新)。

public Product get(Long productId) {
	Product product = null;
	String productCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE + productId;

	//从缓存里查数据
	String productStr = redisUtil.get(productCacheKey);
	if (!StringUtils.isEmpty(productStr)) {
		product = JSON.parseObject(productStr, Product.class);
		// 缓存读延期
		redisUtil.expire(productCacheKey, genProductCacheTimeout(), TimeUnit.SECONDS);
		return product;
	}

	product = productDao.get(productId);
	if (product != null) {
		redisUtil.set(productCacheKey, JSON.toJSONString(product), PRODUCT_CACHE_TIMEOUT, TimeUnit.SECONDS);
	}
	return product;
}

3、实战解决大规模缓存击穿导致线上数据库压力暴增

缓存击穿(缓存失效)

如果是商品要上架,在后台肯定有管理员对商品就行批量导入或者批量更新,按照我们现在代码的架构这时批量更新的商品的缓存过期时间都是一样的。就有可能大批量的商品同时过期,请求时都会直接到达DB,给DB造成很大的压力。

上面这个就是所谓的缓存击穿(缓存失效)

解决方案

添加过期时间的时候,进行随机添加过期时间,而不是加一样的过期时间。

这就不会造成在同一时间缓存批量失效,直接打到DB上。

@Transactional
public Product update(Product product) {
	Product productResult = productDao.update(product);
	redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), JSON.toJSONString(productResult),
			genProductCacheTimeout(), TimeUnit.SECONDS);
	return productResult;
}

private Integer genProductCacheTimeout() {
	//加随机超时机制解决缓存批量失效(击穿)问题
	return PRODUCT_CACHE_TIMEOUT + new Random().nextInt(5) * 60 * 60;
}

4、黑客攻击导致缓存穿透线上数据库宕机bug

缓存穿透

示例一

还是以电商为例,比如在秒杀时,某一个商品的访问并发量肯定很大,但是管理员不小心在后台把该商品的数据全部删除了,包括缓存中的数据。根据我们现有的架构,前端大量的请求就会先查询缓存,缓存没有,接着就会查询DB,但是DB也没有,直接就把后端给穿透了。

示例二

比如有些url就包含了一些含义,比如产品ID等。黑客直接就根据url把产品ID随便设置一个,然后用压测工具,高并发请求来调用这个接口,这个就会产生和示例一一样的结果,直接从后端把缓存和DB穿过去了。就会给DB造成很大的压力。

这种情况其实在很多层面都会防范,我们这里就不讨论安全层面的防范,主要来看后端代码层面是怎么防范的。

解决方案

直接在redis中做一个空缓存,但是要给这个空缓存设置过期时间,防止大范围的空缓存占用存储空间。

// 空缓存
public static final String EMPTY_CACHE = "{}";

public Product get(Long productId) {
	Product product = null;
	String productCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE + productId;

	//从缓存里查数据
	String productStr = redisUtil.get(productCacheKey);
	if (!StringUtils.isEmpty(productStr)) {
		// 如果是空缓存直接返回
		if (EMPTY_CACHE.equals(productStr)) {
			// 给空缓存读延期
			redisUtil.expire(productCacheKey, genEmptyCacheTimeout(), TimeUnit.SECONDS);
			return new Product();
		}
		product = JSON.parseObject(productStr, Product.class);
		// 缓存读延期
		redisUtil.expire(productCacheKey, genProductCacheTimeout(), TimeUnit.SECONDS);
		return product;
	}

	product = productDao.get(productId);
	if (product != null) {
		redisUtil.set(productCacheKey, JSON.toJSONString(product), PRODUCT_CACHE_TIMEOUT, TimeUnit.SECONDS);
	} else {
		// 设置空缓存解决缓存穿透问题
		redisUtil.set(productCacheKey, EMPTY_CACHE, genEmptyCacheTimeout(), TimeUnit.SECONDS);
	}
	return product;
}

// 随机过期时间
private Integer genEmptyCacheTimeout() {
	return 60 + new Random().nextInt(30);
}

5、一次大V直播带货导致线上商品系统崩溃原因分析

背景

一些大V擅长把一些冷门商品在直播间进行售卖,粉丝基础高的大V,直播间观看量可能达到上百万。同一时间就有可能有几万,几十万的请求到这个冷门商品。

突发性热点缓存

冷门商品一般在缓存中过期了,是没有的,那么就会直接达到DB,DB的抗并发能力是不强的。在DB查询到以后就会重建缓存。

这其实就叫做:突发性热点缓存重建导致系统压力暴增

解决方案

使用DCL(双重检测锁)机制解决热点缓存并发重建。

在查询DB,重建缓存的时候给加一把锁(这里使用分布式锁),让其中抢占到锁的那个线程去查询数据并重建缓存。

public Product get(Long productId) {
	Product product = null;
	String productCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE + productId;

	// 从缓存里查数据
	product = getProductFromCache(productCacheKey);
	if (product != null) {
		return product;
	}

	// 加分布式锁解决热点缓存并发重建问题
	RLock hotCreateCacheLock = redisson.getLock(LOCK_PRODUCT_HOT_CACHE_CREATE_PREFIX + productId);
	hotCreateCacheLock.lock();
	try {
		// 从缓存里查数据
		product = getProductFromCache(productCacheKey);
		if (product != null) {
			return product;
		}

		product = productDao.get(productId);
		if (product != null) {
			redisUtil.set(productCacheKey, JSON.toJSONString(product), PRODUCT_CACHE_TIMEOUT, TimeUnit.SECONDS);
		} else {
			// 设置空缓存解决缓存穿透问题
			redisUtil.set(productCacheKey, EMPTY_CACHE, genEmptyCacheTimeout(), TimeUnit.SECONDS);
		}
	} finally {
		hotCreateCacheLock.unlock();
	}

	return product;
}

private Product getProductFromCache(String productCacheKey) {
	Product product = null;
	String productStr = redisUtil.get(productCacheKey);
	if (!StringUtils.isEmpty(productStr)) {
		if (EMPTY_CACHE.equals(productStr)) {
			redisUtil.expire(productCacheKey, genEmptyCacheTimeout(), TimeUnit.SECONDS);
			return new Product();
		}
		product = JSON.parseObject(productStr, Product.class);
		//缓存读延期
		redisUtil.expire(productCacheKey, genProductCacheTimeout(), TimeUnit.SECONDS);
	}
	return product;
}

6、Redis分布式锁解决缓存与数据库双写不一致问题实战

双写不一致

就比如上图所示,线程-1先写数据库stock=10,然后接着写缓存,但是在更新缓存的时候由于卡顿,这个期间线程-2直接把数据库与缓存更新为了stock=6,线程-1的网络恢复正常,接着把缓存更新为stock=10。这时数据库中stock=6,缓存中stock=10。这就造成了缓存与数据库的数据不一致了。

读写并发不一致

如图所示这次是写数据库中,直接删除缓存。最后也会造成数据库和缓存的值是不一致的。

解决方案

在操作DB和操作缓存的步骤之间使用分布式锁。

@Transactional
public Product update(Product product) {
	Product productResult = null;
	// 加分布式读锁解决缓存双写不一致问题
	RLock productUpdateLock = redisson.getLock(LOCK_PRODUCT_UPDATE_PREFIX + product.getId());
	productUpdateLock.lock();
	try {
		productResult = productDao.update(product);
		redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), JSON.toJSONString(productResult),
				genProductCacheTimeout(), TimeUnit.SECONDS);
	} finally {
		productUpdateLock.unlock();
	}
	return productResult;
}

public Product get(Long productId) {
	Product product = null;
	String productCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE + productId;

	// 从缓存里查数据
	product = getProductFromCache(productCacheKey);
	if (product != null) {
		return product;
	}

	// 加分布式锁解决热点缓存并发重建问题
	RLock hotCreateCacheLock = redisson.getLock(LOCK_PRODUCT_HOT_CACHE_CREATE_PREFIX + productId);
	hotCreateCacheLock.lock();
	try {
		// 从缓存里查数据
		product = getProductFromCache(productCacheKey);
		if (product != null) {
			return product;
		}

		// 加分布式读锁解决缓存双写不一致问题
		RLock productUpdateLock = redisson.getLock(LOCK_PRODUCT_UPDATE_PREFIX + productId);
		productUpdateLock.lock();
		try {
			product = productDao.get(productId);
			if (product != null) {
				redisUtil.set(productCacheKey, JSON.toJSONString(product), PRODUCT_CACHE_TIMEOUT, TimeUnit.SECONDS);
			} else {
				// 设置空缓存解决缓存穿透问题
				redisUtil.set(productCacheKey, EMPTY_CACHE, genEmptyCacheTimeout(), TimeUnit.SECONDS);
			}
		} finally {
			productUpdateLock.unlock();
		}
	} finally {
		hotCreateCacheLock.unlock();
	}

	return product;
}

分布式锁优化

其实在一些电商页面,用户大多数的时候都是读操作,而写操作是很少的(加入购物车,生成订单等)。

这个其实就是典型的 读多写少 的场景,这种场景下,分布式锁可以用读写锁来优化。

redisson中就有读写锁的实现:https://github.com/redisson/redisson/wiki/8.-%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E5%92%8C%E5%90%8C%E6%AD%A5%E5%99%A8#85-%E8%AF%BB%E5%86%99%E9%94%81readwritelock

@Transactional
public Product update(Product product) {
	Product productResult = null;
	// 加分布式读锁解决缓存双写不一致问题
//        RLock productUpdateLock = redisson.getLock(LOCK_PRODUCT_UPDATE_PREFIX + product.getId());
//        productUpdateLock.lock();
	//RLock productUpdateLock = redisson.getLock(LOCK_PRODUCT_UPDATE_PREFIX + product.getId());
	RReadWriteLock productUpdateLock = redisson.getReadWriteLock(LOCK_PRODUCT_UPDATE_PREFIX + product.getId());
	RLock writeLock = productUpdateLock.writeLock();
	writeLock.lock();
	try {
		productResult = productDao.update(product);
		redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), JSON.toJSONString(productResult),
				genProductCacheTimeout(), TimeUnit.SECONDS);
	} finally {
//            productUpdateLock.unlock();
		writeLock.unlock();
	}
	return productResult;
}

public Product get(Long productId) {
	Product product = null;
	String productCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE + productId;

	// 从缓存里查数据
	product = getProductFromCache(productCacheKey);
	if (product != null) {
		return product;
	}

	// 加分布式锁解决热点缓存并发重建问题
	RLock hotCreateCacheLock = redisson.getLock(LOCK_PRODUCT_HOT_CACHE_CREATE_PREFIX + productId);
	hotCreateCacheLock.lock();
	try {
		// 从缓存里查数据
		product = getProductFromCache(productCacheKey);
		if (product != null) {
			return product;
		}

		// 加分布式读锁解决缓存双写不一致问题
//            RLock productUpdateLock = redisson.getLock(LOCK_PRODUCT_UPDATE_PREFIX + productId);
//            productUpdateLock.lock();
		RReadWriteLock productUpdateLock = redisson.getReadWriteLock(LOCK_PRODUCT_UPDATE_PREFIX + productId);
		RLock rLock = productUpdateLock.readLock();
		rLock.lock();
		try {
			product = productDao.get(productId);
			if (product != null) {
				redisUtil.set(productCacheKey, JSON.toJSONString(product), PRODUCT_CACHE_TIMEOUT, TimeUnit.SECONDS);
			} else {
				// 设置空缓存解决缓存穿透问题
				redisUtil.set(productCacheKey, EMPTY_CACHE, genEmptyCacheTimeout(), TimeUnit.SECONDS);
			}
		} finally {
//                productUpdateLock.unlock();
			rLock.unlock();
		}
	} finally {
		hotCreateCacheLock.unlock();
	}

	return product;
}

读写锁原理

redisson读写锁的源码和分布式锁的原理差不多,主要还是看里面的 lua 源码,里面 mode 这个属性,如果是读锁,这个值就是read,如果是写锁这个值就是write。

8、一次微博明星热点事件导致系统崩溃原因分析

微博上面的流量明星,他的粉丝数可能上亿,如果他产生了热点事件,可能就会同一时间上百万,上千万的用户来查看,redis可能也扛不住,就会产生一系列级联问题。

缓存雪崩

缓存雪崩指的是缓存层支撑不住或宕掉后, 流量会像奔逃的野牛一样, 打向后端存储层。

由于缓存层承载着大量请求, 有效地保护了存储层, 但是如果缓存层由于某些原因不能提供服务(比如超大并发过来,缓存层支撑不住,或者由于缓存设计不好,类似大量请求访问bigkey,导致缓存能支撑的并发急剧下降), 于是大量请求都会打到存储层, 存储层的调用量会暴增, 造成存储层也会级联宕机的情况。

解决方案

1) 保证缓存层服务高可用性,比如使用Redis Sentinel或Redis Cluster。

2) 依赖隔离组件为后端限流熔断并降级。比如使用Sentinel或Hystrix限流降级组件。

比如服务降级,我们可以针对不同的数据采取不同的处理方式。当业务应用访问的是非核心数据(例如电商商品属性,用户信息等)时,暂时停止从缓存中查询这些数据,而是直接返回预定义的默认降级信息、空值或是错误提示信息;当业务应用访问的是核心数据(例如电商商品库存)时,仍然允许查询缓存,如果缓存缺失,也可以继续通过数据库读取。

3) 提前演练。 在项目上线前, 演练缓存层宕掉后, 应用以及后端的负载情况以及可能出现的问题, 在此基础上做一些预案设定。

9、利用多级缓存架构解决Redis线上集群缓存雪崩问题

redis抗并发是有上线的,JVM层面的缓存抗并发比redis高了很多。JVM层面的缓存其实就可以使用Ehcache和Guava Cache这样的本地缓存。

这里我们就写一下伪代码,用map(实际生产不会使用map)来演示一下:

private Product getProductFromCache(String productCacheKey) {
	Product product = null;
	// 多级缓存查询,jvm级别缓存可以交给单独的热点缓存系统统一维护,有变动推送到各个web应用系统自行更新
	product = productMap.get(productCacheKey);
	if (product != null) {
		return product;
	}
	String productStr = redisUtil.get(productCacheKey);
	if (!StringUtils.isEmpty(productStr)) {
		if (EMPTY_CACHE.equals(productStr)) {
			redisUtil.expire(productCacheKey, genEmptyCacheTimeout(), TimeUnit.SECONDS);
			return new Product();
		}
		product = JSON.parseObject(productStr, Product.class);
		//缓存读延期
		redisUtil.expire(productCacheKey, genProductCacheTimeout(), TimeUnit.SECONDS);
	}
	return product;
}

在代码同步更新JVM级别缓存是在JVM级别,并不是在集群里面全局的。

多级缓存查询,jvm级别缓存可以交给单独的热点缓存系统统一维护,有变动推送到各个web应用系统自行更新,或者使用MQ来做。

有了多级缓存过后就不要考虑绝对一致,达到最终一致就行。架构都有利弊,看如何选择。

最终完整代码如下:

@Service
public class ProductService {

    @Autowired
    private ProductDao productDao;

    @Autowired
    private RedisUtil redisUtil;

    @Autowired
    private Redisson redisson;

    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:";
    public static Map<String, Product> productMap = new HashMap<>();

    @Transactional
    public Product create(Product product) {
        Product productResult = productDao.create(product);
        redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), JSON.toJSONString(productResult),
                PRODUCT_CACHE_TIMEOUT, TimeUnit.SECONDS);
        return productResult;
    }

    @Transactional
    public Product update(Product product) {
        Product productResult = null;
        // 加分布式读锁解决缓存双写不一致问题
//        RLock productUpdateLock = redisson.getLock(LOCK_PRODUCT_UPDATE_PREFIX + product.getId());
//        productUpdateLock.lock();
        //RLock productUpdateLock = redisson.getLock(LOCK_PRODUCT_UPDATE_PREFIX + product.getId());
        RReadWriteLock productUpdateLock = redisson.getReadWriteLock(LOCK_PRODUCT_UPDATE_PREFIX + product.getId());
        RLock writeLock = productUpdateLock.writeLock();
        writeLock.lock();
        try {
            productResult = productDao.update(product);
            redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), JSON.toJSONString(productResult),
                    genProductCacheTimeout(), TimeUnit.SECONDS);
        } finally {
//            productUpdateLock.unlock();
            writeLock.unlock();
        }
        return productResult;
    }

    public Product get(Long productId) {
        Product product = null;
        String productCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE + productId;

        // 从缓存里查数据
        product = getProductFromCache(productCacheKey);
        if (product != null) {
            return product;
        }

        // 加分布式锁解决热点缓存并发重建问题
        RLock hotCreateCacheLock = redisson.getLock(LOCK_PRODUCT_HOT_CACHE_CREATE_PREFIX + productId);
        hotCreateCacheLock.lock();
        try {
            // 从缓存里查数据
            product = getProductFromCache(productCacheKey);
            if (product != null) {
                return product;
            }

            // 加分布式读锁解决缓存双写不一致问题
//            RLock productUpdateLock = redisson.getLock(LOCK_PRODUCT_UPDATE_PREFIX + productId);
//            productUpdateLock.lock();
            RReadWriteLock productUpdateLock = redisson.getReadWriteLock(LOCK_PRODUCT_UPDATE_PREFIX + productId);
            RLock rLock = productUpdateLock.readLock();
            rLock.lock();
            try {
                product = productDao.get(productId);
                if (product != null) {
                    redisUtil.set(productCacheKey, JSON.toJSONString(product), PRODUCT_CACHE_TIMEOUT, TimeUnit.SECONDS);
                } else {
                    // 设置空缓存解决缓存穿透问题
                    redisUtil.set(productCacheKey, EMPTY_CACHE, genEmptyCacheTimeout(), TimeUnit.SECONDS);
                }
            } finally {
//                productUpdateLock.unlock();
                rLock.unlock();
            }
        } finally {
            hotCreateCacheLock.unlock();
        }

        return product;
    }


    private Integer genProductCacheTimeout() {
        //加随机超时机制解决缓存批量失效(击穿)问题
        return PRODUCT_CACHE_TIMEOUT + new Random().nextInt(5) * 60 * 60;
    }

    private Integer genEmptyCacheTimeout() {
        return 60 + new Random().nextInt(30);
    }

    private Product getProductFromCache(String productCacheKey) {
        Product product = null;
        //多级缓存查询,jvm级别缓存可以交给单独的热点缓存系统统一维护,有变动推送到各个web应用系统自行更新
        product = productMap.get(productCacheKey);
        if (product != null) {
            return product;
        }
        String productStr = redisUtil.get(productCacheKey);
        if (!StringUtils.isEmpty(productStr)) {
            if (EMPTY_CACHE.equals(productStr)) {
                redisUtil.expire(productCacheKey, genEmptyCacheTimeout(), TimeUnit.SECONDS);
                return new Product();
            }
            product = JSON.parseObject(productStr, Product.class);
            //缓存读延期
            redisUtil.expire(productCacheKey, genProductCacheTimeout(), TimeUnit.SECONDS);
        }
        return product;
    }

}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值