浅谈Redis高并发缓存架构性能优化实战

目录
  • 场景1: 中小型公司Redis缓存架构以及线上问题实战
  • 场景2: 大厂线上大规模商品缓存数据冷热分离实战
  • 场景3: 基于DCL机制解决热点缓存并发重建问题实战
  • 场景4: 突发性热点缓存重建导致系统压力暴增
  • 场景5: 解决大规模缓存击穿导致线上数据库压力暴增
  • 场景6: 黑客工资导致缓存穿透线上数据库宕机
  • 场景7: 大V直播带货导致线上商品系统崩溃原因分析
  • 场景8: Redis分布式锁解决缓存与数据库双写不一致问题实战
  • 场景9: 大促压力暴增导致分布式锁串行争用问题优化
  • 场景10: 利用多级缓存解决Redis线上集群缓存雪崩问题
  • 场景11: 一次微博明显热点事件导致系统崩溃原因分析
  • 场景12: 大厂对热点数据处理方案

场景1: 中小型公司Redis缓存架构以及线上问题实战

线程A在master获取锁之后,master在同步数据到slave时,master突然宕机(此时数据还没有同步到slave),然后slave会自动选举成为新的master,此时线程B获取锁,结果成功了,这样会造成多个线程获取同一把锁

解决方案

  • 网上说RedLock能解决分布式锁失效的问题。对于RedLock实现原理是: 超过半数Redis节点加锁成功之后才能算成功,否则返回false,和Zookeeper的"ZAB"原理很类似,而且与Redis Cluster集群中解决脑裂问题的方案类似,但是RedLock方案有很大的弊端,也就是会造成Redis可用性的延迟,众所周知,Redis的AP(可用性+分区容忍性)机制,假如把Redis变成CP(一致性+分区容忍性),这样肯定会牺牲一定的可用性,与Redis初衷不符合,也就是说还不如使用Zookeeper。
  • Zookeeper具备CP机制以及实现了ZAB,能够确保某一个节点宕机,也能保证数据一致性,而且效率会比Redis高很多,更适合做分布式锁

场景2: 大厂线上大规模商品缓存数据冷热分离实战

问题: 在高并发场景下,一定要把所有的缓存数据一直保存在缓存不让其失效吗?

虽然一直缓存所有数据没什么大问题,但是考虑到如果数据太多,就会一直占用缓存空间(内存资源非常宝贵),并且数据的维护性也是需要耗时的.

解决方案

  • 对缓存数据做冷热分离在查询数据时,我们只需要在查询代码中再次更新过期时间,这样就能保证热点数据一直在缓存中,而不经常访问的数据过期了就自动从缓存中删除。

流程分析

  • 假如一个热点数据每天访问特别高,不停的查询该数据,每次查询时再次更新过期时间,那么在这个过期时间之内只要有人访问就会一直存在缓存中,这样就保证热点商品数据不会因为过期时间而从缓存中移除;
  • 而对于不经常访问的冷门数据到了过期时间就可以自动释放了,同时也释放除了一部分缓存空间,而且当再次访问冷门数据的时候,从数据库拿到的永远是最新的数据,也减少了维护成本。

场景3: 基于DCL机制解决热点缓存并发重建问题实战

DCL(双重检测锁)

问题: 冷门数据突然变成了热门数据,大量的请求突发性的对热点数据进行缓存重建导致系统压力暴增

解决方案

  • 最容易想到的就是加锁
  • DCL机制。先查一次,缓存有数据就直接返回,没有数据,就加锁,在锁的代码块中再次先查询缓存。这样锁的目的就是为了当第一次缓存从数据库查询更新到缓存中,代码块执行完,其他线程再次进来,此时缓存中就已经存在数据了,这样就减少了查询数据库的次数
