高并发技巧之Redis和本地缓存使用技巧分享

目录
  • 三种缓存的使用场景
  • Redis的使用场景和局限性
  • LoadingCache的使用场景和局限性
  • ReloadableCache的使用场景和局限性
  • 小结
  • 小技巧
  • 缓存使用的简单介绍
    • LoadingCache的使用
    • reloadableCache的使用
  • 老生常谈的缓存击穿/穿透/雪崩问题
    • 缓存击穿
    • 缓存穿透
    • 缓存雪崩

众所周知,缓存最主要的目的就是加速访问,缓解数据库压力。最常用的缓存就是分布式缓存,比如redis,在面对大部分并发场景或者一些中小型公司流量没有那么高的情况,使用redis基本都能解决了。但是在流量较高的情况下可能得使用到本地缓存了,比如guava的LoadingCache和快手开源的ReloadableCache。

三种缓存的使用场景

这部分会介绍redis,比如guava的LoadingCache和快手开源的ReloadableCache的使用场景和局限,通过这一部分的介绍就能知道在怎样的业务场景下应该使用哪种缓存,以及为什么。

Redis的使用场景和局限性

如果宽泛的说redis何时使用,那么自然就是用户访问量过高的地方使用,从而加速访问,并且缓解数据库压力。如果细分的话,还得分为单节点问题和非单节点问题。

如果一个页面用户访问量比较高,但是访问的不是同一个资源。比如用户详情页,访问量比较高,但是每个用户的数据都是不一样的,这种情况显然只能用分布式缓存了,如果使用redis,key为用户唯一键,value则是用户信息。

redis导致的缓存击穿

但是需要注意一点,一定要设置过期时间,而且不能设置到同一时间点过期。举个例子,比如用户又个活动页,活动页能看到用户活动期间获奖数据,粗心的人可能会设置用户数据的过期时间点为活动结束,这样会

单(热)点问题

单节点问题说的是redis的单个节点的并发问题,因为对于相同的key会落到redis集群的同一个节点上,那么如果对这个key的访问量过高,那么这个redis节点就存在并发隐患,这个key就称为热key。

如果所有用户访问的都是同一个资源,比如小爱同学app首页对所有用户展示的内容都一样(初期),服务端给h5返回的是同一个大json,显然得使用到缓存。首先我们考虑下用redis是否可行,由于redis存在单点问题,如果流量过大的话,那么所有用户的请求到达redis的同一个节点,需要评估该节点能否抗住这么大流量。我们的规则是,如果单节点qps达到了千级别就要解决单点问题了(即使redis号称能抗住十万级别的qps),最常见的做法就是使用本地缓存。显然小爱app首页流量不过百,使用redis是没问题的。

LoadingCache的使用场景和局限性

对于这上面说的热key问题,我们最直接的做法就是使用本地缓存,比如你最熟悉的guava的LoadingCache,但是使用本地缓存要求能够接受一定的脏数据,因为如果你更新了首页,本地缓存是不会更新的,它只会根据一定的过期策略来重新加载缓存,不过在我们这个场景是完全没问题的,因为一旦在后台推送了首页后就不会再去改变了。即使改变了也没问题,可以设置写过期为半小时,超过半小时重新加载缓存,这种短时间内的脏数据我们是可以接受的。

LoadingCache导致的缓存击穿

虽然说本地缓存和机器上强相关的,虽然代码层面写的是半小时过期,但由于每台机器的启动时间不同,导致缓存的加载时间不同,过期时间也就不同,也就不会所有机器上的请求在同一时间缓存失效后都去请求数据库。但是对于单一一台机器也是会导致缓存穿透的,假如有10台机器,每台1000的qps,只要有一台缓存过期就可能导致这1000个请求同时打到了数据库。这种问题其实比较好解决,但是容易被忽略,也就是在设置LoadingCache的时候使用LoadingCache的load-miss方法,而不是直接判断cache.getIfPresent()== null然后去请求db;前者会加虚拟机层面的锁,保证只有一个请求打到数据库去,从而完美的解决了这个问题。

但是,如果对于实时性要求较高的情况,比如有段时间要经常做活动,我要保证活动页面能近实时更新,也就是运营在后台配置好了活动信息后,需要在C端近实时展示这次配置的活动信息,此时使用LoadingCache肯定就不能满足了。

ReloadableCache的使用场景和局限性

