package com.zjs.remote.config.redis; import com.zjs.remote.config.ApplicationContextHolder; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang3.RandomUtils; import org.apache.ibatis.cache.Cache; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.redis.core.RedisTemplate; import java.security.MessageDigest; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; /** * MyBatis二級緩存Redis實現 */ public class MybatisRedisCache implements Cache { private static final Logger logger = LoggerFactory.getLogger(MybatisRedisCache.class); /** * 統一緩存頭 */ private static final String CACHE_NAME = "MyBatis:"; /** * 讀寫鎖:解決緩存擊穿 */ private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); /** * 表空間ID:方便後面的緩存清理 */ private final String id; /** * redis服務接口:提供基本的讀寫和清理 */ //private static volatile RedisService redisService; //private RedisTemplate redisService; //(RedisTemplate) ApplicationContextHolder.getBean("redisTemplate"); //這裏使用了redis緩存,使用springboot自動注入 private RedisTemplate redisTemplate; /** * 信息摘要 */ private volatile MessageDigest messageDigest; /////////////////////// 解決緩存雪崩,具體範圍根據業務需要設置合理值 ////////////////////////// /** * 緩存最小有效期 */ private int minExpireMinutes = 60; /** * 緩存最大有效期 */ private int maxExpireMinutes = 120; /** * MyBatis給每個表空間初始化的時候要用到 * * @param id 其實就是namespace的值 */ public MybatisRedisCache(String id) { if (id == null) { throw new IllegalArgumentException("Cache instances require an ID"); } this.id = id; } /** * 獲取ID * * @return 真實值 */ @Override public String getId() { return id; } /** * 創建緩存 * * @param key 其實就是sql語句 * @param value sql語句查詢結果 */ @Override public void putObject(Object key, Object value) { try { String strKey = generateRedisKey(key); // 在redis額外維護CacheNamespace創建的key,clear的時候只清理當前CacheNamespace的數據 getRedisTemplate().opsForHash().put(CACHE_NAME + id, strKey, "1"); // 有效期 隨機,防止雪崩 int expireMinutes = RandomUtils.nextInt(minExpireMinutes, maxExpireMinutes); logger.info("將查詢結果存儲到cache.key:" + strKey + ",value:" + value); getRedisTemplate().opsForValue().set(strKey, value, expireMinutes, TimeUnit.SECONDS); logger.debug("Put cache to redis, id={}", strKey); } catch (Exception e) { logger.error("Redis put failed, key=" + key.toString(), e); } } /** * 讀取緩存 * * @param key 其實就是sql語句 * @return 緩存結果 */ @Override public Object getObject(Object key) { try { String strKey = generateRedisKey(key); logger.debug("Get cache from redis, id={} key={}", id, strKey); return getRedisTemplate().opsForValue().get(strKey); } catch (Exception e) { logger.error("Redis get failed, fail over to db", e); return null; } } /** * 刪除緩存 * * @param key 其實就是sql語句 * @return 結果 */ @Override public Object removeObject(Object key) { try { String strKey = generateRedisKey(key); getRedisTemplate().delete(strKey); logger.debug("Remove cache from redis, id={}", id); } catch (Exception e) { logger.error("Redis remove failed", e); } return null; } /** * 緩存清理 * 應該是根據表空間進行清理 */ @Override public void clear() { try { logger.debug("clear cache, id={}", id); String hsKey = CACHE_NAME + id; // 獲取CacheNamespace所有緩存key Map<Object, Object> idMap = getRedisTemplate().opsForHash().entries(hsKey); if (!idMap.isEmpty()) { Set<Object> keySet = idMap.keySet(); Set<String> keys = new HashSet<>(keySet.size()); keySet.forEach(item -> keys.add(item.toString())); // 清空CacheNamespace下面所有緩存Key getRedisTemplate().delete(keys); // 清空CacheNamespace getRedisTemplate().delete(hsKey); } } catch (Exception e) { logger.error("clear cache failed", e); } } /** * 獲取緩存大小,暫時沒用上 * * @return 長度 */ @Override public int getSize() { return 0; } /** * 獲取讀寫鎖:爲了解決緩存擊穿 * mybites 3.2 不生效 * @return 鎖 */ @Override public ReadWriteLock getReadWriteLock() { return readWriteLock; } /** * 獲取Redis服務接口 * 使用雙重檢查保證線程安全 * * @return 服務實例 */ private RedisTemplate getRedisTemplate() { if (redisTemplate == null) { synchronized (RedisTemplate.class) { if (redisTemplate == null) { redisTemplate = (RedisTemplate) ApplicationContextHolder.getBean("redisTemplate"); } } } return redisTemplate; } private String generateRedisKey(Object key) { String o = this.id + DigestUtils.sha256Hex(key.toString().getBytes()); logger.info(o); return o; } public static byte[] append(byte[]... bas) { int iLen = 0; byte[][] var2 = bas; int var3 = bas.length; int var4; for (var4 = 0; var4 < var3; ++var4) { byte[] ba = var2[var4]; if (ba != null && ba.length > 0) { iLen += ba.length; } } byte[] result = new byte[iLen]; iLen = 0; byte[][] var8 = bas; var4 = bas.length; for (int var9 = 0; var9 < var4; ++var9) { byte[] ba = var8[var9]; if (ba != null && ba.length > 0) { System.arraycopy(ba, 0, result, iLen, ba.length); iLen += ba.length; } } return result; } public int getMinExpireMinutes() { return minExpireMinutes; } public MybatisRedisCache setMinExpireMinutes(int minExpireMinutes) { this.minExpireMinutes = minExpireMinutes; return this; } public int getMaxExpireMinutes() { return maxExpireMinutes; } public MybatisRedisCache setMaxExpireMinutes(int maxExpireMinutes) { this.maxExpireMinutes = maxExpireMinutes; return this; } }
爲什麼會記錄一下?
網絡上目前的都有坑。 這是基於我找到的最好的實例,再次修改的。
package com.zjs.remote.config.redis;
import com.zjs.remote.config.ApplicationContextHolder;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.RandomUtils;
import org.apache.ibatis.cache.Cache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;
import java.security.MessageDigest;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* MyBatis二級緩存Redis實現
* 重點處理以下幾個問題
* 1、緩存穿透:存儲空值解決,MyBatis框架實現
* 2、緩存擊穿:使用互斥鎖,我們自己實現
* 3、緩存雪崩:緩存有效期設置爲一個隨機範圍,我們自己實現
* 4、讀寫性能:redis key不能過長
*/
public class MybatisRedisCache implements Cache
{
private static final Logger logger = LoggerFactory.getLogger(MybatisRedisCache.class);
/**
* 統一緩存頭
*/
private static final String CACHE_NAME = "MyBatis:";
/**
* 讀寫鎖:解決緩存擊穿
*/
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
/**
* 表空間ID:方便後面的緩存清理
*/
private final String id;
/**
* redis服務接口:提供基本的讀寫和清理
*/
//private static volatile RedisService redisService;
//private RedisTemplate redisService; //(RedisTemplate) ApplicationContextHolder.getBean("redisTemplate");
//這裏使用了redis緩存,使用springboot自動注入
private RedisTemplate redisTemplate;
/**
* 信息摘要
*/
private volatile MessageDigest messageDigest;
/////////////////////// 解決緩存雪崩,具體範圍根據業務需要設置合理值 //////////////////////////
/**
* 緩存最小有效期
*/
private int minExpireMinutes = 60;
/**
* 緩存最大有效期
*/
private int maxExpireMinutes = 120;
/**
* MyBatis給每個表空間初始化的時候要用到
*
* @param id 其實就是namespace的值
*/
public MybatisRedisCache(String id) {
if (id == null) {
throw new IllegalArgumentException("Cache instances require an ID");
}
this.id = id;
}
/**
* 獲取ID
*
* @return 真實值
*/
@Override
public String getId() {
return id;
}
/**
* 創建緩存
*
* @param key 其實就是sql語句
* @param value sql語句查詢結果
*/
@Override
public void putObject(Object key, Object value) {
try {
String strKey = generateRedisKey(key);
// 在redis額外維護CacheNamespace創建的key,clear的時候只清理當前CacheNamespace的數據
getRedisTemplate().opsForHash().put(CACHE_NAME + id, strKey, "1");
// 有效期 隨機,防止雪崩
int expireMinutes = RandomUtils.nextInt(minExpireMinutes, maxExpireMinutes);
logger.info("將查詢結果存儲到cache.key:" + strKey + ",value:" + value);
getRedisTemplate().opsForValue().set(strKey, value, expireMinutes, TimeUnit.SECONDS);
logger.debug("Put cache to redis, id={}", strKey);
} catch (Exception e) {
logger.error("Redis put failed, key=" + key.toString(), e);
}
}
/**
* 讀取緩存
*
* @param key 其實就是sql語句
* @return 緩存結果
*/
@Override
public Object getObject(Object key) {
try {
String strKey = generateRedisKey(key);
logger.debug("Get cache from redis, id={} key={}", id, strKey);
return getRedisTemplate().opsForValue().get(strKey);
} catch (Exception e) {
logger.error("Redis get failed, fail over to db", e);
return null;
}
}
/**
* 刪除緩存
*
* @param key 其實就是sql語句
* @return 結果
*/
@Override
public Object removeObject(Object key) {
try {
String strKey = generateRedisKey(key);
getRedisTemplate().delete(strKey);
logger.debug("Remove cache from redis, id={}", id);
} catch (Exception e) {
logger.error("Redis remove failed", e);
}
return null;
}
/**
* 緩存清理
* 應該是根據表空間進行清理
*/
@Override
public void clear() {
try {
logger.debug("clear cache, id={}", id);
String hsKey = CACHE_NAME + id;
// 獲取CacheNamespace所有緩存key
Map<Object, Object> idMap = getRedisTemplate().opsForHash().entries(hsKey);
if (!idMap.isEmpty()) {
Set<Object> keySet = idMap.keySet();
Set<String> keys = new HashSet<>(keySet.size());
keySet.forEach(item -> keys.add(item.toString()));
// 清空CacheNamespace下面所有緩存Key
getRedisTemplate().delete(keys);
// 清空CacheNamespace
getRedisTemplate().delete(hsKey);
}
} catch (Exception e) {
logger.error("clear cache failed", e);
}
}
/**
* 獲取緩存大小,暫時沒用上
*
* @return 長度
*/
@Override
public int getSize() {
return 0;
}
/**
* 獲取讀寫鎖:爲了解決緩存擊穿
*
* @return 鎖
*/
@Override
public ReadWriteLock getReadWriteLock() {
return readWriteLock;
}
/**
* 獲取Redis服務接口
* 使用雙重檢查保證線程安全
*
* @return 服務實例
*/
private RedisTemplate getRedisTemplate() {
if (redisTemplate == null) {
synchronized (RedisTemplate.class) {
if (redisTemplate == null) {
redisTemplate = (RedisTemplate) ApplicationContextHolder.getBean("redisTemplate");
}
}
}
return redisTemplate;
}
private String generateRedisKey(Object key) {
String o = this.id + DigestUtils.sha256Hex(key.toString().getBytes());
logger.info(o);
return o;
}
public static byte[] append(byte[]... bas) {
int iLen = 0;
byte[][] var2 = bas;
int var3 = bas.length;
int var4;
for (var4 = 0; var4 < var3; ++var4) {
byte[] ba = var2[var4];
if (ba != null && ba.length > 0) {
iLen += ba.length;
}
}
byte[] result = new byte[iLen];
iLen = 0;
byte[][] var8 = bas;
var4 = bas.length;
for (int var9 = 0; var9 < var4; ++var9) {
byte[] ba = var8[var9];
if (ba != null && ba.length > 0) {
System.arraycopy(ba, 0, result, iLen, ba.length);
iLen += ba.length;
}
}
return result;
}
public int getMinExpireMinutes() {
return minExpireMinutes;
}
public MybatisRedisCache setMinExpireMinutes(int minExpireMinutes) {
this.minExpireMinutes = minExpireMinutes;
return this;
}
public int getMaxExpireMinutes() {
return maxExpireMinutes;
}
public MybatisRedisCache setMaxExpireMinutes(int maxExpireMinutes) {
this.maxExpireMinutes = maxExpireMinutes;
return this;
}
}