张开涛:京东业务数据应用级缓存示例

开发 开发工具
我们的业务数据如商品类目、店铺、商品基本信息都可以进行适当的本地缓存,以提升性能。对于多实例的情况时不仅会使用本地缓存,还会使用分布式缓存,因此需要进行适当的API封装以简化缓存操作。

一、多级缓存API封装

我们的业务数据如商品类目、店铺、商品基本信息都可以进行适当的本地缓存,以提升性能。对于多实例的情况时不仅会使用本地缓存,还会使用分布式缓存,因此需要进行适当的API封装以简化缓存操作。

[[189271]]

1. 本地缓存初始化

  1. public class LocalCacheInitService extends BaseService { 
  2.    @Override 
  3.     publicvoid afterPropertiesSet() throws Exception { 
  4.         //商品类目缓存 
  5.         Cache<String, Object> categoryCache = 
  6.                CacheBuilder.newBuilder() 
  7.                         .softValues() 
  8.                         .maximumSize(1000000) 
  9.                        .expireAfterWrite(Switches.CATEGORY.getExpiresInSeconds()/ 2, TimeUnit.SECONDS) 
  10.                         .build(); 
  11.        addCache(CacheKeys.CATEGORY_KEY, categoryCache); 
  12.     } 
  13.   
  14.     privatevoid addCache(String key, Cache<??> cache) { 
  15.         localCacheService.addCache(key,cache); 
  16.     } 

本地缓存过期时间使用分布式缓存过期时间的一半,防止本地缓存数据缓存时间太长造成多实例间的数据不一致。

另外,将缓存KEY前缀与本地缓存关联,从而匹配缓存KEY前缀就可以找到相关联的本地缓存。

2. 写缓存API封装

先写本地缓存,如果需要写分布式缓存,则通过异步更新分布式缓存。

  1. public void set(final String key, final Object value, final intremoteCacheExpiresInSeconds) throws RuntimeException { 
  2.     if (value== null) { 
  3.         return; 
  4.     } 
  5.   
  6.     //复制值对象 
  7.     //本地缓存是引用,分布式缓存需要序列化 
  8.     //如果不复制的话,则假设之后数据改了将造成本地缓存与分布式缓存不一致 
  9.     final Object finalValue = copy(value); 
  10.     //如果配置了写本地缓存,则根据KEY获得相关的本地缓存,然后写入 
  11.     if (writeLocalCache) { 
  12.        Cache localCache = getLocalCache(key); 
  13.         if(localCache != null) { 
  14.            localCache.put(key, finalValue); 
  15.         } 
  16.     } 
  17.     //如果配置了不写分布式缓存,则直接返回 
  18.     if (!writeRemoteCache) { 
  19.         return; 
  20.     } 
  21.     //异步更新分布式缓存 
  22.     asyncTaskExecutor.execute(() -> { 
  23.         try { 
  24.             redisCache.set(key,JSONUtils.toJSON(finalValue), remoteCacheExpiresInSeconds); 
  25.         } catch(Exception e) { 
  26.             LOG.error("updateredis cache error, key : {}", key, e); 
  27.         } 
  28.     }); 

此处使用了异步更新,目的是让用户请求尽快返回。而因为有本地缓存,所以即使分布式缓存更新比较慢又产生了回源,也可以在本地缓存***。

3. 读缓存API封装

先读本地缓存,本地缓存不***的再批量查询分布式缓存,在查询分布式缓存时通过分区批量查询。

  1. private Map innerMget(List<String> keys, List<Class> types) throwsException { 
  2.    Map<String, Object> result = Maps.newHashMap(); 
  3.    List<String> missKeys = Lists.newArrayList(); 
  4.    List<Class> missTypes = Lists.newArrayList(); 
  5.     //如果配置了读本地缓存,则先读本地缓存 
  6.     if(readLocalCache) { 
  7.         for(int i = 0; i < keys.size(); i++) { 
  8.            String key = keys.get(i); 
  9.            Class type = types.get(i); 
  10.            Cache localCache = getLocalCache(key); 
  11.             if(localCache != null) { 
  12.                Object value = localCache.getIfPresent(key); 
  13.                result.put(key, value); 
  14.                if (value == null) { 
  15.                    missKeys.add(key); 
  16.                     missTypes.add(type); 
  17.                } 
  18.            } else { 
  19.                missKeys.add(key); 
  20.                missTypes.add(type); 
  21.            } 
  22.         } 
  23.     } 
  24.     //如果配置了不读分布式缓存,则返回 
  25.     if(!readRemoteCache) { 
  26.         returnresult; 
  27.     } 
  28.     finalMap<String, String> missResult = Maps.newHashMap(); 
  29.   
  30.     //对KEY分区,不要一次性批量调用太大 
  31.     final List<List<String>>keysPage = Lists.partition(missKeys, 10); 
  32.    List<Future<Map<String, String>>> pageFutures = Lists.newArrayList(); 
  33.   
  34.     try { 
  35.         //批量获取分布式缓存数据 
  36.         for(final List<String>partitionKeys : keysPage) { 
  37.            pageFutures.add(asyncTaskExecutor.submit(() -> redisCache.mget(partitionKeys))); 
  38.         } 
  39.         for(Future<Map<String,String>> future : pageFutures) { 
  40.            missResult.putAll(future.get(3000, TimeUnit.MILLISECONDS)); 
  41.         } 
  42.     } catch(Exception e) { 
  43.        pageFutures.forEach(future -> future.cancel(true)); 
  44.         throw e; 
  45.     } 
  46.     //合并result和missResult,此处实现省略 
  47.     return result; 

此处将批量读缓存进行了分区,防止乱用批量获取API。

二、NULL Cache

首先,定义NULL对象。

  1. private static final String NULL_STRING =new String(); 

当DB没有数据时,写入NULL对象到缓存

  1. //查询DB 
  2. String value = loadDB(); 
  3. //如果DB没有数据,则将其封装为NULL_STRING并放入缓存 
  4. if(value == null) { 
  5.     value = NULL_STRING
  6. myCache.put(id, value); 

读取数据时,如果发现NULL对象,则返回null,而不是回源到DB

  1. value = suitCache.getIfPresent(id); 
  2. //DB没有数据,返回null 
  3. if(value == NULL_STRING) { 
  4.     return null; 

通过这种方式可以防止当KEY对应的数据在DB不存在时频繁查询DB的情况。

三、强制获取***数据

在实际应用中,我们经常需要强制更新数据,此时就不能使用缓存数据了,可以通过配置ThreadLocal开关来决定是否强制刷新缓存(refresh方法要配合CacheLoader一起使用)。

  1. if(ForceUpdater.isForceUpdateMyInfo()) { 
  2.     myCache.refresh(skuId); 
  3. String result = myCache.get(skuId); 
  4. if(result == NULL_STRING) { 
  5.     return null; 

四、失败统计

  1. private LoadingCache<String, AtomicInteger> failedCache = 
  2.        CacheBuilder.newBuilder() 
  3.                .softValues() 
  4.                .maximumSize(10000) 
  5.                .build(new CacheLoader<String, AtomicInteger>() { 
  6.                    @Override 
  7.                     public AtomicIntegerload(String skuId) throws Exception { 
  8.                         return new AtomicInteger(0); 
  9.                    } 
  10.                }); 

当失败时,通过failedCache.getUnchecked(id).incrementAndGet()增加失败次数;当成功时,使用failedCache.invalidate(id)失效缓存。通过这种方式可以控制失败重试次数,而且又是内存敏感缓存。当内存不足时,可以清理该缓存腾出一些空间。

五、延迟报警

  1. private static LoadingCache<String, Integer> alarmCache = 
  2.        CacheBuilder.newBuilder() 
  3.                 .softValues() 
  4.                .maximumSize(10000).expireAfterAccess(1, TimeUnit.HOURS) 
  5.                .build(new CacheLoader<String, Integer>() { 
  6.                    @Override 
  7.                    public Integer load(String key) throws Exception { 
  8.                         return 0; 
  9.                    } 
  10.                }); 
  11.   
  12. //报警代码 
  13. Integer count = 0
  14. if(redis != null) { 
  15.     StringcountStr = Objects.firstNonNull(redis.opsForValue().get(key), "0"); 
  16.     count =Integer.valueOf(countStr); 
  17. } else { 
  18.     count = alarmCache.get(key); 
  19. if(count % 5 == 0) { //5次报一次 
  20.     //报警 
  21. countcount = count + 1; 
  22. if(redis != null) { 
  23.     redis.opsForValue().set(key,String.valueOf(count), 1, TimeUnit. HOURS); 
  24. } else { 
  25.     alarmCache.put(key,count); 

如果一出问题就报警,则存在报警量非常多或者假报警,因此,可以考虑N久报警了M次,才真正报警。此时,也可以使用Cache来统计。本示例还加入了Redis分布式缓存记录支持。

六、性能测试

笔者使用JMH 1.14进行基准性能测试,比如测试写。

  1. @Benchmark 
  2. @Warmup(iterations = 10time = 10timeUnit =TimeUnit.SECONDS) 
  3. @Measurement(iterations = 10time = 10timeUnitTimeUnit.SECONDS) 
  4. @BenchmarkMode(Mode.Throughput) 
  5. @OutputTimeUnit(TimeUnit.SECONDS) 
  6. @Fork(1) 
  7. public void test_1_Write() { 
  8.     counterWritercounterWriter= counterWriter + 1; 
  9.     myCache.put("key"+ counterWriter, "value" + counterWriter); 

使用JMH时首先进行JVM预热,然后进行度量,产生测试结果(本文使用吞吐量)。建议读者按照需求进行基准性能测试来选择适合自己的缓存框架。

【本文是51CTO专栏作者张开涛的原创文章,作者微信公众号:开涛的博客( kaitao-1234567)】

戳这里,看该作者更多好文

责任编辑:赵宁宁 来源: 51CTO专栏
相关推荐

2017-05-01 17:03:01

Java缓存分布式

2017-05-05 10:13:03

应用级缓存缓存代码

2017-05-10 11:40:29

缓存Nginx HTTP

2017-05-18 16:07:23

回滚数据库代码

2017-04-18 14:49:38

应用层API代码

2017-06-16 15:16:15

2017-06-04 16:24:27

线程线程池中断

2017-07-02 16:50:21

2012-12-13 17:38:48

2012年度IT博客大IT博客大赛博客

2016-06-17 14:19:52

数据中心

2010-06-02 17:46:54

MySQL 查询缓存

2022-05-12 14:34:14

京东数据

2015-07-24 12:38:00

吴静涛

2016-01-04 15:16:01

京东详情页实践

2010-10-19 08:59:40

PHP缓存技术

2016-11-10 14:38:44

京东深度学习

2009-08-13 17:50:49

Hibernate 3

2018-01-18 19:11:36

2011-01-28 09:29:51

PHPWeb

2020-11-19 15:01:26

京东大数据数据平台
点赞
收藏

51CTO技术栈公众号