用户合同数据缓存业务实践

业务维护用户合同数据表,当合同表数据量造成某些性能瓶颈(B+树深度增加,带来的磁盘IO性能开销;数据库实例的负载瓶颈;DBA数据运维压力,备份和恢复耗时)的时候,通常就会采取分库表+缓存等方案为数库;这篇不可不打算聊分库表,分库表参考:2、mysql数据分库表实践 ;这篇博客主要聊通过Redis缓存合同数据的一些事儿;

当我们缓存合同数据的时候,缓存使用的众多问题中——击穿、穿透、雪崩、预热、限流、降级、存储pojo、一致性中等,结合实际业务场景我们对一致性和存储POJO方面的问题定制可行方案;这两个方面我应该考虑哪些问题?

  • 更新缓存数据我们常用的 Cache Aside Pattern 原理&优点、缺陷是什么?
  • 如何解决Cache Aside Pattern存在的一致性缺陷问题?
  • 存储POJO时,我们会面临哪些序列化问题?如何解决?

我们为什么使用Cache Aside Pattern(旁路缓存)更新缓存?

首先看一下Cache Aside Pattern更新缓存的过程:

Cache Aside Pattern读写缓存
Cache Aside Pattern 缓存读写

Cache Aside Pattern模式的方案设计的核心优秀思想:

(1)、删除缓存,而非更新缓存(lazy计算的思想)

  • 避免了复杂业务场景,缓存数据不单单是数据库中直接获取数据;例如,更新某个表的字段,需要查询另外两个表的数据并进行运算,才能计算出缓存最新的值;
  • 频繁更新一个,没有使用的缓存是一件很不划算的事儿;举个栗子,一个缓存涉及的表的字段,在 1 分钟内就修改了 20 次,或者是 100 次,那么缓存更新 20 次、100 次;但是这个缓存在 1 分钟内只被读取了 1 次,有大量的冷数据;

(2)、先删除缓存,再修改数据库

  • 如果先修改库,删除缓存失败,会导致读取到的缓存中的旧数据,出现数据不一致;
  • 先删除缓存,只是删除成功才会去更新数据库;即使更新库失败了,读取缓存的时候还会重新读库、入缓存;(高并发情况没考虑);

Cache Aside Pattern模式的方案设计的缺陷&待解决:

  • 当并发量高的情况;即使是先删除缓存,后更新数据库,当数据还没有完成修改时;一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中。随后数据变更的程序完成了数据库的修改。数据库和缓存中的数据不一样了。。。

如何解决Cache Aside Pattern带来的问题?

围绕这个核心不一致问题,业务合同数据缓存的解决方案核心流程://todo  此处少一个流程图

 

 

  • 通过分布式锁+事务增强+更新操作持久化同步队列+定时补单 共同保证DB和Redis数据的最终一致性;

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<T> 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> T getAndCache(String key,Supplier<T> dbSupplier,
                              Serializer<T> 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) {
                   ...
                }
            }
        }
    }
  • 获取缓存数据前,先校验是否有用户数据的分布式锁;如果有数据变更添加的锁,使用DB获取数据直接返回,不做缓存更新;
  • 若不存在锁,则首先读取缓存数据;若缓存二进制数据存在,进行反序列化后返回;
  • 若获取缓存数据不存在,则添加用户数据变更的分布式锁,加锁成功时使用DB获取数据,并序列化为二进制数据后setex到Redis;若加锁失败,只使用DB获取数据返回,不做Redis缓存更新;
  • 最后记得释放锁;

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> TR execute(Set<String> cacheKeys,
                           TransactionTemplate transTemplate,
                           TransactionCallback<TR> 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<String> cacheKeySet) {
        if (CollectionUtils.isEmpty(cacheKeySet)) {
            return;
        }
        Set<CacheKey> 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<Set<CacheKey>> cacheKeys = new ThreadLocal<>();

    private final ThreadLocal<Map<String, Boolean>> cacheLock = new ThreadLocal<>();

    private final ThreadLocal<Boolean> cacheSynchronizer = new ThreadLocal<>();
  • 注册事物事务处理监听,从而实现事物提交后做处理。
/**
 * 事务提交前后进行一些必要的处理
 */
public class CacheTransactionSynchronizer extends TransactionSynchronizationAdapter {

    public CacheTransactionSynchronizer(CacheThreadLocal cacheThreadLocal,
                                        Consumer<CacheKey> saveCacheKey,
                                        Consumer<CacheKey> deleteCache,
                                        Runnable clearFunc) {
        this.cacheThreadLocal = cacheThreadLocal;
        this.saveCacheKey = saveCacheKey;
        this.deleteCache = deleteCache;
        this.clearFunc = clearFunc;
    }

    @Override
    public void beforeCommit(boolean readOnly) {
        Set<CacheKey> 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<CacheKey> 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<CacheKey> saveCacheKey;

    private Consumer<CacheKey> 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<String> dto = builderTaskProcessUtil()
                .analyParam(parameter, "online.del.cache", LOGGER, LOGGER, "");
        builderTaskProcessUtil().handle(dto);
    }


    private TaskProcessUtil<CacheKey, String> builderTaskProcessUtil() {
        return new TaskProcessUtil<CacheKey, String>() {
            DataSource dataSource = ((DataSourceTransactionManager) template.getTransactionManager()).getDataSource();

            @Override
            public void consume(CacheKey queue, TaskParamDto<String> 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<CacheKey> produce(TaskParamDto<String> 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);
        }
    }

参考文档:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值