目录
一、背景
- redis承受所有读压力:商品库存redis当做存储使用,所有读请求全部通过redis
- 商品请求量加大,redis承受压力越来越大:
- 随着辅导的业务快速发展,在售规格超过百万
- 中台接入新业务线[斑马]
二、目标
降低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) 方法来替换线程池。
|
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在访问时会判断是否过期,如果过期了,会同步加载并返回新结果
|
3.3 Caffeine原理简介
五、本地缓存在商品中的应用[本地缓存+广播消息]
一、分析
由于本地缓存会带来数据一致性的问题,可以把商品的信息分为三类:
- 数据永远不会发生改变:比如id关联关系 [skuId – spuId]
- 数据会发生改变,但不会影响下单: 比如价格、商品基本信息
- 数据会发生改变,但严重影响下单:比如库存 [库存不足时,下单肯定会失败]
| 特点 | 本地缓存 | ||
|---|---|---|---|
| 1 | 数据永远不会改变 | 不存在数据一致性问题 | 缓存时间以天为单位 |
| 2 | 数据会改变,但不影响下单 | 数据一致性容忍度比较高 | 缓存时间以秒为单位 |
| 3 | 数据会改变,跟下单强相关 | 数据一致性容忍度较低 | 尽量不要进行本地缓存 |
二、存在的问题
1、本地缓存数据一致性问题
商品中心库存访问量巨大,Redis承受压力大。由于库存是否还有剩余跟用户能否下单强关联,在一致性上面希望能尽量准确,所以通过本地缓存能减少Redis压力,但是在一定程度上又希望库存量能够保证一致性,两者是相悖的,所以要么本地缓存时间尽可能小,要么解决数据一致性的问题。
1、方案一:库存本地缓存时间尽量小,尽可能保证数据一致性
由于辅导在售规格超过百万,业务会定期来刷新数据,所以在这样场景下,本地缓存时间设置得很小(比如1s),缓存命中的几率也十分小,并不能解决问题
2、方案二:通过消息广播解决数据一致性问题
- 通过广播消息把每台服务器的本地缓存置为失效,库存本地缓存主动失效时间可以设置以分为单位
- 准备数据修复任务,防止数据错误,手动修复

2、本地缓存大小问题[可暂不解决]
以当前的数据量:在售规格数为百万,与之对应的库存数也是百万。如果全部放在本地缓存里,大约100M,所以能够支撑。但是如果后续业务发展迅速,则不能有多少在售库存数就缓存多少
3、广播消息性能问题[可暂不解决]
库存广播消息目前跟下单的qps有关,当库存发生变化后 ,会进行广播消息,如果下单量特别大,则存在单台服务器会不断消费消息。目前下单量不超过100 QPS,所以单台服务器也能够支撑
3、最终方案
| 数据特点 | 信息 | 缓存特点 | 结果 | |
|---|---|---|---|---|
| 1 | 不存在数据一致性问题:不会变更 | skuId- spuId映射关系 |
| 46w → 23w |
| 2 | 数据一致性容忍度比较高 && 请求量不高 | spu信息 |
| |
| 3 | 数据一致性容忍度较低 或者 请求量巨大 | sku信息 库存信息 |
| sku:360w → 20w 库存:320w → 10w |
4、后续展望
- 缓存数量大小:在目前的业务下,在售规格数量全部放进本地缓存里还能接受
- 库存变更频繁:下单引发的库存更新需要所有服务器处理广播消息,目前也能支持。
但是如果后续业务不断发展,则这种方案就会存在问题
1、库存本地缓存数量的大小
问题:在售库存数太多,不能够全部放进本地缓存里
目标:减少库存本地缓存的数量
方式:筛选热点数据,只针对热点数据进行本地缓存。
| 方案 | Redis分布式统计 | 单机内存统计 |
|---|---|---|
| 特点 | 能够筛选出全局下的热点数据 但是通过Redis计数反而会增加Redis压力 | 只能筛选单机下的热点数据,下次请求可能并不会请求到本地缓存 机器众多,缓存命中率?? |
| 实现方案 | 本地先统计,定时上报至Redis,比如30s上报一次,可通过异步方式实现。通过zset可以快速获取到热点数据 | |
| 本地统计 –> 异步上报 |
2、库存变更频繁
问题:下单量变多,导致库存数变更频繁,单机消费广播消息遇到瓶颈
目标:减少库存广播的频率
- 只针对热点数据进行库存变更的广播
- 之所以库存跟下单强相关,是在库存将要售罄时才会有强关联关系,其他时候其实关系并不大。可以当库存使用占比达到某个阈值时才进行消息广播

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

被折叠的 条评论
为什么被折叠?