public Product get(Long productId) {
    Product product = null;
    String productCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE + productId;
    //DCL机制:第一次先从缓存里查数据
    product = getProductFromCache(productCacheKey);
    if (product != null) {
        return product;
    }

    //加分布式锁解决热点缓存并发重建问题
    RLock hotCreateCacheLock = redisson.getLock(LOCK_PRODUCT_HOT_CACHE_CREATE_PREFIX + productId);
    hotCreateCacheLock.lock();
    // 这个优化谨慎使用,防止超时导致的大规模并发重建问题
    // hotCreateCacheLock.tryLock(1, TimeUnit.SECONDS);
    try {
        //DCL机制:在分布式锁里面第二次查询
        product = getProductFromCache(productCacheKey);
        if (product != null) {
            return product;
        }

        //RLock productUpdateLock = redisson.getLock(LOCK_PRODUCT_UPDATE_PREFIX + productId);
        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),
                        genProductCacheTimeout(), TimeUnit.SECONDS);
            } else {
                //设置空缓存解决缓存穿透问题
                redisUtil.set(productCacheKey, EMPTY_CACHE, genEmptyCacheTimeout(), TimeUnit.SECONDS);
            }
        } finally {
            rLock.unlock();
        }
    } finally {
        hotCreateCacheLock.unlock();
    }

    return product;
}

场景4: 突发性热点缓存重建导致系统压力暴增

问题: 假如当前有10w个线程没有拿到锁正在排队,这种情况只能等到获取锁的线程执行完代码释放锁后,那排队的10w个线程才能再次竞争锁。这里需要关注的问题点就是又要再次竞争锁,意味着线程竞争锁的次数可能最少>1,频繁的竞争锁对Redis性能也是有消耗的,有没有更好的办法让每个线程竞争锁的次数尽可能减少呢?

解决方案

  • 可以通过tryLock(time,TimeUnit)先让所有线程尝试获取锁
  • 假如获取锁的线程执行数据库查询然后将数据更新到缓存所需要的时间为1s,那么当其他线程获取锁时间结束后,会解除阻塞状态直接往下执行,然后再次查询缓存的时候发现缓存有数据了就直接返回。
  • 这样设计的好处就是把分布式锁在某些特定的场景使其"串行变并发",不过这个优化需要谨慎使用,防止超时导致的大规模并发重建问题。毕竟没有任何方案是完全解决问题的,主要是根据公司业务而定.

场景5: 解决大规模缓存击穿导致线上数据库压力暴增

缓存击穿/缓存失效: 可能同一时间热点数据全部过期而造成缓存查不到数据,请求就会从数据库查询,高并发情况下会导致数据库压力

解决方案

  • 对于这个场景,可以给数据设置过期时间时,不要将所有缓存数据的过期时间设置为相同的过期时间,最好可以给每个数据的过期时间设置一个随机数,保证数据在不同的时间段过期。

代码案例

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

场景6: 黑客工资导致缓存穿透线上数据库宕机

缓存穿透: 如果黑客通过脚本文件不停的传一些不存在的参数刷网站的接口,而这种垃圾参数在缓存和数据库又不存在,这样就会一直地查数据库,最终可能导致数据库并发量过大而卡死宕机。

解决方案

  • 网关限流。Nginx、Sentinel、Hystrix都可以实现
  • 代码层面。可以使用多级缓存,比如一级缓存采用布隆过滤器,二级缓存可以使用guava中的Cache,三级缓存使用Redis,为什么一级缓存使用布隆过滤器呢,其结构和bitmap类似,用于存储数据状态,能存大量的key

布隆过滤器

  • 布隆过滤器就是一个大型的位数组和几个不一样的无偏Hash函数.当布隆过滤器说某个值存在时,这个值可能不存在,当说不存在时,那就肯定不存在。

场景7: 大V直播带货导致线上商品系统崩溃原因分析

问题: 这种场景可能是在某个时刻把冷门商品一下子变成了热门商品。因为冷门的数据可能在缓存时间过期就删除,而此时刚好有大量请求,比如直播期间推送一个商品连接,假如同时有几十万人抢购,而缓存没有的话,意味着所有的请求全部达到了数据库中查询,而对于数据库单节点支撑并发量也就不到1w,此时这么大的请求量,肯定会把数据库整宕机(这种场景比较少,但是小概率还是会有)

