业务维护用户合同数据表,当合同表数据量造成某些性能瓶颈(B+树深度增加,带来的磁盘IO性能开销;数据库实例的负载瓶颈;DBA数据运维压力,备份和恢复耗时)的时候,通常就会采取分库表+缓存等方案为数库;这篇不可不打算聊分库表,分库表参考:2、mysql数据分库表实践 ;这篇博客主要聊通过Redis缓存合同数据的一些事儿;
当我们缓存合同数据的时候,缓存使用的众多问题中——击穿、穿透、雪崩、预热、限流、降级、存储pojo、一致性中等,结合实际业务场景我们对一致性和存储POJO方面的问题定制可行方案;这两个方面我应该考虑哪些问题?
首先看一下Cache Aside Pattern更新缓存的过程:
Cache Aside Pattern模式的方案设计的核心优秀思想:
(1)、删除缓存,而非更新缓存(lazy计算的思想)
(2)、先删除缓存,再修改数据库
Cache Aside Pattern模式的方案设计的缺陷&待解决:
围绕这个核心不一致问题,业务合同数据缓存的解决方案核心流程://todo 此处少一个流程图
1、合同数据获取核心逻辑
/**
* 走缓存策略加载数据
* @param customId
* @param productNo
* @return
*/
@Override
public ContractCacheInfo loadFromCache(String customId, String productNo) {
// 获取缓存数据,若缓存数据不存在做更新
ContractCacheInfo cacheInfo=(ContractCacheInfo)defaultTradeCache.query(calculator.getQueryKey(customId, productNo),() -> cacheService.loadFromDB(customId, productNo));
return cacheInfo;
}
@Override
public T query(String key, Supplier dbSupplier) {
...
// 校验是否存在锁。如果检测lock发生异常,认为有锁
boolean locked = true;
try {
locked = redisLock.existsLock(lockerKeyCalculator.apply(key));
} catch (Throwable t) {
}
if (locked) {
return dbSupplier.get();
}
T info = null;
try {
// 校验锁不存在,则先从缓存中获取数据,反序列化
byte[] data = redis.get(key.getBytes(Charsets.UTF_8));
if (data != null) {
info = serializer.decode(data, clazz);
}
} catch (Throwable ex) {
}
if (info != null) {
return info;
}
// 缓存不存在;查询DB后,load data to db
return getAndCache(key, dbSupplier, serializer, conf);
}
/**
* 加载DB数据到,写入缓存(加分布式锁)
*/
private T getAndCache(String key,Supplier dbSupplier,
Serializer serializer,TradeCacheConfig conf) {
String lockKey = lockerKeyCalculator.apply(key);
// 获取分布式锁;加锁失败,直接读库,不加载缓存;加锁成功,才读库且加载缓存
boolean locked = redisLock.tryLock(lockKey, conf.lockExpireInMs());
try {
if (!locked) {
return dbSupplier.get();
}
// 查询DB数据 (DB 操作异常,直接对外抛出异常)
T data = dbSupplier.get();
if (data != null) {
redis.setex(key.getBytes(Charsets.UTF_8), Math.max(conf.cacheExpireInMs() / 1000, 1),serializer.encode(data));
}
return data;
}catch(Exception e){
...
}finally {
if (locked) {
//解锁失败,不进行处理,这样可能会导致下次加锁加不上
try {
redisLock.unLock(lockKey);
} catch (Throwable t) {
...
}
}
}
}
2、合同数据变更核心逻辑
/**
* 更新合同数据
*/
private void procPaySwitch() {
...
defaultTradeCache.execute(calculator.getCacheKey(pc.getCustomId(), pc.getProductNo().getProductNo()),template, new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
// 更新数据
platContractService.updateByIdAndPlatStatus(pc, tarPlatStatus, descStatus, Boolean.TRUE);
platContractHistoryMapper.save(pcHis);
}
});
}
@Override
public TR execute(Set cacheKeys,
TransactionTemplate transTemplate,
TransactionCallback action)
throws TransactionException {
if (cacheKeys == null || cacheKeys.size() == 0) {
cacheKeys = Collections.emptySet();
}
// 添加需要处理的 key
cacheThreadLocal.addCacheKeys(cacheKeys);
// 加锁,加锁失败,依然可以正常进行,加锁会进行必要的重试
cacheKeys.forEach(cacheKey -> cacheThreadLocal.setCacheLock(cacheKey, lockBeforeTrans(cacheKey)));
return transTemplate.execute((status) -> {
try {
// 注册事物事务处理监听,从而实现事物提交后做处理。
if (!cacheThreadLocal.hasCacheSynchronizer()) {
JdbcTemplate jt = new JdbcTemplate(getDataSource(transTemplate));
TransactionSynchronizationManager.registerSynchronization(
new CacheTransactionSynchronizer(
this.cacheThreadLocal,
t -> CacheKeyStore.save(jt, t),
t -> deleteCache(t, jt.getDataSource()),
() -> {
if (conf instanceof DefaultTradeCacheConfig) {
((DefaultTradeCacheConfig) conf).clearOnceSwitch();
}
}));
cacheThreadLocal.setCacheSynchronizer();
}
} catch (Exception e) {
Metrics.sum("trade_cache", "add_cache_syncer:error:count");
}
// 执行DB 操作
return action.doInTransaction(status);
});
}
/**
* 添加即将缓存的cache key
*
* @param cacheKeySet
*/
public void addCacheKeys(Set cacheKeySet) {
if (CollectionUtils.isEmpty(cacheKeySet)) {
return;
}
Set keys = cacheKeys.get();
if (CollectionUtils.isEmpty(keys)) {
cacheKeys.set(cacheKeySet.stream().map(s -> new CacheKey(s)).collect(Collectors.toSet()));
} else {
cacheKeySet.stream().forEach(c -> {
CacheKey newkey = new CacheKey(c);
if (!keys.contains(newkey)) {
keys.add(newkey);
}
});
}
}
/**
* 事物前加锁操作
* 尝试加锁3次,每次间隔200ms,加锁成功返回true,失败返回false
*/
private boolean lockBeforeTrans(String cacheKey) {
//缓存当前不可操作 ,不进行加锁
if (!(redis.isOnline() && conf.setEnable())) {
return false;
}
//加锁前先确认是否已经加锁,已经加锁直接返回(嵌套事务相关)
boolean hasLocker = cacheThreadLocal.hasCacheLock(cacheKey);
if(hasLocker){
return true;
}
//加锁逻辑
int tryCounts = 0;
while (tryCounts++ < 3) {
try {
boolean locked = redisLock.tryLock(lockerKeyCalculator.apply(cacheKey), conf.lockExpireInMs());
if (locked) {
return locked;
}
} catch (Exception ex) {
log.error("setCacheLock has error, key:{}", cacheKey, ex);
}
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
}
if (tryCounts > 1) {
Metrics.sum("trade_cache", "lockBeforeTrans:try:count");
}
}
return false;
}
/**
* 删除缓存
*/
private void deleteCache(CacheKey cacheKey, DataSource dataSource) {
JdbcTemplate jt = new JdbcTemplate(dataSource);
boolean hasLocker = cacheThreadLocal.hasCacheLock(cacheKey.getCacheKey());
log.debug("deleteCache hasLocker:{},cacheKey.getCacheKey:{}", hasLocker, cacheKey.getCacheKey());
// 同步处理若成功
if (!deleteSync(cacheKey, hasLocker, jt)) {
// 异步处理
deleteAsync(cacheKey, jt);
}
}
/**
* 同步清除缓存
*/
private boolean deleteSync(final CacheKey cacheKey, boolean hasLock, JdbcTemplate jdbc) {
if (!redis.isOnline()) {
return true;
}
String lockerKey = lockerKeyCalculator.apply(cacheKey.getCacheKey());
//是否已经加过锁
boolean locked = hasLock;
if (!hasLock) {
try {
locked = redisLock.tryLock(lockerKey, conf.lockExpireInMs());
} catch (Throwable t) {
Metrics.sum("trade_cache", "delsynctrylock:error:count");
}
if (!locked) {
return false;
}
}
try {
try {
// 删除缓存
this.redis.del(cacheKey.getCacheKey());
// 删除DB
CacheKeyStore.delete(jdbc, cacheKey);
return true;
} catch (Throwable t) {
Metrics.sum("trade_cache", "delsyncdel:error:count");
log.error("delete cache error from redis and db, errMsg={}", t.getMessage(), t);
}
} finally {
if (locked) {
try {
redisLock.unLock(lockerKey);
} catch (Throwable t) {
Metrics.sum("trade_cache", "delsyncunlock:error:count");
}
}
}
return false;
}
/**
* 异步清除缓存
*/
private void deleteAsync(final CacheKey cacheKey, JdbcTemplate jdbc) {
// 线程异步处理
executor.execute(() -> {
// 异步重试
for (int i = 0; i < conf.deleteCacheRetryIntervalsInMs().length; i++) {
try {
//间歇时间
Thread.sleep(conf.deleteCacheRetryIntervalsInMs()[i]);
if (deleteSync(cacheKey, false, jdbc)) {
Metrics.sum("trade_cache", "async_del_cache_retry_times:count");
break;
}
} catch (Throwable e) {
Metrics.sum("trade_cache", "delasync:error:count");
log.error("asyncDelCache has exp", e);
}
}
});
}
- 合同数据变更的编程事务进行增强;事务开启前先对操作数据加分布式锁;可以批量添加;
private final ThreadLocal> cacheKeys = new ThreadLocal<>();
private final ThreadLocal
- 注册事物事务处理监听,从而实现事物提交后做处理。
/**
* 事务提交前后进行一些必要的处理
*/
public class CacheTransactionSynchronizer extends TransactionSynchronizationAdapter {
public CacheTransactionSynchronizer(CacheThreadLocal cacheThreadLocal,
Consumer saveCacheKey,
Consumer deleteCache,
Runnable clearFunc) {
this.cacheThreadLocal = cacheThreadLocal;
this.saveCacheKey = saveCacheKey;
this.deleteCache = deleteCache;
this.clearFunc = clearFunc;
}
@Override
public void beforeCommit(boolean readOnly) {
Set cacheKeys = cacheThreadLocal.getCacheKeys();
if (CollectionUtils.isEmpty(cacheKeys)) return;
try {
//将这些cache 可以插入到待删除的列表中
cacheKeys.forEach(cacheKey -> saveCacheKey.accept(cacheKey));
Metrics.sum("trade_cache", "before_commit:save_cache_key:succ:count");
} catch (Exception ex) {
TradeCacheLog.log.error("beforeCommit ex", ex);
Metrics.sum("trade_cache", "before_commit:save_cache_key:fail:count");
}
}
@Override
public void afterCommit() {
// 删除缓存处理
Set cacheKeys = cacheThreadLocal.getCacheKeys();
if (CollectionUtils.isEmpty(cacheKeys)) return;
try {
cacheKeys.forEach(cacheKey -> deleteCache.accept(cacheKey));
Metrics.sum("trade_cache", "after_commit:clear_cache:succ:count");
} catch (Exception ex) {
TradeCacheLog.log.error("afterCommit has exp.", ex);
Metrics.sum("trade_cache", "after_commit:clear_cache:fail:count");
}
if (clearFunc != null) {
clearFunc.run();
}
}
@Override
public void afterCompletion(int status) {
Metrics.sum("trade_cache", "after_completion:clear_thread_local:count");
cacheThreadLocal.clear();
}
private CacheThreadLocal cacheThreadLocal;
private Consumer saveCacheKey;
private Consumer deleteCache;
private Runnable clearFunc;
}
- 事务提交前(beforeCommit):将这些cache 删除操作,插入到待删除的同步队列中;队列存储介质这里使用的一个数据库表;列表起队列的作用,主要是数据变更操作串行化;
- 事务提交后(afterCommit):删除Redis缓存,并删除缓存变更的操作队列DB数据;完成后解锁;
- 事务完成后(afterCompletion):清理ThreadLocol中的数据标识、锁定标识;
3、合同数据变更补定时逻辑
由于读请求进行了非常轻度的异步化,所以一定要注意读超时的问题,每个读请求必须在超时时间范围内返回,该解决方案,最大的风险点在于说,可能数据更新很频繁,导致队列中积压了大量更新操作在里面,然后读请求会发生大量的超时,最后导致大量的请求直接走数据库。务必通过一些模拟真实的测试,看看更新数据的频率是怎样的。
另外一点,因为一个队列中,可能会积压针对多个数据项的更新操作,因此需要根据自己的业务情况进行测试,可能需要部署多个服务,每个服务分摊一些数据的更新操作。
如果一个DB数据队列积压100个合同变更操作,每个用户合同修改操作要耗费 10ms 去完成,那么最后一个用户合同的读请求,可能等待 10 * 100 = 1000ms = 1s 后,才能得到数据,这个时候就导致读请求的长时阻塞。
一定要做根据实际业务系统的运行情况,去进行一些压力测试,和模拟线上环境,去看看最繁忙的时候,内存队列可能会挤压多少更新操作,可能会导致最后一个更新操作对应的读请求,会 hang 多少时间
如果读请求在 200ms 返回,如果你计算过后,哪怕是最繁忙的时候,积压 10 个更新操作,最多等待 200ms,那还可以的。
如果一个内存队列中可能积压的更新操作特别多,那么你就要加机器,让每个机器上部署的服务实例处理更少的数据,那么每个内存队列中积压的更新操作就会越少。
其实根据之前的项目经验,一般来说,数据的写频率是很低的,因此实际上正常来说,在队列中积压的更新操作应该是很少的。像这种针对读高并发、读缓存架构的项目,一般来说写请求是非常少的,每秒的 QPS 能到几百就不错了。
DB更新用户合同数据修改操作队列,补处理定时代码,每隔1s执行一次:
/**
* 离线删除缓存
*/
@QSchedule("fintech.contract.online.del.cache")
public void onlineProcDelKey(Parameter parameter) {
final TaskParamDto dto = builderTaskProcessUtil()
.analyParam(parameter, "online.del.cache", LOGGER, LOGGER, "");
builderTaskProcessUtil().handle(dto);
}
private TaskProcessUtil builderTaskProcessUtil() {
return new TaskProcessUtil() {
DataSource dataSource = ((DataSourceTransactionManager) template.getTransactionManager()).getDataSource();
@Override
public void consume(CacheKey queue, TaskParamDto taskParamDto) {
try {
defaultTradeCache.delete(queue.getCacheKey(), dataSource);
LOGGER.info("online.del.cache cacheQueue:{}", queue);
} catch (Exception ex) {
LOGGER.error("online.del.cache has exp, cacheQueue:{}", queue, ex);
}
}
@Override
public List produce(TaskParamDto paramDto) {
// 默认一次处理 200 条数据
Integer pageSize = paramDto.getPageSize();
if (pageSize == 0) {
pageSize = 200;
}
return CacheKeyStore.query(new JdbcTemplate(dataSource), pageSize);
}
};
}
@Override
public void delete(String cacheKey, DataSource dataSource) {
deleteCache(new CacheKey(cacheKey), dataSource);
}
/**
* 删除缓存
*/
private void deleteCache(CacheKey cacheKey, DataSource dataSource) {
JdbcTemplate jt = new JdbcTemplate(dataSource);
boolean hasLocker = cacheThreadLocal.hasCacheLock(cacheKey.getCacheKey());
log.debug("deleteCache hasLocker:{},cacheKey.getCacheKey:{}", hasLocker, cacheKey.getCacheKey());
// 同步处理若成功
if (!deleteSync(cacheKey, hasLocker, jt)) {
// 异步处理
deleteAsync(cacheKey, jt);
}
}
参考文档:
- 如何保证缓存与数据库的双写一致性?
- 缓存更新的套路
- 主从DB与cache一致性优
你可能感兴趣的:(热点业务设计相关那些事儿)