Java实现本地小数据量缓存尝试与实践&设计思考
话不多说先贴代码
/** * 缓存工具 */ public class ConcurrentHashMapCacheUtils{ /** * 当前缓存个数 */ public static Integer CURRENT_SIZE = 0; /** * 时间一分钟 */ static final Long ONE_MINUTE = 60 * 1000L; /** * 缓存超时 */ private static final Long TTL_TIME = 60 * 1000L; /** * 缓存对象 */ private static final ConcurrentHashMap<String, CacheObj> CACHE_OBJECT_MAP = new ConcurrentHashMap<>(); /** * 清理过期缓存是否在运行 */ private static volatile Boolean CLEAN_THREAD_IS_RUN = false; /** * 设置缓存 */ public static void setCache(String cacheKey, String cacheValue, long cacheTime) { Long ttlTime = null; if (cacheTime <= 0L) { if (cacheTime == -1L) { ttlTime = -1L; } else { return; } } CURRENT_SIZE = CURRENT_SIZE + 1; if (ttlTime == null) { ttlTime = System.currentTimeMillis() + cacheTime; } CacheObj cacheObj = new CacheObj(cacheValue, ttlTime); CACHE_OBJECT_MAP.put(cacheKey, cacheObj); } /** * 设置缓存 */ public static void setCache(String cacheKey, String cacheValue) { setCache(cacheKey, cacheValue, TTL_TIME); } public static long getCurrentSize(){ return CACHE_OBJECT_MAP.mappingCount(); } public static List<String> getRecentApp(){ List<String> list = new ArrayList<>(16); for (String key:CACHE_OBJECT_MAP.keySet()){ list.add(key); } return list; } /** * 获取缓存 */ public static String getCache(String cacheKey) { startCleanThread(); if (checkCache(cacheKey)) { return CACHE_OBJECT_MAP.get(cacheKey).getCacheValue(); } return null; } /** * 删除某个缓存 */ public static void deleteCache(String cacheKey) { Object cacheValue = CACHE_OBJECT_MAP.remove(cacheKey); if (cacheValue != null) { CURRENT_SIZE = CURRENT_SIZE - 1; } } /** * 判断缓存在不在,过没过期 */ private static boolean checkCache(String cacheKey) { CacheObj cacheObj = CACHE_OBJECT_MAP.get(cacheKey); if (cacheObj == null) { return false; } if (cacheObj.getTtlTime() == -1L) { return true; } if (cacheObj.getTtlTime() < System.currentTimeMillis()) { deleteCache(cacheKey); return false; } return true; } /** * 删除过期的缓存 */ static void deleteTimeOut() { List<String> deleteKeyList = new LinkedList<>(); for(Map.Entry<String, CacheObj> entry : CACHE_OBJECT_MAP.entrySet()) { if (entry.getValue().getTtlTime() < System.currentTimeMillis() && entry.getValue().getTtlTime() != -1L) { deleteKeyList.add(entry.getKey()); } } for (String deleteKey : deleteKeyList) { deleteCache(deleteKey); } } /** * 设置清理线程的运行状态为正在运行 */ static void setCleanThreadRun() { CLEAN_THREAD_IS_RUN = true; } /** * 开启清理过期缓存的线程 */ private static void startCleanThread() { if (!CLEAN_THREAD_IS_RUN) { ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNamePrefix("clean-cache-pool-").build(); ThreadPoolExecutor cleanThreadPool = new ThreadPoolExecutor( 8, 16, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(8), namedThreadFactory ); cleanThreadPool.execute(new CleanTimeOutThread()); } } } class CacheObj { /** * 缓存对象 */ private String cacheValue; /** * 缓存过期时间 */ private Long ttlTime; CacheObj(String cacheValue, Long ttlTime) { this.cacheValue = cacheValue; this.ttlTime = ttlTime; } String getCacheValue() { return cacheValue; } Long getTtlTime() { return ttlTime; } @Override public String toString() { return "CacheObj {" + "cacheValue = " + cacheValue + ", ttlTime = " + ttlTime + '}'; } } /** * 每一分钟清理一次过期缓存 */ class CleanTimeOutThread implements Runnable{ private static Logger logger = LoggerFactory.getLogger(CleanTimeOutThread.class); @Override public void run() { ConcurrentHashMapCacheUtils.setCleanThreadRun(); while (true) { ConcurrentHashMapCacheUtils.deleteTimeOut(); try { Thread.sleep(ConcurrentHashMapCacheUtils.ONE_MINUTE); } catch (InterruptedException e) { logger.error("Time-out Cache has not been cleaned!{}", e.getMessage()); } if(1==2){ break; } } } }
1、背景
在公司对某个开源组件的使用中,频繁出现客户端无法请求到数据的情况,经排查是发生了并发数过大数据库性能瓶颈的情况。
于是有了对服务端的优化喝如下的思考。
2、设计思考
2.1、是否选择缓存
直接查询DB还是添加缓存,这个取决于系统的并发数,如果系统并发数数据库性能足以支持,则无使用缓存的必要。
如果选择使用缓存,则需要面对的一个风险是:
服务启动/重启的瞬间会出现大量对于数据库的请求,容易发生缓存的击穿/雪崩情况。
关于这种情况我做了专门的优化来避免出现缓存击穿/雪崩,这一段的代码后面优化后再上。
2.2、缓存种类的选择
2.2.1、Java内存
优点:
-
速度快
-
无额外网络开销
-
系统复杂度低
缺点:
-
受限于热点数据数量,对应用内存大小有要求
-
大量缓存同时失效会发生雪崩导致服务性能瞬间下降
-
存在击穿风险
-
多实例存在缓存一致性问题,可能出现对一条数据的重复查询
2.2.2、redis
优点:
-
支持大量数据缓存,扩展性好
-
在多实例时不需要考虑缓存一致性问题
缺点:
-
系统依赖redis,如果redis不可用会导致系统不可用
-
存在击穿风险