对于上面说的LoadingCache不能解决的实时问题,可以考虑使用ReloadableCache,这是快手开源的一个本地缓存框架,最大的特点是支持多机器同时更新缓存,假设我们修改了首页信息,然后请求打到的是A机器,这个时候重新加载ReloadableCache,然后它会发出通知,监听了同一zk节点的其他机器收到通知后重新更新缓存。使用这个缓存一般的要求是将全量数据加载到本地缓存,所以如果数据量过大肯定会对gc造成压力,这种情况就不能使用了。由于小爱同学首页这个首页是带有状态的,一般online状态的就那么两个,所以完全可以使用ReloadableCache来只装载online状态的首页。

小结

到这里三种缓存基本都介绍完了,做个小结:

  • 对于非热点的数据访问,比如用户维度的数据,直接使用redis即可;
  • 对于热点数据的访问,如果流量不是很高,无脑使用redis即可;
  • 对于热点数据,如果允许一定时间内的脏数据,使用LoadingCache即可;
  • 对于热点数据,如果一致性要求较高,同时数据量不大的情况,使用ReloadableCache即可;

小技巧

不管哪种本地缓存虽然都带有虚拟机层面的加锁来解决击穿问题,但是意外总有可能以你意想不到的方式发生,保险起见你可以使用两级缓存的方式即本地缓存+redis+db。

缓存使用的简单介绍

这里redis的使用就不再多说了,相信很多人对api的使用比我还熟悉

LoadingCache的使用

这个是guava提供的网上一抓一大把,但是给两点注意事项

  • 要使用load-miss的话, 要么使用V get(K key, Callable<? extends V> loader);要么使用build的时候使用的是build(CacheLoader<? super K1, V1> loader)这个时候可以直接使用get()了。此外建议使用load-miss,而不是getIfPresent==null的时候再去查数据库,这可能导致缓存击穿;
  • 使用load-miss是因为这是线程安全的,如果缓存失效的话,多个线程调用get的时候只会有一个线程去db查询,其他线程需要等待,也就是说这是线程安全的。
LoadingCache<String, String> cache = CacheBuilder.newBuilder()
                .maximumSize(1000L)
                .expireAfterAccess(Duration.ofHours(1L)) // 多久不访问就过期
                .expireAfterWrite(Duration.ofHours(1L))  // 多久这个key没修改就过期
                .build(new CacheLoader<String, String>() {
                    @Override
                    public String load(String key) throws Exception {
                        // 数据装载方式,一般就是loadDB
                        return key + " world";
                    }
                });
String value = cache.get("hello"); // 返回hello world

reloadableCache的使用

导入三方依赖

<dependency>
  <groupId>com.github.phantomthief</groupId>
  <artifactId>zknotify-cache</artifactId>
  <version>0.1.22</version>
</dependency>

需要看文档,不然无法使用,有兴趣自己写一个也行的。

public interface ReloadableCache<T> extends Supplier<T> {

    /**
     * 获取缓存数据
     */
    @Override
    T get();

    /**
     * 通知全局缓存更新
     * 注意:如果本地缓存没有初始化,本方法并不会初始化本地缓存并重新加载
     *
     * 如果需要初始化本地缓存,请先调用 {@link ReloadableCache#get()}
     */
    void reload();

    /**
     * 更新本地缓存的本地副本
     * 注意:如果本地缓存没有初始化,本方法并不会初始化并刷新本地的缓存
     *
     * 如果需要初始化本地缓存,请先调用 {@link ReloadableCache#get()}
     */
    void reloadLocal();
}

老生常谈的缓存击穿/穿透/雪崩问题

这三个真的是亘古不变的问题,如果流量大确实需要考虑。

缓存击穿

简单说就是缓存失效,导致大量请求同一时间打到了数据库。对于缓存击穿问题上面已经给出了很多解决方案了。

  • 比如使用本地缓存
  • 本地缓存使用load-miss方法
  • 使用第三方服务来加载缓存

1.2和都说过,主要来看3。假如业务愿意只能使用redis而无法使用本地缓存,比如数据量过大,实时性要求比较高。那么当缓存失效的时候就得想办法保证只有少量的请求打到数据库。很自然的就想到了使用分布式锁,理论上说是可行的,但实际上存在隐患。我们的分布式锁相信很多人都是使用redis+lua的方式实现的,并且在while中进行了轮训,这样请求量大,数据多的话会导致无形中让redis成了隐患,并且占了太多业务线程,其实仅仅是引入了分布式锁就加大了复杂度,我们的原则就是能不用就不用。

那么我们是不是可以设计一个类似分布式锁,但是更可靠的rpc服务呢?当调用get方法的时候这个rpc服务保证相同的key打到同一个节点,并且使用synchronized来进行加锁,之后完成数据的加载。在快手提供了一个叫cacheSetter的框架。下面提供一个简易版,自己写也很容易实现。

