redis是高性能的key-value数据库。我一直把它作为mysql缓存使用。效果也不错。直到有一天,运维跑过来对我说,整个系统挂掉了,把我的系统停掉,其他系统才会正常,而且我系统跑得的时候,发现redis的CPU占用很高。
真是奇怪了,之前一直好好的,今天怎么了,而且是白天低峰期。后来听说,商务通系统(别的一个系统)早上上了一个版本,默认把我的系统数据上传开启。一想明白了,都是redis的模式删除的锅。这样商品数据会一下子上传,而我在商品上传协议中,做清除商品缓存这种事情,里头有这种代码。
private static final String script = "local keys = redis.call('KEYS', ARGV[1]); local keysCount = table.getn(keys); if(keysCount > 0) then for _, key in ipairs(keys) do redis.call('del', key); end; end; return keysCount;";
String keyPattern = redisService.getRealKey(CacheConstants.CACHE_COMPANY + "*");
Long value = redisService.eval(script, Long.class, 0, keyPattern);
这块代码,就是把前缀相同的缓存删除掉,就算没有key可删除,也是相当消耗资源。
一两个商家做这种事情,如果有几十个商家一起做,就有大问题,引发redis崩溃,由于大家现在共用一个redis,别人也用不了redis,引发雪崩了。redis模式匹配keys命令效率低,很多文章都说明,没想到会发生在我身上,引发的后果这么严重。想到这,赶紧上一个版本,把这个代码注释掉,redis的CPU马上下降下来,系统正常了。
但是注释代码只是权宜之计,由于博主的做的系统,实时要求性极高。要求数据一变动,要马上更新,这势必要清除缓存。缓存又不可能都写到一个key中。网上都说写一个set来保存key, 这样先查set,再去删除。这得多麻烦。后来我想了一个办法,使用redis的hash,一个商家一个hash,这样清除缓存的时候,只要删除一个hash就行。redis的key最后长成这样。
由于spring自的cacheble注解,实在难以支持这样的结构 ,我就写了一个自定义的缓存注解
MyCacheable来支持这样的结构,上代码。
注解MyCacheable
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
public @interface MyCacheable {
String cacheName() default "";
long cacheExpireSecond() default 7200;
/**
* 是否方法名称也放到缓存子key中
* @author deng
* @date 2018年6月13日
* @return
*/
boolean methodNameSub() default false;
AOP切面
@Aspect
@Component
public class MyCacheAspect {
@Autowired
private MyCacheAspectService myCacheAspectService;
@Around("@annotation(myCacheable) ")
public Object cache(ProceedingJoinPoint pjp, MyCacheable myCacheable) throws Throwable {
return myCacheAspectService.cacheProceed(pjp, myCacheable);
}
}
具体处理redis保存数据的业务类, 其实就是处理key的生成,分成没有参数,一个参数,多个参数的情况。没有参数,就要把方法名称作key一部分。也是参考了Cacheable的实现
@Aspect
@Component
public class MyCacheAspect {
@Autowired
private MyCacheAspectService myCacheAspectService;
@Around("@annotation(myCacheable) ")
public Object cache(ProceedingJoinPoint pjp, MyCacheable myCacheable) throws Throwable {
return myCacheAspectService.cacheProceed(pjp, myCacheable);
}
}
@Service
public class MyCacheAspectServiceImpl implements MyCacheAspectService {
@Autowired
private RedisService redisService;
@Override
public Object cacheProceed(ProceedingJoinPoint pjp, MyCacheable myCacheable) throws Throwable {
Object[] args = pjp.getArgs();
// 没有参数
if (args == null || args.length == 0) {
return noParamcacheProceed(pjp, myCacheable);
}
// 一个参数
if (args != null && args.length == 1) {
return oneParamcacheProceed(pjp, myCacheable);
}
if (args != null && args.length >= 2) {
return twoParamcacheProceed(pjp, myCacheable);
}
return pjp.proceed(args);
}
/**
* 生成子key
*
* @author deng
* @date 2018年6月11日
* @param args
* @return
*/
private Object[] getSubKey(Object[] args) {
if (args != null && args.length >= 2) {
Object[] subArgs = new Object[args.length - 1];
for (int i = 1; i < args.length; i++) {
subArgs[i - 1] = args[i];
}
return subArgs;
}
return args;
}
/**
* 整合Key
*
* @author deng
* @date 2018年10月23日
* @param arg1
* @param args2
* @return
*/
private Object[] getSubKey(Object arg1, Object[] args2) {
if (args2 != null && args2.length >= 2) {
Object[] subArgs = new Object[args2.length];
subArgs[0] = arg1;
for (int i = 1; i < args2.length; i++) {
subArgs[i] = args2[i];
}
return subArgs;
}
return args2;
}
/**
* 两个以上参数处理
*
* @author deng
* @date 2018年6月13日
* @param pjp
* @param myCacheable
* @return
* @throws Throwable
*/
private Object twoParamcacheProceed(ProceedingJoinPoint pjp, MyCacheable myCacheable) throws Throwable {
Object[] args = pjp.getArgs();
String cacheName = myCacheable.cacheName();
long expireSecond = myCacheable.cacheExpireSecond();
Object keyArgs = args[0];
String key = RedisUtil.mergeKey(cacheName, keyArgs);
String subKey = null;
boolean methodNameSub = myCacheable.methodNameSub();
if (methodNameSub) {
MethodSignature signature = (MethodSignature) pjp.getSignature();
subKey = RedisUtil.mergeKey(getSubKey(signature.getMethod().getName(), args));
} else {
subKey = RedisUtil.mergeKey(getSubKey(args));
}
return getCache(key, subKey, expireSecond, pjp);
}
private Object getCache(String key, String subKey, long expireSecond, ProceedingJoinPoint pjp) throws Throwable {
MethodSignature signature = (MethodSignature) pjp.getSignature();
Class<?> returnType = signature.getMethod().getReturnType();
Object[] args = pjp.getArgs();
Object value = redisService.hgetObjectMapper(key, subKey, returnType);
if (value != null) {
return value;
} else {
value = pjp.proceed(args);
redisService.hsetObjectMapper(key, subKey, value, expireSecond);
return value;
}
}
private Object noParamcacheProceed(ProceedingJoinPoint pjp, MyCacheable myCacheable) throws Throwable {
MethodSignature signature = (MethodSignature) pjp.getSignature();
String cacheName = myCacheable.cacheName();
long expireSecond = myCacheable.cacheExpireSecond();
if (CommonUtils.isEmpty(cacheName)) {
cacheName = getCacheConfigCacheName(pjp);
}
String key = cacheName;
String subKey = signature.getMethod().getName();
return getCache(key, subKey, expireSecond, pjp);
}
private CacheConfig getCacheConfig(ProceedingJoinPoint pjp) {
Class<?> classTarget = pjp.getTarget().getClass();
return classTarget.getAnnotation(CacheConfig.class);
}
private String getCacheConfigCacheName(ProceedingJoinPoint pjp) {
return getCacheConfig(pjp).cacheNames()[0];
}
/**
* 只有一个参数的
*
* @author deng
* @date 2018年6月13日
* @param pjp
* @param myCacheable
* @return
* @throws Throwable
*/
private Object oneParamcacheProceed(ProceedingJoinPoint pjp, MyCacheable myCacheable) throws Throwable {
Object[] args = pjp.getArgs();
String cacheName = myCacheable.cacheName();
if (CommonUtils.isEmpty(cacheName)) {
cacheName = getCacheConfigCacheName(pjp);
}
long expireSecond = myCacheable.cacheExpireSecond();
boolean methodNameSub = myCacheable.methodNameSub();
MethodSignature signature = (MethodSignature) pjp.getSignature();
String subKey = null;
String key = null;
Object keyArgs = args[0];
// 方法名称放到子Key中去
if (methodNameSub) {
key = cacheName;
subKey = RedisUtil.mergeKey(signature.getMethod().getName(), keyArgs);
} else {
key = RedisUtil.mergeKey(cacheName, keyArgs);
subKey = signature.getMethod().getName();
}
return getCache(key, subKey, expireSecond, pjp);
}
具体使用注解MyCacheable的例子
@Service
@CacheConfig(cacheNames = CacheConstants.CACHE_PARAMETER)
public class ParameterServiceImpl implements ParameterService {
@Override
@MyCacheable
public String km_staffpay() {
return this.getParameterValue(KM_STAFFPAY);
}
@MyCacheable
@Override
public String mktrouteAggregation(int companyid) {
Parameter parameter = parameterRepository.findFirstByCompanyIDAndParameterName(companyid, MKTROUTE_AGGREGATION);
if (parameter != null) {
return parameter.getParameterValue();
}
return "111";
}
}
最后写点工具类,主要是key和value的序列化的方法,默认的序列化,是对象序列化,不好看,我这边改成使用ObjectMapper来序列化,就是改成json保存
@Override
public void hsetObjectMapper(String key, Object subKey, Object value, long expirSeconds) {
stringRedisTemplate.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
byte[] byteKey = stringRedisTemplate.getStringSerializer().serialize(getRealKey(key));
connection.hSet(byteKey, stringRedisTemplate.getStringSerializer().serialize(String.valueOf(subKey)),
RedisUtil.serialize(value));
expire(connection, byteKey, expirSeconds);
return true;
}
});
}
/**
* 使用ObjectMapper来序列化对象
* @author deng
* @date 2018年6月19日
* @param object
* @return
*/
public static byte[] serialize(Object object) {
try {
return mapper.writeValueAsBytes(object);
} catch (JsonProcessingException e) {
new WjRuntimeException(e);
}
return null;
}
总结一下,在环境中尽量不要把相关key写到一个key中,其中hash是可以有一个子key,可以考虑使用,这样清除缓存的时候只要删除一个key就行,不要去使用keys *的命令。