解决方案

  • 可以通过tryLock(time,TimeUnit)先让所有线程尝试获取锁
  • 假如获取锁的线程执行数据库查询然后将数据更新到缓存所需要的时间为1s,那么当其他线程获取锁时间结束后,会解除阻塞状态直接往下执行,然后再次查询缓存的时候发现缓存有数据了就直接返回。
  • 这样设计的好处就是把分布式锁在某些特定的场景使其"串行变并发",不过这个优化需要谨慎使用,防止超时导致的大规模并发重建问题。毕竟没有任何方案是完全解决问题的,主要是根据公司业务而定.

场景8: Redis分布式锁解决缓存与数据库双写不一致问题实战

解决方案

  • 重入锁保证并发安全。通常说在分布式锁中再加一把锁,锁太重,性能不是很好,还有优化空间
  • 分布式读写锁(ReadWriteLock),实现机制和ReentranReadWriteLock一直,适合读多写少的场景,注意读写锁的key得一致
  • 使用canal通过监听binlog日志及时去修改缓存,但是引入中间件,增加系统的维护度

Lua脚本设置读写锁

local mode = redis.call('hget', KEYS[1], 'mode');
if (mode == false)
then redis.call('hset', KEYS[1], 'mode', 'read');
redis.call('hset', KEYS[1], ARGV[2], 1);
redis.call('set', KEYS[2] .. ':1', 1);
redis.call('pexpire', KEYS[2] .. ':1', ARGV[1]);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
if (mode == 'read') or (mode == 'write' and redis.call('hexists', KEYS[1], ARGV[3]) == 1)
then local ind = redis.call('hincrby', KEYS[1], ARGV[2], 1);
local key = KEYS[2] .. ':' .. ind;
redis.call('set', key, 1);
redis.call('pexpire', key, ARGV[1]); redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
return redis.call('pttl', KEYS[1]);

ReadWriteLock代码案例

@Transactional
public Product update(Product product) {
  Product productResult = null;
  //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 {
          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();
    // 这个优化谨慎使用,防止超时导致的大规模并发重建问题
    // hotCreateCacheLock.tryLock(1, TimeUnit.SECONDS);
    try {
        product = getProductFromCache(productCacheKey);
        if (product != null) {
            return product;
        }

        //RLock productUpdateLock = redisson.getLock(LOCK_PRODUCT_UPDATE_PREFIX + productId);
        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),
                        genProductCacheTimeout(), TimeUnit.SECONDS);
            } else {
                //设置空缓存解决缓存穿透问题
                redisUtil.set(productCacheKey, EMPTY_CACHE, genEmptyCacheTimeout(), TimeUnit.SECONDS);
            }
        } finally {
            rLock.unlock();
        }
    } finally {
        hotCreateCacheLock.unlock();
    }

    return product;
}

场景9: 大促压力暴增导致分布式锁串行争用问题优化

解决方案

  • 可以采用分段锁,和JDK7的ConcurrentHashMap的实现原理很类似,将一个锁,分成多个锁,比如lock,分成lock_1、lock_2...
  • 然后将库存平均分摊到每把锁,这样做的目的是分摊分布式锁的压力,本来只有一个锁,意味着所有的线程进来只能一个线程获取到锁,如果分摊为10把锁,那么同一时间可以有10个线程同时获取到锁对同一个商品进行操作,也就意味着在同等环境下,分段锁的效率比只用一个锁要高得多

场景10: 利用多级缓存解决Redis线上集群缓存雪崩问题

缓存雪崩: 缓存支撑不住或者宕机,然后大量请求涌入数据库。

解决方案

  • 网关限流。Nginx、Sentinel、Hystrix都可以实现
  • 代码层面。可以使用多级缓存,比如一级缓存采用布隆过滤器,二级缓存可以使用guava中的Cache,三级缓存使用Redis,为什么一级缓存使用布隆过滤器呢,其结构和bitmap类似,用于存储数据状态,能存大量的key

场景11: 一次微博明显热点事件导致系统崩溃原因分析

问题: 比如微博上某一天某个明星事件成为了热点新闻,此时很多吃瓜群众全部涌入这个热点,如果并发每秒达到几十万甚至上百万的并发量,但是Redis服务器单节点只能支撑并发10w而已,那么可能因为这么高的并发量导致很多请求卡死在那,要知道我们其他业务服务也会用到Redis,一旦Redis卡死,就会影响到其他业务,导致整个业务瘫痪,这就是典型的缓存雪崩问题