import com.google.common.collect.Lists;
import org.apache.commons.collections4.CollectionUtils;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CountDownLatch;

/**
 * @Description 分布式加载缓存的rpc服务,如果部署了多台机器那么调用端最好使用id做一致性hash保证相同id的请求打到同一台机器。
 **/
public abstract class AbstractCacheSetterService implements CacheSetterService {

    private final ConcurrentMap<String, CountDownLatch> loadCache = new ConcurrentHashMap<>();

    private final Object lock = new Object();

    @Override
    public void load(Collection<String> needLoadIds) {
        if (CollectionUtils.isEmpty(needLoadIds)) {
            return;
        }
        CountDownLatch latch;
        Collection<CountDownLatch> loadingLatchList;
        synchronized (lock) {
            loadingLatchList = excludeLoadingIds(needLoadIds);

            needLoadIds = Collections.unmodifiableCollection(needLoadIds);

            latch = saveLatch(needLoadIds);
        }
        System.out.println("needLoadIds:" + needLoadIds);
        try {
            if (CollectionUtils.isNotEmpty(needLoadIds)) {
                loadCache(needLoadIds);
            }
        } finally {
            release(needLoadIds, latch);
            block(loadingLatchList);
        }

    }

    /**
     * 加锁
     * @param loadingLatchList 需要加锁的id对应的CountDownLatch
     */
    protected void block(Collection<CountDownLatch> loadingLatchList) {
        if (CollectionUtils.isEmpty(loadingLatchList)) {
            return;
        }
        System.out.println("block:" + loadingLatchList);
        loadingLatchList.forEach(l -> {
            try {
                l.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
    }

    /**
     * 释放锁
     * @param needLoadIds 需要释放锁的id集合
     * @param latch 通过该CountDownLatch来释放锁
     */
    private void release(Collection<String> needLoadIds, CountDownLatch latch) {
        if (CollectionUtils.isEmpty(needLoadIds)) {
            return;
        }
        synchronized (lock) {
            needLoadIds.forEach(id -> loadCache.remove(id));
        }
        if (latch != null) {
            latch.countDown();
        }
    }

    /**
     * 加载缓存,比如根据id从db查询数据,然后设置到redis中
     * @param needLoadIds 加载缓存的id集合
     */
    protected abstract void loadCache(Collection<String> needLoadIds);

    /**
     * 对需要加载缓存的id绑定CountDownLatch,后续相同的id请求来了从map中找到CountDownLatch,并且await,直到该线程加载完了缓存
     * @param needLoadIds 能够正在去加载缓存的id集合
     * @return 公用的CountDownLatch
     */
    protected CountDownLatch saveLatch(Collection<String> needLoadIds) {
        if (CollectionUtils.isEmpty(needLoadIds)) {
            return null;
        }
        CountDownLatch latch = new CountDownLatch(1);
        needLoadIds.forEach(loadId -> loadCache.put(loadId, latch));
        System.out.println("loadCache:" + loadCache);
        return latch;
    }

    /**
     * 哪些id正在加载数据,此时持有相同id的线程需要等待
     * @param ids 需要加载缓存的id集合
     * @return 正在加载的id所对应的CountDownLatch集合
     */
    private Collection<CountDownLatch> excludeLoadingIds(Collection<String> ids) {
        List<CountDownLatch> loadingLatchList = Lists.newArrayList();
        Iterator<String> iterator = ids.iterator();
        while (iterator.hasNext()) {
            String id = iterator.next();
            CountDownLatch latch = loadCache.get(id);
            if (latch != null) {
                loadingLatchList.add(latch);
                iterator.remove();
            }
        }
        System.out.println("loadingLatchList:" + loadingLatchList);
        return loadingLatchList;
    }
}

业务实现

import java.util.Collection;
public class BizCacheSetterRpcService extends AbstractCacheSetterService {
    @Override
    protected void loadCache(Collection<String> needLoadIds) {
        // 读取db进行处理
   	// 设置缓存
    }
}

缓存穿透

简单来说就是请求的数据在数据库不存在,导致无效请求打穿数据库。

解法也很简单,从db获取数据的方法(getByKey(K key))一定要给个默认值。

比如我有个奖池,金额上限是1W,用户完成任务的时候给他发笔钱,并且使用redis记录下来,并且落表,用户在任务页面能实时看到奖池剩余金额,在任务开始的时候显然奖池金额是不变的,redis和db里面都没有发放金额的记录,这就导致每次必然都去查db,对于这种情况,从db没查出来数据应该缓存个值0到缓存。

缓存雪崩

就是大量缓存集中失效打到了db,当然肯定都是一类的业务缓存,归根到底是代码写的有问题。可以将缓存失效的过期时间打散,别让其集中失效就可以了。

以上就是高并发技巧之Redis和本地缓存使用技巧分享的详细内容,更多关于Redis本地缓存的资料请关注我们其它相关文章!

(0)

相关推荐

  • Redis高并发情况下并发扣减库存项目实战

    目录 第一种方案:纯MySQL扣减实现 MySQL架构升级 第二种方案:缓存实现扣减 第三种方案:数据库+缓存 顺序写的性能更好 顺序写的架构 扣减流程 相信大家从网上学习项目大部分人第一个项目都是电商,生活中时时刻刻也会用到电商APP,例如淘宝,京东等.做技术的人都知道,电商的业务逻辑简单,但是大部分电商都会涉及到高并发高可用,对并发和对数据的处理要求是很高的.这里我今天就讲一下高并发情况下是如何扣减库存的? 我们对扣减库存所需要关注的技术点如下: 当前剩余的数量大于等于当前需要扣减的数量,不

  • 使用Redis解决高并发方案及思路解读

    目录 NoSQL Redis 痛点 思路 分布式锁 锁续命 扩展 结语 NoSQL Not Only SQL的简称.NoSQL是解决传统的RDBMS在应对某些问题时比较乏力而提出的. 即非关系型数据库,它们不保证关系数据的ACID特性,数据之间一般没有关联,在扩展上就非常容易实现,并且拥有较高的性能. Redis redis是nosql的典型代表,也是目前互联网公司的必用技术. redis是键值(Key-Value)存储数据库,主要会使用到哈希表.大多数时候是直接以缓存的形式被使用,使得请求不直

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

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

  • Redis高并发场景下秒杀超卖解决方案(秒杀场景)

    目录 1 什么是秒杀 2 为什么要防止超卖 3 单体架构常规秒杀 3.1 常规减库存代码 3.2 模拟高并发 3.3 超卖现象 3.4 分析原因 4 简单实现悲观乐观锁解决单体架构超卖 4.1 悲观锁 4.2 乐观锁 4.3 redis锁setnx 4.4 使用Redision 5 分布式锁的解决方案 6 采用缓存队列防止超卖 1 什么是秒杀 秒杀最直观的定义:在高并发场景下而下单某一个商品,这个过程就叫秒杀 [秒杀场景] 火车票抢票 双十一限购商品 热度高的明星演唱会门票 … 2 为什么要防止

  • Redis处理高并发之布隆过滤器详解

    目录 前言 缓存穿透.击穿.雪崩 缓存穿透 出现情况 常见的解决方案 缓存击穿 出现情况 解决方案 缓存雪崩 解决方案 布隆过滤器 Bloom filter 总结 前言 随着我们业务开发越来越来大,并染请求就会越来越多,那么我们的项目的压力就会越来越大,基本都会使用缓存,除本地缓存,还会用到redis缓存,但是你以为使用缓存就没啥问题了么,那肯定不是的,使用了缓存又会出现新的问题,比如,缓存的key失效导致大量的请求到数据库,大量的读请求瞬间到达了数据库,cpu的使用率爆增,导致数据库都可能挂掉

  • 高并发下Redis如何保持数据一致性(避免读后写)

    “读后写” 通常意义上我们说读后写是指针对同一个数据的先读后写,且写入的值依赖于读取的值. 关于这个定义要拆成两部分来看,一:同一个数据:二:写依赖于读.(记住这个拆分,后续会用到,记为定义一.定义二)只有当这两部分都成立时,读后写的问题才会出现. 在项目中,当面对较多的并发时,使用redis进行读后写操作,是非常容易出问题的,常常使得程序不具备鲁棒性,bug很难稳定复现(得到的值往往跟并发数有关). 举个栗子: 存在A.B两个进程,同时操作下面这段代码: $objRedis = new Red

  • Redis与本地缓存的结合实现

    目录 前言 设计示例 Redis懒加载缓存 流程图 代码示例 优点 缺点 总结 Redis结合本地缓存 流程图 代码示例 优点 缺点 总结 后记 前言 我们开发中经常用到Redis作为缓存,将高频数据放在Redis中能够提高业务性能,降低MySQL等关系型数据库压力,甚至一些系统使用Redis进行数据持久化,Redis松散的文档结构非常适合业务系统开发,在精确查询,数据统计业务有着很大的优势.但是高频数据流处理系统中,Redis的压力也会很大,同时I/0开销才是耗时的主要原因,这时候为了降低Re

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

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

  • Python+redis通过限流保护高并发系统

    保护高并发系统的三大利器:缓存.降级和限流.那什么是限流呢?用我没读过太多书的话来讲,限流就是限制流量.我们都知道服务器的处理能力是有上限的,如果超过了上限继续放任请求进来的话,可能会发生不可控的后果.而通过限流,在请求数量超出阈值的时候就排队等待甚至拒绝服务,就可以使系统在扛不住过高并发的情况下做到有损服务而不是不服务. 举个例子,如各地都出现口罩紧缺的情况,广州政府为了缓解市民买不到口罩的状况,上线了预约服务,只有预约到的市民才能到指定的药店购买少量口罩.这就是生活中限流的情况,说这个也是希

  • Java LocalCache 本地缓存的实现实例

    源码地址: GitHub 使用场景 在Java应用中,对于访问频率高,更新少的数据,通常的方案是将这类数据加入缓存中.相对从数据库中读取来说,读缓存效率会有很大提升. 在集群环境下,常用的分布式缓存有Redis.Memcached等.但在某些业务场景上,可能不需要去搭建一套复杂的分布式缓存系统,在单机环境下,通常是会希望使用内部的缓存(LocalCache). 实现 这里提供了两种LocalCache的实现,一种是基于ConcurrentHashMap实现基本本地缓存,另外一种是基于Linked

  • Java本地缓存的实现代码

    使用场景 在 Java 应用中,对于访问频率高,更新少的数据,通常的方案是将这类数据加入缓存中.相对从数据库中读取来说,读缓存效率会有很大提升. 在集群环境下,常用的分布式缓存有 Redis . Memcached 等.但在某些业务场景上,可能不需要去搭建一套复杂的分布式缓存系统,在单机环境下,通常是会希望使用内部的缓存( LocalCache ). 实现 这里提供了两种 LocalCache 的实现,一种是基于 ConcurrentHashMap 实现基本本地缓存,另外一种是基于 Linked

  • 高并发系统的限流详解及实现

    在开发高并发系统时有三把利器用来保护系统:缓存.降级和限流.本文结合作者的一些经验介绍限流的相关概念.算法和常规的实现方式. 缓存 缓存比较好理解,在大型高并发系统中,如果没有缓存数据库将分分钟被爆,系统也会瞬间瘫痪.使用缓存不单单能够提升系统访问速度.提高并发访问量,也是保护数据库.保护系统的有效方式.大型网站一般主要是"读",缓存的使用很容易被想到.在大型"写"系统中,缓存也常常扮演者非常重要的角色.比如累积一些数据批量写入,内存里面的缓存队列(生产消费),以及

  • redis实现多级缓存同步方案详解

    目录 前言 多级缓存数据同步 如何使用redis6客户端缓存 总结 前言 前阵子参加业务部门的技术方案评审,故事的背景是这样:业务部门上线一个专为公司高管使用的系统.这个系统技术架构形如下图 按理来说这个系统因为受众很小,可以说基本上没并发,业务也没很复杂,但就是这么一个系统,连续2次出现数据库宕机,而导致系统无法正常运行.因为这几次事故,业务部门负责人组织这次技术方案评审,主题如何避免再次出现类似这种故障? 当时有个比较资深的技术,他提出当数据库出现宕机时,可以切换到redis,redis里面

  • jedispool连redis高并发卡死的问题

    java端在使用jedispool 连接redis的时候,在高并发的时候经常死锁,或报连接异常,JedisConnectionException,或者getResource 异常等各种问题 在使用jedispool 的时候一定要注意两点 1. 在获取 jedisPool和jedis的时候加上线程同步,保证不要创建过多的jedispool 和 jedis 2. 用完Jedis实例后需要返还给JedisPool 整理了一下redis工具类,通过大量测试和高并发测试的 package com.casp

  • 如何利用Redis锁解决高并发问题详解

    redis技术的使用: redis真的是一个很好的技术,它可以很好的在一定程度上解决网站一瞬间的并发量,例如商品抢购秒杀等活动... redis之所以能解决高并发的原因是它可以直接访问内存,而以往我们用的是数据库(硬盘),提高了访问效率,解决了数据库服务器压力. 为什么redis的地位越来越高,我们为何不选择memcache,这是因为memcache只能存储字符串,而redis存储类型很丰富(例如有字符串.LIST.SET等),memcache每个值最大只能存储1M,存储资源非常有限,十分消耗内

随机推荐