共计 7410 个字符,预计需要花费 19 分钟才能阅读完成。
这篇文章主要介绍“Redis 和本地缓存使用的技巧有哪些”,在日常操作中,相信很多人在 Redis 和本地缓存使用的技巧有哪些问题上存在疑惑,丸趣 TV 小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Redis 和本地缓存使用的技巧有哪些”的疑惑有所帮助!接下来,请跟着丸趣 TV 小编一起来学习吧!
三种缓存的使用场景
这部分会介绍 redis,比如 guava 的 LoadingCache 和快手开源的 ReloadableCache 的使用场景和局限,通过这一部分的介绍就能知道在怎样的业务场景下应该使用哪种缓存,以及为什么。
Redis 的使用场景和局限性
如果宽泛的说 redis 何时使用,那么自然就是用户访问量过高的地方使用,从而加速访问,并且缓解数据库压力。如果细分的话,还得分为单节点问题和非单节点问题。
如果一个页面用户访问量比较高,但是访问的不是同一个资源。比如用户详情页,访问量比较高,但是每个用户的数据都是不一样的,这种情况显然只能用分布式缓存了,如果使用 redis,key 为用户唯一键,value 则是用户信息。
redis 导致的缓存击穿。
但是需要注意一点,一定要设置过期时间,而且不能设置到同一时间点过期。举个例子,比如用户又个活动页,活动页能看到用户活动期间获奖数据,粗心的人可能会设置用户数据的过期时间点为活动结束,这样会
单(热)点问题
单节点问题说的是 redis 的单个节点的并发问题,因为对于相同的 key 会落到 redis 集群的同一个节点上,那么如果对这个 key 的访问量过高,那么这个 redis 节点就存在并发隐患, 这个 key 就称为热 key。
如果所有用户访问的都是同一个资源,比如小爱同学 app 首页对所有用户展示的内容都一样(初期),服务端给 h6 返回的是同一个大 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 和本地缓存使用的技巧有哪些”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注丸趣 TV 网站,丸趣 TV 小编会继续努力为大家带来更多实用的文章!