话不多说先贴代码
/**
* 缓存工具
*/
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不可用会导致系统不可用
- 存在击穿风险