本地缓存在商品中心的应用

本文探讨了在商品中心背景下的本地缓存应用,选择了Caffeine作为本地缓存方案,以降低对Redis的压力。文章详细分析了Caffeine的特性,包括其缓存策略和数据一致性问题,以及在商品库存中的具体应用和未来展望,旨在解决高并发下的库存数据一致性挑战。

目录

 

一、背景

二、目标

三、方案选型:本地缓存

1、方案比较

1.1 数据库

1.2 本地缓存

2、最终方案:本地缓存

四、本地缓存

1、缓存的特征

1.1 命中率

1.2 最大空间

1.3 清空策略  

2、本地缓存方案比较

3、Caffeine 

3.1 简介

3.2 特点

3.3 Caffeine原理简介

五、本地缓存在商品中的应用[本地缓存+广播消息]

一、分析

二、存在的问题

1、本地缓存数据一致性问题

2、本地缓存大小问题[可暂不解决]

3、广播消息性能问题[可暂不解决]

3、最终方案

4、后续展望

1、库存本地缓存数量的大小

2、库存变更频繁


一、背景

  1. redis承受所有读压力:商品库存redis当做存储使用,所有读请求全部通过redis
  2. 商品请求量加大,redis承受压力越来越大:
    1. 随着辅导的业务快速发展,在售规格超过百万
    2. 中台接入新业务线[斑马]

二、目标

降低redis承受压力,使系统风险可控。

 

三、方案选型:本地缓存

1、方案比较

降低redis压力,只能把部分流量分发到其他地方。

1.1 数据库

数据库的吞吐量远不及redis,如果要承受同等请求量,成本是一个问题

1.2 本地缓存

把数据缓存到服务器内存里

  • 优点:能够减少网络耗时,访问效率比访问redis更快
  • 缺点:
    • 数据一致性问题 
    • 内存大小问题[如果数据量非常大,则不适合] 
    • 内存无法共用,一定程度上存在浪费

2、最终方案:本地缓存

由于商品访问量一般来自于C端,和业务在售商品数量有关,所以我们属于数据量不大,但是访问量巨大的场景。

但需要考虑数据一致性问题。

 

四、本地缓存

1、缓存的特征

1.1 命中率

命中率 = 返回正确结果数/请求缓存次数,命中率越高,说明缓存的使用率越高

1.2 最大空间

缓存中可以存放最大元素的数量,一旦缓存超过这个数量,则将触发缓存清空策略。防止空间无限扩大

1.3 清空策略  

  • FIFO:先进先出策略
  • LFU:最少使用
  • LRU:最近最少使用
  • 根据过期时间清理

 

2、本地缓存方案比较

压测对比: https://github.com/ben-manes/caffeine/wiki/Benchmarks

无论是全读场景、全写场景、或者读写混合场景,Caffeine都是完胜对手。所以建议使用 Caffeine 来做本地缓存

 

3、Caffeine 

3.1 简介

caffeine 是基于JDK8 的高性能本地缓存库,类似于ConcurrentHashMap,其LocalCache接口就是实现了JDK中的ConcurrentMap接口。但是caffeine为了保护应用程序,提供了自动剔除策略。

 

3.2 特点

  • 自动加载数据到本地缓存中,并且可以配置成异步加载
  • 提供了基于数量剔除策略
  • 提供了基于失效时间剔除策略,这个时间可以是最后 一次 访问或者写入算起
  • 异步刷新
  • Key可以被包装成Weak引用,在下一次GC时可以被回收。value被封装为weak或者soft引用[内存不足时被回收]

 

3.2.1 填充策略

  • 手动加载: 直接调用cache.put(key, value)
  • 同步加载: Caffeine.build()
  • 异步加载:Caffeine.buildAsync(),默认使用ForkJoinPool.commonPool()来执行异步线程,但是我们可以通过Caffeine.executor(Executor) 方法来替换线程池。
//同步加载
private LoadingCache<Long, InventoryEntity> inventoryLocalCache = Caffeine.newBuilder()
            .expireAfterWrite(15, TimeUnit.MINUTES)
            .initialCapacity(1024 * 64)
            .maximumSize(1024 * 1024)
            .executor(ThreadPoolConfiguration.localCacheExecutor)
            .build(new CacheLoader<Long, InventoryEntity>() {
                @Override
                public InventoryEntity load(@NotNull Long key) throws Exception {
                    return inventoryCache.getInventoryById(key);
                }
            });


