redis模式匹配的坑及应对之道

1 篇文章 0 订阅

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 *的命令。

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值