缓存工具CacheUtil - 并发环境的缓存值存取
目的
- 适合并发环境的缓存值存取
- 读取缓存值时,只需关注数据来源。不用再关注将源数据存入缓存等后续处理。
- 应用程序N次读取数据时,数据源读取一次,缓存读取N-1次。
设计
当从缓存查找失败,则去数据源获取。获取成功,存入缓存并返回。
实现
-
Sourcable
/** * 可溯源的。可从数据源获取数据 */ public interface Sourcable { /** * 从数据源获取 */ <T> T get(); }
-
CacheUtil
public class CacheUtil { private static final transient Log logger = LogFactory.getLog(CacheUtil.class); // @Resource(name = "memCacheServiceImpl") private static CacheService cache; /** * 获取value * 如果从缓存查找失败,则尝试从数据源获取,获取成功,存入缓存并返回。 * @param key * @param source Sourcable的实现,用于读取源数据。 * @param isShort 是否(短时间)暂存 */ public static <T> T get(String key, Sourcable source, boolean isShort) { logger.debug("-> [" + Thread.currentThread().getId() + "] Enter"); T value = get(key); if (value == null && source != null) { // 缓存查找失败,尝试从数据源获取 logger.debug("-> [" + Thread.currentThread().getId() + "] try to Lock"); key = key.intern(); synchronized (key) { logger.debug("-> [" + Thread.currentThread().getId() + "] Locked"); value = get(key); // 双重检查。防止多线程重复从数据源读取 if (value == null) { // 从数据源获取 value = source.get(); if (value != null) { // 存入缓存 if (isShort) { // 暂存cache put4short(key, value); } else { // 常规存入cache,正常有效期 put(key, value); } } logger.debug("-> [" + Thread.currentThread().getId() + "] Loaded"); } } } logger.debug("-> [" + Thread.currentThread().getId() + "] get: " + value); return value; } // 其他代码省略.. }
测试
- CacheUtilTest
public class CacheUtilTest extends SpringTest { @Test public void get() throws Exception { final Long resourceId = 173L; final String cacheKey = Resource.class.getName() + '#' + resourceId; // 清除 CacheUtil.delete(cacheKey); // 1000个线程并发存取 int num = 1000; Executor executor = Executors.newFixedThreadPool(num); final CountDownLatch latch = new CountDownLatch(num); for (int i = 0; i < num; i++) { executor.execute(() -> { // new String(cacheKey)。 用于验证不同key对象时,同步锁的有效性 String key = new String(cacheKey); // 读取缓存值时,只需关注数据来源。不用再关注将源数据存入缓存。 Resource resource = CacheUtil.get(key, new Sourcable() { @Override public <T> T get() { List<Resource> resources = DAOUtil.findByHql("FROM Resource WHERE resourceId=" + resourceId); if (CollectionUtils.isNotEmpty(resources)) { return resources.get(0); } return null; } }); latch.countDown(); }); } latch.await(); } }
附录
-
SpringTest
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:context/spring.xml") public abstract class SpringTest {}
-
结果
10个线程运行结果:
[PAY][DEBUG] 2016-10-14 15:14:20,526 (CacheUtil.java:46).get - [pool-1-thread-2] 14030
-> [29] Enter
[PAY][DEBUG] 2016-10-14 15:14:20,527 (CacheUtil.java:46).get - [pool-1-thread-8] 14031
-> [35] Enter
[PAY][DEBUG] 2016-10-14 15:14:20,527 (CacheUtil.java:46).get - [pool-1-thread-10] 14031
-> [37] Enter
[PAY][DEBUG] 2016-10-14 15:14:20,528 (CacheUtil.java:46).get - [pool-1-thread-9] 14032
-> [36] Enter
[PAY][DEBUG] 2016-10-14 15:14:20,527 (CacheUtil.java:46).get - [pool-1-thread-4] 14031
-> [31] Enter
[PAY][DEBUG] 2016-10-14 15:14:20,527 (CacheUtil.java:46).get - [pool-1-thread-7] 14031
-> [34] Enter
[PAY][DEBUG] 2016-10-14 15:14:20,526 (CacheUtil.java:46).get - [pool-1-thread-6] 14030
-> [33] Enter
[PAY][DEBUG] 2016-10-14 15:14:20,526 (CacheUtil.java:46).get - [pool-1-thread-5] 14030
-> [32] Enter
[PAY][DEBUG] 2016-10-14 15:14:20,555 (CacheUtil.java:49).get - [pool-1-thread-7] 14059
-> [34] try to Lock
[PAY][DEBUG] 2016-10-14 15:14:20,557 (CacheUtil.java:53).get - [pool-1-thread-7] 14061
-> [34] Locked
[PAY][DEBUG] 2016-10-14 15:14:20,561 (CacheUtil.java:49).get - [pool-1-thread-5] 14065
-> [32] try to Lock
[PAY][DEBUG] 2016-10-14 15:14:20,555 (CacheUtil.java:49).get - [pool-1-thread-4] 14059
-> [31] try to Lock
[PAY][DEBUG] 2016-10-14 15:14:20,547 (CacheUtil.java:49).get - [pool-1-thread-10] 14051
-> [37] try to Lock
[PAY][DEBUG] 2016-10-14 15:14:20,547 (CacheUtil.java:49).get - [pool-1-thread-8] 14051
-> [35] try to Lock
[PAY][DEBUG] 2016-10-14 15:14:20,547 (CacheUtil.java:49).get - [pool-1-thread-9] 14051
-> [36] try to Lock
[PAY][DEBUG] 2016-10-14 15:14:20,547 (CacheUtil.java:49).get - [pool-1-thread-2] 14051
-> [29] try to Lock
[PAY][DEBUG] 2016-10-14 15:14:20,528 (CacheUtil.java:46).get - [pool-1-thread-1] 14032
-> [28] Enter
[PAY][DEBUG] 2016-10-14 15:14:20,527 (CacheUtil.java:46).get - [pool-1-thread-3] 14031
-> [30] Enter
[PAY][DEBUG] 2016-10-14 15:14:20,565 (CacheUtil.java:49).get - [pool-1-thread-6] 14069
-> [33] try to Lock
Hibernate: FROM Resource WHERE resourceId=173
[PAY][DEBUG] 2016-10-14 15:14:20,595 (CacheUtil.java:49).get - [pool-1-thread-1] 14099
-> [28] try to Lock
[PAY][DEBUG] 2016-10-14 15:14:20,599 (CacheUtil.java:49).get - [pool-1-thread-3] 14103
-> [30] try to Lock
[PAY][DEBUG] 2016-10-14 15:14:20,639 (CacheUtil.java:72).get - [pool-1-thread-7] 14143
-> [34] Loaded
[PAY][DEBUG] 2016-10-14 15:14:20,639 (CacheUtil.java:53).get - [pool-1-thread-3] 14143
-> [30] Locked
[PAY][DEBUG] 2016-10-14 15:14:20,639 (CacheUtil.java:76).get - [pool-1-thread-7] 14143
-> [34] get: /uploadfiles/resource/2014122313575119.png
[PAY][DEBUG] 2016-10-14 15:14:20,645 (CacheUtil.java:53).get - [pool-1-thread-1] 14149
-> [28] Locked
[PAY][DEBUG] 2016-10-14 15:14:20,645 (CacheUtil.java:76).get - [pool-1-thread-3] 14149
-> [30] get: /uploadfiles/resource/2014122313575119.png
[PAY][DEBUG] 2016-10-14 15:14:20,651 (CacheUtil.java:76).get - [pool-1-thread-1] 14155
-> [28] get: /uploadfiles/resource/2014122313575119.png
[PAY][DEBUG] 2016-10-14 15:14:20,651 (CacheUtil.java:53).get - [pool-1-thread-6] 14155
-> [33] Locked
[PAY][DEBUG] 2016-10-14 15:14:20,658 (CacheUtil.java:76).get - [pool-1-thread-6] 14162
-> [33] get: /uploadfiles/resource/2014122313575119.png
[PAY][DEBUG] 2016-10-14 15:14:20,658 (CacheUtil.java:53).get - [pool-1-thread-2] 14162
-> [29] Locked
[PAY][DEBUG] 2016-10-14 15:14:20,665 (CacheUtil.java:76).get - [pool-1-thread-2] 14169
-> [29] get: /uploadfiles/resource/2014122313575119.png
[PAY][DEBUG] 2016-10-14 15:14:20,665 (CacheUtil.java:53).get - [pool-1-thread-9] 14169
-> [36] Locked
[PAY][DEBUG] 2016-10-14 15:14:20,671 (CacheUtil.java:76).get - [pool-1-thread-9] 14175
-> [36] get: /uploadfiles/resource/2014122313575119.png
[PAY][DEBUG] 2016-10-14 15:14:20,671 (CacheUtil.java:53).get - [pool-1-thread-8] 14175
-> [35] Locked
[PAY][DEBUG] 2016-10-14 15:14:20,676 (CacheUtil.java:76).get - [pool-1-thread-8] 14180
-> [35] get: /uploadfiles/resource/2014122313575119.png
[PAY][DEBUG] 2016-10-14 15:14:20,676 (CacheUtil.java:53).get - [pool-1-thread-10] 14180
-> [37] Locked
[PAY][DEBUG] 2016-10-14 15:14:20,681 (CacheUtil.java:76).get - [pool-1-thread-10] 14185
-> [37] get: /uploadfiles/resource/2014122313575119.png
[PAY][DEBUG] 2016-10-14 15:14:20,681 (CacheUtil.java:53).get - [pool-1-thread-4] 14185
-> [31] Locked
[PAY][DEBUG] 2016-10-14 15:14:20,686 (CacheUtil.java:76).get - [pool-1-thread-4] 14190
-> [31] get: /uploadfiles/resource/2014122313575119.png
[PAY][DEBUG] 2016-10-14 15:14:20,686 (CacheUtil.java:53).get - [pool-1-thread-5] 14190
-> [32] Locked
[PAY][DEBUG] 2016-10-14 15:14:20,694 (CacheUtil.java:76).get - [pool-1-thread-5] 14198
-> [32] get: /uploadfiles/resource/2014122313575119.png
只有一个id为34的线程Loaded
,其余线程均等待 线程34 处理(从数据库查询结果并存入缓存)完成后,再查找缓存从而命中数据。
问题
- 数据源失效问题
当数据源不存在数据时,一直缓存失败,则会一直从数据源获取数据。从而造成数据源负担。
为防止这个情况,可以再进一步做个过滤保障机制。