//异步加载 
private LoadingCache<Long, InventoryEntity> asyncLoadingCache = Caffeine.newBuilder()
            .expireAfterWrite(15, TimeUnit.MINUTES)
            .initialCapacity(1024 * 64)
            .maximumSize(1024 * 1024)
            .executor(ThreadPoolConfiguration.localCacheExecutor)
            .buildAsync(new CacheLoader<Long, InventoryEntity>() {
                @Override
                public InventoryEntity load(@NotNull Long key) throws Exception {
                    return inventoryCache.getInventoryById(key);
                }
            });

//异步加载返回Future
CompletableFuture result = asyncLoadingCache.get(id);
//异步转同步,synchronous 返回了一个LoadingCacheView视图, 可以转换成了一个同步加载的缓存LoadingCache。底层还是通过异步来获取值,只是封装了一下future.get()
loadingCache = asyncLoadingCache.synchronous();

3.2.2 失效策略 

  • 基于大小
    • Caffeine.maximumSize(MAX_SIZE),当数量超过 MAX_SIZE 时,会使用Window TinyLfu策略来删除缓存。

 

  • 基于时间
    • expireAfterAccess(long, TimeUnit):在最后一次访问或者写入后开始计时,在指定的时间后过期。假如一直有请求访问该key,那么这个缓存将一直不会过期。
    • expireAfterWrite(long, TimeUnit): 在最后一次写入缓存后开始计时,在指定的时间后过期。
    • expireAfter(Expiry): 自定义策略,过期时间由Expiry实现独自计算

 

  • 基于引用:AsyncLoadingCache不支持弱引用和软引用
    • Caffeine.weakKeys() 使用弱引用存储key。如果没有其他地方对该key有强引用,那么该缓存就会被垃圾回收器回收。

      Caffeine.weakValues() 使用弱引用存储value。如果没有其他地方对该value有强引用,那么该缓存就会被垃圾回收器回收。

      Caffeine.softValues() 使用软引用存储value。当内存满了过后,软引用的对象以将使用最近最少使用(least-recently-used ) 的方式进行垃圾回收。由于使用软引用是需要等到内存满了才进行回收,所以我们通常建议给缓存配置一个使用内存的最大值。

    • Caffeine.weakValues()和Caffeine.softValues()不可以一起使用。

3.2.3 刷新策略 

      刷新的是通过LoadingCache.refresh(key)方法来指定,并通过调用CacheLoader.reload方法来执行,刷新key会异步地为这个key加载新的value,并返回旧的值(如果有的话)。

刷新操作是使用Executor异步执行的,默认执行程序是ForkJoinPool.commonPool(), 可以通过 Caffeine.executor(Executor)覆盖。

注意:由于是异步刷新,所以从本地缓存get的时候,会返回旧值再去异步刷新,如果在访问量特别小并且机器数特别多的时候,旧值可能会存在很长一段时间。所以可以结合expireAfterWrite 一起使用,expireAfterWrite在访问时会判断是否过期,如果过期了,会同步加载并返回新结果

 

private LoadingCache<Long, InventoryEntity> inventoryLocalCache = Caffeine.newBuilder()
            .expireAfterWrite(15, TimeUnit.MINUTES)
            .initialCapacity(1024 * 64)
            .maximumSize(1024 * 1024)
            .executor(ThreadPoolConfiguration.localCacheExecutor)
            .build(new CacheLoader<Long, InventoryEntity>() {
                @Override
                public InventoryEntity load(@NotNull Long key) throws Exception {
                    return inventoryCache.getInventoryById(key);
                }

                @Override
                public @NotNull
                Map<Long, InventoryEntity> loadAll(@NotNull Iterable<? extends Long> keys)
                        throws Exception {
                    List<InventoryEntity> inventoryEntities = inventoryCache.getInventoriesByIds(Lists.newArrayList(keys));
                    Map<Long, InventoryEntity> inventoryEntityMap = inventoryEntities
                            .stream()
                            .collect(Collectors.toMap(InventoryEntity::getId, Function.identity()));
                    return inventoryEntityMap;
                }
            });

 

3.3 Caffeine原理简介

 

 

 

 

五、本地缓存在商品中的应用[本地缓存+广播消息]

