小公司并发量不大的情况下,问题不是很大,但是大公司高并发量,会出现大量问题,列举如下:
存在的问题:
1. 缓存容量小问题:几百G的海量数据不可能一直都放到redis缓存中,大大降低redis(<10G)作为内存数据库的效率
解决方案:设置固定过期时间,比如说一天,虽然一开始redis数据量很大,但是一天之后,会有大量数据失效,达到冷热数据的分离。
jedis.set(SystemConstants.REDIS_KEY_PREFIX + product.getProductId(), gson.toJson(product)); jedis.expire(SystemConstants.REDIS_KEY_PREFIX + product.getProductId(), SystemConstants.REDIS_KEY_EXPIRED_TIME); 2. 缓存击穿问题:虽然设置了过期时间,仍然会出现缓存击穿问题, 即单个热点key失效的瞬间,持续的大并发请求就会击破缓存,直接请求到数据库,好像蛮力击穿一样(缓存无数据/数据库有数据) 解决方案:设置随机过期时间
jedis.expire(SystemConstants.REDIS_KEY_PREFIX + productId, genRandomExpiredTime(5)); public Integer genRandomExpiredTime(Integer random) { return SystemConstants.REDIS_KEY_EXPIRED_TIME + new Random().nextInt(random) * 60 * 60; }
3. 缓存穿透问题:用户访问的数据既不在缓存当中,也不在数据库中,按道理说数据库都没有这个数据,就不能一直来查数据库了,防止黑客恶意攻击。
解决方案一:缓存空值(null)或默认值 + 过期时间
在数据库查询不存在时,将其缓存为空值(null)或默认值,缓存失效时间一般设置为5分钟之内,当数据库被写入或更新该key的新数据时,缓存必须同时被刷新,避免数据不一致。
@Override public Product getProduct(Long productId) { String redisId = SystemConstants.REDIS_KEY_PREFIX + productId; // 1. 先查redis String productRedis = jedis.get(redisId); if (!StringUtil.isBlank(productRedis)) { // 判断缓存是否是默认值,避免缓存穿透 if (productRedis.equals(SystemConstants.REDIS_DEFAULT_CACHE)) { jedis.expire(redisId, genRandomExpiredTime(3)); return null; } jedis.expire(redisId, genRandomExpiredTime(5)); return gson.fromJson(productRedis, Product.class); } // 2. redis没有,再查mysql数据库 Product productMysql = productRepo.findByProductId(productId); if (productMysql != null) { // 3. 数据库有,则更新redis数据 jedis.set(redisId, gson.toJson(productMysql)); jedis.expire(redisId, genRandomExpiredTime(5)); } else { // 缓存空或默认值 + 过期时间,避免缓存穿透 jedis.set(redisId, SystemConstants.REDIS_DEFAULT_CACHE); jedis.expire(redisId, genRandomExpiredTime(3)); } return productMysql; } 4. 突发性热点缓存重建导致数据库系统压力倍增:也就是说某一数据本来是冷数据,存储在数据库中,突然出现大量访问,redis还没缓存该数据,因此需要大量查询数据库并重建缓存,也就是以下代码重复执行,要是只执行一次就好了。 if (!StringUtil.isBlank(productRedis)) { // 3. 数据库有,则更新redis数据 jedis.set(redisId, gson.toJson(productMysql)); jedis.expire(redisId, genRandomExpiredTime(5)); }
这段代码对之前的实现进行了优化,主要是为了解决缓存穿透和突发性热点带来的问题。
对于缓存穿透问题,即当大量查询数据库中不存在的数据时,例如一些恶意的攻击,会导致缓存失效,直接对数据库进行大量查询,可能会导致数据库挂掉。为了解决这个问题,这段代码对于数据库中没有的数据,会在缓存中设置一个默认值SystemConstants.REDIS_DEFAULT_CACHE,并且设置一个短时间的过期时间(这里是随机的1到3分钟)。这样当再次查询这个不存在的数据时,就不会对数据库进行查询。
对于突发性热点问题,会发生在某一时刻突然对某一特定的数据产生大量访问,此时如果缓存中没有这个数据,所有的请求都会去查询数据库并设置缓存,可能会导致数据库压力增大甚至挂掉。这个问题稍微复杂一些,可以使用一种常见的解决方案,就是对第一个查询这个数据的请求进行阻塞,并让它去数据库中查询数据并设置缓存,其他的请求则等待这个请求设置完缓存后再从缓存中获取数据。这需要用到一些分布式锁的技术,例如Redis的SETNX命令。
这样就可以防止并发时大量的请求都去查询数据库并设置缓存,减轻数据库的压力。
1、解决方案一:DCL双端检锁机制
但仍然存在以下问题,一方面synchronized锁住的是单个JVM,若是该web项目集群部署,则在每个JVM都需要锁一次,另一方面,假如productId=101是热点数据会被锁住,但是其他数据productId=202也需要排队等待,效率降低。
2、解决方案二:分布式锁setnx
但仍然存在redis缓存和mysql数据库数据不一致问题
3、解决方案三:锁优化-读写锁
5. 缓存雪崩:在使用缓存时,通常会对缓存设置过期时间,一方面目的是保持缓存与数据库数据的一致性,另一方面是减少冷缓存占用过多的内存空间。但当缓存中大量热点缓存在某一个时刻同时实效,请求全部转发到数据库,从而导致数据库压力骤增,造成系统崩溃等情况,这就是缓存雪崩。
解决方案:
1. key均匀失效: 将key的过期时间后面加上一个随机数(比如随机1-5分钟),让key均匀的失效。
2. 双key策略: 主key设置过期时间,备key不设置过期时间,当主key失效时,直接返回备key值。
3. 构建缓存高可用集群
来源:
互联网
本文观点不代表源码解析立场,不承担法律责任,文章及观点也不构成任何投资意见。
评论列表