解决方案: 参考场景10

场景12: 大厂对热点数据处理方案

解决方案

  • 如果按照场景10的方案去实现,需要考虑数据一致性问题,这样就不得不每次对数据进行增加、删除、更新都要立马通知其他节点更新数据,能做到及时更新数据的方案可能就是:Redis发布/订阅、MQ等
  • 虽然说这些方案实现也可以,但是不可避免的我们需要再维护相关的中间件,提高了维护成本
  • 目前大厂对于热点数据专门会有一个类似于热点缓存系统来维护,所有的web应用只需要监听这个系统,只要有热点时,直接更新缓存,这样既能减少代码耦合,还能更好的维护热点数据。
  • 那么热点数据来源怎么获取呢?可以在设计查询的接口使用类似于Spring AOP的方式,每次查询就把数据传送到热点数据,一般大厂都会有数据分析岗位,根据热点规则将数据分类

到此这篇关于浅谈Redis高并发缓存架构性能优化实战的文章就介绍到这了,更多相关Redis高并发缓存内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Spring Boot实战解决高并发数据入库之 Redis 缓存+MySQL 批量入库问题

    目录 前言 架构设计 代码实现 测试 总结 前言 最近在做阅读类的业务,需要记录用户的PV,UV: 项目状况:前期尝试业务阶段: 特点: 快速实现(不需要做太重,满足初期推广运营即可)快速投入市场去运营 收集用户的原始数据,三要素: 谁在什么时间阅读哪篇文章 提到PV,UV脑海中首先浮现特点: 需要考虑性能(每个客户每打开一篇文章进行记录)允许数据有较小误差(少部分数据丢失) 架构设计 架构图: 时序图 记录基础数据MySQL表结构 CREATE TABLE `zh_article_count`

  • 浅谈Redis高并发缓存架构性能优化实战

    目录 场景1: 中小型公司Redis缓存架构以及线上问题实战 场景2: 大厂线上大规模商品缓存数据冷热分离实战 场景3: 基于DCL机制解决热点缓存并发重建问题实战 场景4: 突发性热点缓存重建导致系统压力暴增 场景5: 解决大规模缓存击穿导致线上数据库压力暴增 场景6: 黑客工资导致缓存穿透线上数据库宕机 场景7: 大V直播带货导致线上商品系统崩溃原因分析 场景8: Redis分布式锁解决缓存与数据库双写不一致问题实战 场景9: 大促压力暴增导致分布式锁串行争用问题优化 场景10: 利用多级缓

  • 浅谈C++高并发场景下读多写少的优化方案

    目录 概述 分析 双缓冲 工程实现上需要攻克的难点 核心代码实现 简单说说golang中双缓冲的实现 相关文献 概述 一谈到高并发的优化方案,往往能想到模块水平拆分.数据库读写分离.分库分表,加缓存.加mq等,这些都是从系统架构上解决.单模块作为系统的组成单元,其性能好坏也能很大的影响整体性能,本文从单模块下读多写少的场景出发,探讨其解决方案,以其更好的实现高并发.不同的业务场景,读和写的频率各有侧重,有两种常见的业务场景: 读多写少:典型场景如广告检索端.白名单更新维护.loadbalance

  • Redis高并发防止秒杀超卖实战源码解决方案

    目录 1:解决思路 2:添加 redis 常量 3:添加 redis 配置类 4:修改业务层 1:秒杀业务逻辑层 2:添加需要抢购的代金券 3:抢购代金券 5:postman 测试 6:压力测试 8:配置Lua 9:修改业务层 1:抢购代金券 10:压力测试 1:解决思路 将活动写入 redis 中,通过 redis 自减指令扣除库存. 2:添加 redis 常量 commons/constant/RedisKeyConstant.java seckill_vouchers("seckill_v

  • 浅谈Java高并发解决方案以及高负载优化方法

    目录 1.HTML静态化 2.图片服务器分离 3.数据库集群和库表散列 4.缓存 5.镜像 6.负载均衡 1)硬件四层交换 2)软件四层交换 一.高并发高负载类网站关注点之数据库 需要注意的是: 二.高并发高负载网站的系统架构之HTML静态化 网站HTML静态化解决方案 : 三.高并发高负载类网站关注点之缓存.负载均衡.存储 负载均衡/加速 存储 四.高并发高负载网站的系统架构之图片服务器分离 利用Apache实现图片服务器的分离,缘由: 环境介绍: 步骤: 五.高并发高负载网站的系统架构之数据

  • 浅谈Nginx10m+高并发内核优化详解

    何为高并发 默认的Linux内核参数考虑的是最通用场景,不符合用于支持高并发访问的Web服务器,所以需要修改Linux内核参数,这样可以让Nginx拥有更高的性能: 在优化内核时,可以做的事情很多,不过,我们通常会根据业务特点来进行调整,当Nginx作为静态web内容服务器.反向代理或者提供压缩服务器的服务器时,期内核参数的调整都是不同的,这里针对最通用的.使Nginx支持更多并发请求的TCP网络参数做简单的配置: 这些需要修改/etc/sysctl.conf来更改内核参数. 配置方法 配置详析

  • 浅谈Redis在微服务架构中的几种应用场景

    本文介绍在SpringCloud中使用Redis作为Pub/Sub异步通信.缓存或主数据库和配置服务器的三种场景应用. Redis可以广泛用于微服务架构.它可能是您应用程序以多种不同方式利用的少数流行软件解决方案之一.根据要求,它可以充当主数据库,缓存或消息代理.虽然它也是一个键/值存储,但我们可以将它用作微服务体系结构中的配置服务器或发现服务器.虽然它通常被定义为内存中的数据结构,但我们也可以在持久模式下运行它. 这里我将向您展示一些使用Redis与Spring Boot和Spring Clo

  • 浅谈Redis缓存击穿、缓存穿透、缓存雪崩的解决方案

    目录 前言 Redis缓存使用场景 Redis缓存穿透 解决方案 1.对空值缓存 2.添加参数校验 3.采用布隆过滤器 Redis缓存雪崩 解决方案 1.大量热点数据同时失效带来的缓存雪崩问题 2. 服务降级 3. Redis 缓存实例发生故障宕机带来的缓存雪崩问题 Redis缓存击穿 解决方案 1. 热key不过期 2. 分布式锁 总结 缓存击穿 缓存穿透 缓存雪崩 前言 在日常的项目中,缓存的使用场景是比较多的.缓存是分布式系统中的重要组件,主要解决在高并发.大数据场景下,热点数据访问的性能

  • 浅谈redis缓存在项目中的使用

    背景 Redis 是一个开源的内存数据结构存储系统. 可以作为数据库.缓存和消息中间件使用. 支持多种类型的数据结构. Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence). 通过 Redis 哨兵(Sentinel)和 Redis 集群(Cluster)的自动分区,提供高可用性(high availability). 基本数

  • 浅谈Redis 缓存的三大问题及其解决方案

    目录 一.缓存穿透 1. 常见解决方案 2. 布隆过滤器 3. 缓存空数据与布隆过滤器的比较 二.缓存击穿 解决方案 三.缓存雪崩 解决方案 Redis 经常用于系统中的缓存,这样可以解决目前 IO 设备无法满足互联网应用海量的读写请求的问题. 一.缓存穿透 缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起 id 为-1 的数据或者特别大的不存在的数据.有可能是黑客利用漏洞攻击从而去压垮应用的数据库. 1. 常见解决方案 对于缓存穿透问题,常见的解决方案有以下三种: 验证拦截:

  • 浅谈Redis缓存有哪些淘汰策略

    目录 Redis过期策略 定时删除 惰性删除 定期删除 Redis的内存淘汰机制 LRU和LFU的区别 LRU LFU Redis重启如何恢复数据呢? 总结 Redis过期策略 我们首先来了解一下Redis的内存淘汰机制. 定时删除 概述     redis默认是每隔 100ms 就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除.注意这里是随机抽取的.为什么要随机呢?你想一想假如 redis 存了几十万个 key ,每隔100ms就遍历所有的设置过期时间的 key 的话,就会

随机推荐