分布式应用缓存使用模式

在我们使用缓存时,有一些模式或者策略。其主要分为两大类,Cache-Aside:由业务代码直接维护缓存;Cache-As-SoR:把Cache作为数据源,所有的读写操作都是对Cache而言的,如果要执行持久化等操作,则是由Cache本身去处理。

Cache-Aside

读场景:先从缓存读取,如果没有命中,则回源到DB中去获取,并且将数据放入缓存,以备下一次使用。

写场景:先将数据写入DB,成功后再将数据同步至缓存(也可能是删除过期缓存)。

Cache-Aside方式的缓存策略,通常情况下我们会想到AOP就可以解决这个事情。笔者也写过简单的基于AOP的注解缓存,但是还有更为强大的中间件封装。其中有Spring官方提供的Spring Cache和Alibaba开源的JetCache。但是笔者认为JetCache使用起来更加便利,功能更为强大,后续会有文章单独介绍。

缓存更新时,有可能会出现多实例同时更新,则可以考虑以下2中场景。

用户维度的数据(账户流水/订单等),产生并发的几率比较小,通常情况下不会考虑到并发更新的问题,加上个过期时间就可以了。

如果是基础数据(商品等),则可以考虑使用canal订阅binlog,来实现增量更新分布式缓存,这样就不会出现缓存数据不一致的情况。但是本地缓存要设置合理的过期时间,来容忍本地缓存的延迟。

Cache-As-SoR

Cache-As-SoR把Cache看成数据源,所有的操作都只是对Cache操作的,然后Cache再异步的去进行持久化存储。其有3种实现:read-through、write-through、write-behind。

Read-Through

业务代码首先读取Cache,当Cache没有时,此时回源操作交给Cache去操作。这样对业务代码来说是透明的。下面看一下Guava的实现:

LoadingCache<Integer, Result<Category>> getCache = CacheBuilder.newBuilder()
	.softValues().maximumSize(5000).expireAfterWrite(2, TimeUnit.MINUTES)
	.build(new CacheLoader<Integer, Result<Category>>() {
		@Override
		public Result<Category> load(final Integer sortId) throws Exception {
			return categoryService.get(sortId);
		}
	});

build cache时,传入了一个CacheLoader用来加载缓存,流程如下:

(1)业务代码调用getCache(sortId)时,首先查询Cache,若有则直接返回。

(2)若没有,则委托给CacheLoader去回源,然后写入缓存。

这种委托模式使代码看起来更加的整洁,并且在委托代码中可以更容易的实现同步控制,避免并发更新缓存使得DB压力过大。但是这种模式需要注意的是,委托的过程中失败的问题,要去不断的重试去履约缓存的操作。

Write-Through

业务代码首先写到Cache中,然后负责写Cache和DB数据。Guava Cache并没有提供支持,Ehcache提供了这种支持。

CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder().build(true);
Cache<String, String> myCache = cacheManager.createCache("myCache", 
		CacheConfigurationBuilder.newCacheConfigurationBuilder(String.class, String.class, 
			ResourcePoolsBuilder.newResourcePoolsBuilder().heap(100, MemoryUnit.MB)))
	.withDispatcherConcurrency(4)
	.withExpiry(Expirations.timeToLiveExpiration(Duration.of(10, TimeUnit.SECONDS)))
	.withLoaderWriter(new DefaultCacheLoaderWriter<String, String>() {
		@Override
		public void write(String key, String value) throws Exception {
			// write
		}
		@Override
		public void writeAll(Iterable<? extends Map.Entry<? extends String, ? extends String>> entries) throws BulkCacheWritingException, Exception {
			for (Object entry : entries) {
				// batch write
			}
		}
		@Override
		public void delete(String key) throws Exception {
			// delete
		}
		@Override
		public void deleteAll(Iterable<? extends String> keys) throws BulkCacheWritingException, Exception {
			for (Object key : keys) {
				// batch delete
			}
		}
	}).build();

Write-Behind

这种模式是在写入缓存之后,再异步的进行持久化操作。异步化之后,可以实现批量入库、延迟入库等。


参考:《亿级流量网站架构核心技术》