为了有效避免缓存击穿、穿透和雪崩的问题。最基本的缓存设计就是从数据库中查询数据时,无论数据库中是否存在数据,都会将查询的结果缓存起来,并设置一定的有效期。后续请求访问缓存时,如果缓存中存在指定Key时,哪怕对应的Value值为空,也会将数据返回给客户端,客户端根据具体情况进行处理。
业务设计上,如果从数据库中未查询到对应的数据,直接将nul或者空字符串等保存到缓存中,一方面是在代码的可能读性上比较差,不便于后期的维护,另一方面对于混合型缓存的实现上缺乏有效的逻辑处理能力。
通用模型设计:接口化+模板化 多级缓存策略
通用缓存接口
java
@Data //自动生成字段get set
public class SeckillCommonCache {
//缓存数据是否存在
protected boolean exist;
//缓存版本号
protected Long version;
//稍后再试
protected boolean retryLater;
}
通用泛型缓存实现类
java
@Data
public class SeckillBusinessCache<T> extends SeckillCommonCache {
private T data; //泛型字段,表示具体的业务数据
public SeckillBusinessCache<T> with(T data){ //设置缓存中的业务数据 data,并设置缓存数据存在,返回当前对象
this.data = data;
this.exist = true;
return this;
}
public SeckillBusinessCache<T> withVersion(Long version){ //设置缓存的版本号
this.version = version;
return this;
}
public SeckillBusinessCache<T> retryLater(){ //表示需要稍后重试
this.retryLater = true;
return this;
}
public SeckillBusinessCache<T> notExist(){ //表示缓存中没有数据
this.exist = false;
return this;
}
}
整体流程
对获取xxx列表接口进行本地缓存+redis缓存实现
在访问xxx列表接口时,优先从本地缓存获取数据
-
如果本地缓存不存在数据,则从redis缓存中获取数据,且同一时刻只能有一个线程更新本地缓存数据。
-
如果从redis缓存中不存在数据,则同一时刻也只能有一个线程查询到业务数据后,将数据更新到缓存中。
-
其他没有访问数据库机会的线程,快速返回,不占程访问数据库的系统资源。
java
public interface SeckillCacheService {
/**
* 构建缓存的key
*/
String buildCacheKey(Object key);
}
java
public interface SeckillActivityListCacheService extends SeckillCacheService {
// 根据状态和版本号,利用二级缓存 获取活动列表
SeckillBusinessCache<List<SeckillActivity>> getCachedActivities(Integer status, Long version);
//更新缓存数据
SeckillBusinessCache<List<SeckillActivity>> tryUpdateSeckillActivityCacheByLock(Integer status);
}
java
@Service
public class SeckillActivityListCacheServiceImpl implements SeckillActivityListCacheService {
private final static Logger logger = LoggerFactory.getLogger(SeckillActivityListCacheServiceImpl.class);
@Autowired
private LocalCacheService<Long, SeckillBusinessCache<List<SeckillActivity>>> localCacheService;
//分布式锁的key
private static final String SECKILL_ACTIVITES_UPDATE_CACHE_LOCK_KEY = "SECKILL_ACTIVITIES_UPDATE_CACHE_LOCK_KEY_";
//本地锁
private final Lock localCacheUpdatelock = new ReentrantLock();
@Autowired
private DistributedCacheService distributedCacheService;
@Autowired
private SeckillActivityRepository seckillActivityRepository;
@Autowired
private DistributedLockFactory distributedLockFactory;
@Override
public String buildCacheKey(Object key) {
return StringUtil.append(SeckillConstants.SECKILL_ACTIVITIES_CACHE_KEY, key);
}
@Override
public SeckillBusinessCache<List<SeckillActivity>> getCachedActivities(Integer status, Long version) {
//获取本地缓存
SeckillBusinessCache<List<SeckillActivity>> seckillActivitiyListCache = localCacheService.getIfPresent(status.longValue());
if (seckillActivitiyListCache != null){
if (version == null){
logger.info("SeckillActivitesCache|命中本地缓存|{}", status);
return seckillActivitiyListCache;
}
//传递过来的版本小于或等于缓存中的版本号
if (version.compareTo(seckillActivitiyListCache.getVersion()) <= 0){
logger.info("SeckillActivitesCache|命中本地缓存|{}", status);
return seckillActivitiyListCache;
}
if (version.compareTo(seckillActivitiyListCache.getVersion()) > 0){
return getDistributedCache(status);
}
}
return getDistributedCache(status);
}
/**
* 获取分布式缓存中的数据
*/
private SeckillBusinessCache<List<SeckillActivity>> getDistributedCache(Integer status) {
logger.info("SeckillActivitesCache|读取分布式缓存|{}", status);
SeckillBusinessCache<List<SeckillActivity>> seckillActivitiyListCache = SeckillActivityBuilder.getSeckillBusinessCacheList(distributedCacheService.getObject(buildCacheKey(status)), SeckillActivity.class);
if (seckillActivitiyListCache == null){
seckillActivitiyListCache = tryUpdateSeckillActivityCacheByLock(status);
}
if (seckillActivitiyListCache != null && !seckillActivitiyListCache.isRetryLater()){
if (localCacheUpdatelock.tryLock()){
try {
localCacheService.put(status.longValue(), seckillActivitiyListCache);
logger.info("SeckillActivitesCache|本地缓存已经更新|{}", status);
}finally {
localCacheUpdatelock.unlock();
}
}
}
return seckillActivitiyListCache;
}
/**
* 根据状态更新分布式缓存数据
*/
@Override
public SeckillBusinessCache<List<SeckillActivity>> tryUpdateSeckillActivityCacheByLock(Integer status) {
logger.info("SeckillActivitesCache|更新分布式缓存|{}", status);
DistributedLock lock = distributedLockFactory.getDistributedLock(SECKILL_ACTIVITES_UPDATE_CACHE_LOCK_KEY.concat(String.valueOf(status)));
try {
boolean isLockSuccess = lock.tryLock(1, 5, TimeUnit.SECONDS);
if (!isLockSuccess){
return new SeckillBusinessCache<List<SeckillActivity>>().retryLater();
}
List<SeckillActivity> seckillActivityList = seckillActivityRepository.getSeckillActivityList(status);
SeckillBusinessCache<List<SeckillActivity>> seckillActivitiyListCache;
if (seckillActivityList == null){
seckillActivitiyListCache = new SeckillBusinessCache<List<SeckillActivity>>().notExist();
}else {
seckillActivitiyListCache = new SeckillBusinessCache<List<SeckillActivity>>().with(seckillActivityList).withVersion(SystemClock.millisClock().now());
}
distributedCacheService.put(buildCacheKey(status), JSON.toJSONString(seckillActivitiyListCache), SeckillConstants.FIVE_MINUTES);
logger.info("SeckillActivitesCache|分布式缓存已经更新|{}", status);
return seckillActivitiyListCache;
} catch (InterruptedException e) {
logger.info("SeckillActivitesCache|更新分布式缓存失败|{}", status);
return new SeckillBusinessCache<List<SeckillActivity>>().retryLater();
} finally {
lock.unlock();
}
}
}
getCachedActivities()
方法 获取本地缓存数据
首先从本地缓存中获取数据
-
如果存在数据,则验证版本号
-
如果发送过来的版本号为空,则直接返回本地缓存的数据。
-
如果接收到的版本号<=缓存中的版本号,说明缓存中的版本更新,则直接返回本地缓存的数据。
-
-
如果本地缓存的数据为空或接收到的版本号>缓存中的版本号,则调用
getDistributedCache()
方法获取redis缓存数据。
getDistributedCache()
方法 获取分布式缓存数据
首先从分布式缓存中获取数据
-
如果分布式缓存数据为空,则调用
tryUpdateSeckillActivityCacheByLock()
方法尝试从数据库获取数据,并更新分布式缓存数据 -
如果分布式缓存数据不为空,并且不需要稍后重试,则尝试获取本地锁
-
如果本地锁获取成功,则将数据更新到本地缓存,释放本地锁。
-
对更新本地缓存的逻辑添加本地锁
ReentrantLock
,可以实现同一时刻只有一个线程更新本地缓存,避免了多个线程并发更新本地缓存引起的问题
-
tryUpdateSeckillActivitvCacheByLock()
方法 根据状态更新分布式缓存数据
首先获取分布式锁
-
如果分布式锁获取失败,将缓存模型数据设置为稍后重试,直接返回缓存模型数据。
-
如果获取分布式锁成功,则获取数据库数据
-
如果数据库中不存在要查询的数据,则创建缓存模型数据实例,设置为不存在业务数据
-
否则,将查询出的数据设置到缓存模型中,并添加到分布式缓存中,返回缓存模型数据,释放分布式锁。
-
具体应用在其他接口上,比如活动列表缓存,只需要调用getCachedActivities()
方法 即可
测试
效果如下: 如果本地缓存中存在数据,就直接返回本地缓存的数据,由于本地缓存数据的有效期设置的是5秒,所以,当5秒左右的时间后会在同一时刻使用一个线程获取分布式缓存中的数据并更新本地缓存
同样的,分布式缓存中数据的过期时间为5分钟,当超过5分钟再次访问接口时,如果本地缓存没有数据,分布式缓存也查询不到数据,则在同一时刻使用一个线程获取数据库中的数据,并更新分布式缓存中和本地缓存中的数据。