一、分析

由于本地缓存会带来数据一致性的问题,可以把商品的信息分为三类:

  1. 数据永远不会发生改变:比如id关联关系 [skuId – spuId]
  2. 数据会发生改变,但不会影响下单: 比如价格、商品基本信息 
  3. 数据会发生改变,但严重影响下单:比如库存 [库存不足时,下单肯定会失败] 

 

  特点本地缓存
1数据永远不会改变不存在数据一致性问题缓存时间以天为单位
2数据会改变,但不影响下单数据一致性容忍度比较高缓存时间以秒为单位
3数据会改变,跟下单强相关数据一致性容忍度较低尽量不要进行本地缓存

 

二、存在的问题

1、本地缓存数据一致性问题

商品中心库存访问量巨大,Redis承受压力大。由于库存是否还有剩余跟用户能否下单强关联,在一致性上面希望能尽量准确,所以通过本地缓存能减少Redis压力,但是在一定程度上又希望库存量能够保证一致性,两者是相悖的,所以要么本地缓存时间尽可能小,要么解决数据一致性的问题。

1、方案一:库存本地缓存时间尽量小,尽可能保证数据一致性

由于辅导在售规格超过百万,业务会定期来刷新数据,所以在这样场景下,本地缓存时间设置得很小(比如1s),缓存命中的几率也十分小,并不能解决问题

 

2、方案二:通过消息广播解决数据一致性问题

  • 通过广播消息把每台服务器的本地缓存置为失效,库存本地缓存主动失效时间可以设置以分为单位
  • 准备数据修复任务,防止数据错误,手动修复

 

2、本地缓存大小问题[可暂不解决]

以当前的数据量:在售规格数为百万,与之对应的库存数也是百万。如果全部放在本地缓存里,大约100M,所以能够支撑。但是如果后续业务发展迅速,则不能有多少在售库存数就缓存多少

 

3、广播消息性能问题[可暂不解决]

库存广播消息目前跟下单的qps有关,当库存发生变化后 ,会进行广播消息,如果下单量特别大,则存在单台服务器会不断消费消息。目前下单量不超过100 QPS,所以单台服务器也能够支撑

 

3、最终方案

 数据特点信息缓存特点结果
1不存在数据一致性问题:不会变更skuId- spuId映射关系
  • 缓存时间以天为单位
  • 缓存大小:在售规格大小
  • 失效策略:基于时间写入后失效
  • 缓存命中率接近 100%
46w → 23w
2

数据一致性容忍度比较高

&& 请求量不高

spu信息
  • 缓存时间以秒为单位
  • 缓存大小:偏小
  • 失效策略:基于时间写入后失效
  • 缓存命中率较低
 
3

数据一致性容忍度较低

或者

请求量巨大

sku信息

库存信息

  • 缓存时间以分为单位
  • 缓存大小:在售规格大小
  • 失效策略:基于时间写入后失效 &&  更新后失效【消息广播】
  • 缓存命中率:高

sku:360w → 20w

库存:320w → 10w 

 

 

4、后续展望

  • 缓存数量大小:在目前的业务下,在售规格数量全部放进本地缓存里还能接受
  • 库存变更频繁:下单引发的库存更新需要所有服务器处理广播消息,目前也能支持。

但是如果后续业务不断发展,则这种方案就会存在问题

1、库存本地缓存数量的大小

问题:在售库存数太多,不能够全部放进本地缓存里

目标:减少库存本地缓存的数量

方式:筛选热点数据,只针对热点数据进行本地缓存。

方案Redis分布式统计 单机内存统计
特点

能够筛选出全局下的热点数据

但是通过Redis计数反而会增加Redis压力

只能筛选单机下的热点数据,下次请求可能并不会请求到本地缓存

机器众多,缓存命中率??

实现方案

本地先统计,定时上报至Redis,比如30s上报一次,可通过异步方式实现。通过zset可以快速获取到热点数据

 
 本地统计 –> 异步上报 

2、库存变更频繁

问题:下单量变多,导致库存数变更频繁,单机消费广播消息遇到瓶颈

目标:减少库存广播的频率

  • 只针对热点数据进行库存变更的广播
  • 之所以库存跟下单强相关,是在库存将要售罄时才会有强关联关系,其他时候其实关系并不大。可以当库存使用占比达到某个阈值时才进行消息广播 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值