需求背景
Spring cache是Spring提供的一个缓存框架,利用了AOP实现了基于注解的缓存功能。虽然使用方便,但有如下缺陷。
首先,针对不同业务,不支持手动设置不同的缓存过期时间,例如商品缓存想要30s过期,优惠券信息想要50s过期。
此外,缓存注解不能避免缓存雪崩的场景,需要借助额外的编码才能实现。
目前项目的部分业务涉及到不同维度的数据,每种维度数据需要不同的缓存过期时间,而且也会有缓存雪崩场景。为了满足这些场景,当前做法是在Spring cache之上做了大量编码工作,增加了研发和维护成本。
所以,我们希望自己实现一个可以满足上述需求的缓存切面框架,通过注解方式一键满足缓存的灵活需求。
目标
在Spring cache之上实现自定义缓存切面框架,一键设置缓存注解,减少研发成本。
代码开发步骤
1.自定义注解
EasyCache表示使用的缓存名称、缓存key、缓存过期时间等
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EasyCache {
String cacheName() default "redisCache";
String cacheKey();
int expire() default -1;
Lock lock() default @Lock;
}
Lock表示使用哪种锁(默认redis)、锁的key、失效时间
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Lock {
String lockType() default "redis";
String key() default "";
int expire() default 60;
}
2.自定义实体类
EasyCacheBean
@Data
public class EasyCacheBean {
private String cacheName;
private String cacheKey;
private int expire;
private Method method;
private LockBean lockBean;
}
LockBean
@Data
public class LockBean {
private String lockType;
private String key;
private String value;
private int expire;
}
3.自定义缓存操作接口
Cache接口
public interface Cache {
String getName();
<T> T get(String key);
void put(String key, Object value);
void put(String key, Object value,int expire);
void evict(String key);
}
Cache接口的redis实现类
public class RedisCache implements Cache {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public String getName() {
return "redisCache";
}
@Override
public <T> T get(String key) {
return (T)redisTemplate.opsForValue().get(key);
}
@Override
public void put(String key, Object value) {
redisTemplate.opsForValue().set(key, value);
}
@Override
public void put(String key, Object value, int expire) {
redisTemplate.opsForValue().set(key, value, expire, TimeUnit.SECONDS);
}
@Override
public void evict(String key) {
}
}
4.自定义缓存管理器接口
Cache接口理论上可以有多个实现类,即有多种缓存方式。所以我们需要一个缓存管理器,维护所有Cache接口的实现类,当指定使用哪种缓存时,就从管理器中获取。
CacheManager接口
public interface CacheManager {
Cache getCache(String cacheName);
}
CacheManager接口的默认实现类DefaultCacheManager
@Component
public class DefaultCacheManager implements CacheManager, InitializingBean, ApplicationContextAware {
private ApplicationContext applicationContext;
private ConcurrentHashMap<String, Cache> caches = new ConcurrentHashMap();
@Override
public Cache getCache(String cacheName) {
return caches.get(cacheName);
}
@Override
public void putCache(String cacheName, Cache cache) {
}
@Override
public void afterPropertiesSet() throws Exception {
Map<String, Cache> cacheBeans = applicationContext.getBeansOfType(Cache.class);
for (String s : cacheBeans.keySet()) {
caches.put(cacheBeans.get(s).getName(),cacheBeans.get(s));
}
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
5.自定义锁操作接口
Lock接口,包含加锁和解锁
public interface Lock {
String getName();
boolean doLock(LockBean lockBean);
boolean unlock(LockBean lockBean);
}
我们定义两种锁的实现类:redis锁和jdk内置锁
5.1RedisLock
@Slf4j
@Component
public class RedisLock implements Lock {
private static final String SUCCESS = "OK";
private static final GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer();
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public String getName() {
return "redis";
}
public boolean lock(LockBean lockBean) {
try {
String ret = redisTemplate.execute((RedisCallback<String>) connection -> {
Object nativeConnection = connection.getNativeConnection();
if (nativeConnection instanceof RedisAsyncCommands) {
RedisAsyncCommands commands = (RedisAsyncCommands) nativeConnection;
//同步方法执行、setnx禁止异步
return commands.getStatefulConnection().sync().set(serializer.serialize(lockBean.getKey()), serializer.serialize(lockBean.getValue()), SetArgs.Builder.nx().ex(lockBean.getExpire()));
}
if (nativeConnection instanceof RedisAdvancedClusterAsyncCommands) {
RedisAdvancedClusterAsyncCommands clusterAsyncCommands = (RedisAdvancedClusterAsyncCommands) nativeConnection;
return clusterAsyncCommands.getStatefulConnection().sync().set(serializer.serialize(lockBean.getKey()), serializer.serialize(lockBean.getValue()), SetArgs.Builder.nx().ex(lockBean.getExpire()));
}
return null;
});
return SUCCESS.equals(ret);
} catch (Exception e) {
log.error("get lock error ,lock:{}", lockBean.getKey(), e);
return false;
}
}
@Override
public boolean doLock(LockBean lockBean) {
Random random = new Random();
while (true) {
if(lock(lockBean)) {
return true;
}
log.info("等待线程" + Thread.currentThread().getName());
try {
Thread.sleep(random.nextInt(100));
} catch (InterruptedException e) {
log.error("获取分布式锁休眠被中断:", e);
Thread.currentThread().interrupt();
}
}
}
@Override
public boolean unlock(LockBean lockBean) {
try {
byte[] keyByte = serializer.serialize(lockBean.getKey());
return redisTemplate.execute((RedisCallback<Boolean>) connection -> {
Long result = connection.del(keyByte);
return result != null && result == 1L;
});
} catch (Exception e) {
log.error("get lock error ,lock:{}", lockBean.getKey(), e);
return false;
}
}
}
5.2JdkLock
@Component
public class JdkLock implements Lock {
private ConcurrentHashMap<String, ReentrantLock> lockMap = new ConcurrentHashMap<>();
@Override
public String getName() {
return "jdk";
}
@Override
public boolean doLock(LockBean lockBean) {
ReentrantLock reentrantLock = new ReentrantLock();
ReentrantLock oldLock = lockMap.putIfAbsent(lockBean.getKey(), reentrantLock);
if(oldLock != null){
oldLock.lock();
}else {
reentrantLock.lock();
}
return true;
}
@Override
public boolean unlock(LockBean lockBean) {
ReentrantLock reentrantLock = lockMap.get(lockBean.getKey());
reentrantLock.unlock();
return true;
}
}
6.自定义锁管理接口
同Cache接口一样,Lock接口理论上可以有多个实现类,即有多种锁的方式。所以我们需要一个管理器,维护所有Lock接口的实现类。
LockManager
public interface LockManager {
Lock getLock(String name);
}
LockManager 默认实现类DefaultLockManager
@Component
public class DefaultLockManager implements LockManager, InitializingBean, ApplicationContextAware {
private ApplicationContext applicationContext;
private ConcurrentHashMap<String,Lock> locks = new ConcurrentHashMap<>();
@Override
public Lock getLock(String name) {
return locks.get(name);
}
@Override
public void putLock(String name, Lock lock) {
locks.put(name,lock);
}
@Override
public void afterPropertiesSet() throws Exception {
Map<String, Lock> lockbeans = applicationContext.getBeansOfType(Lock.class);
for (Map.Entry<String, Lock> entry : lockbeans.entrySet()) {
locks.put(entry.getValue().getName(),entry.getValue());
}
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
7.自定义Advisor相关
下面就是自定义缓存切面的核心逻辑了,我们需要借助spring aop中的Advisor组件。
Advisor的介绍可参考我的另一篇博客聊聊spring aop中的advisor组件。
7.1 EasyCacheMethodMatcher
用于判断类中的方法是否有@EasyCache注解,如果有则获取注解信息,并包装为
EasyCacheBean,加入自身缓存
@Component
public class EasyCacheMethodMatcher implements MethodMatcher {
private static ConcurrentHashMap<MethodClassKey, EasyCacheBean> cache = new ConcurrentHashMap(1024);
@Override
public boolean matches(Method method, Class<?> targetClass) {
MethodClassKey key = new MethodClassKey(method, targetClass);
EasyCacheBean existsBean = cache.get(key);
if(existsBean != null){
return true;
}
Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
if(AnnotatedElementUtils.hasAnnotation(specificMethod, EasyCache.class)){
EasyCacheBean easyCacheBean = new EasyCacheBean();
EasyCache easyCacheAnno = AnnotationUtils.getAnnotation(specificMethod, EasyCache.class);
easyCacheBean.setCacheName(easyCacheAnno.cacheName());
easyCacheBean.setExpire(easyCacheAnno.expire());
easyCacheBean.setCacheKey(easyCacheAnno.cacheKey());
Lock lockAnno = easyCacheAnno.lock();
LockBean lockBean = new LockBean();
lockBean.setExpire(lockAnno.expire());
lockBean.setKey(lockAnno.key());
lockBean.setLockType(lockAnno.lockType());
easyCacheBean.setLockBean(lockBean);
easyCacheBean.setMethod(specificMethod);
cache.putIfAbsent(key, easyCacheBean);
return true;
}
return false;
}
@Override
public boolean isRuntime() {
return false;
}
@Override
public boolean matches(Method method, Class<?> targetClass, Object... args) {
return false;
}
public static EasyCacheBean getCache(Method method, Class<?> targetClass){
return cache.get(new MethodClassKey(method, targetClass));
}
}
7.2 EasyCachePointcut
即spring中的Pointcut组件,需要ClassFilter和刚才的MethodMatcher。此处ClassFilter.TRUE表示所有类都符合条件
public class EasyCachePointcut implements Pointcut {
@Override
public ClassFilter getClassFilter() {
return ClassFilter.TRUE;
}
@Override
public MethodMatcher getMethodMatcher() {
return new EasyCacheMethodMatcher();
}
}
7.3 EasyCacheAdvice
光有Pointcut还不行,我们还需要Advice
@Component
public class EasyCacheAdvice implements MethodInterceptor {
@Autowired
private CacheManager cacheManager;
@Autowired
private LockManager lockManager;
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
Class<?> targetClass = invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null;
EasyCacheBean easyCacheBean = EasyCacheMethodMatcher.getCache(invocation.getMethod(), targetClass);
//缓存数据的key,根据方法和参数信息生成
String cacheKey = getCacheKey(invocation, easyCacheBean);
//获取缓存组件
Cache cache = cacheManager.getCache(easyCacheBean.getCacheName());
if(cache.get(cacheKey) != null){
return cache.get(cacheKey);
}
LockBean lockBean = easyCacheBean.getLockBean();
//重新设置锁的key
lockBean.setKey(getLockKey(invocation, easyCacheBean));
//设置锁的key的值
lockBean.setValue(System.currentTimeMillis() + "");
//获取锁
Lock lock = lockManager.getLock(lockBean.getLockType());
Object result = null;
try {
//加锁
lock.doLock(lockBean);
if(cache.get(cacheKey) != null){
return cache.get(cacheKey);
}
//执行业务方法
result = invocation.proceed();
if(result != null){
//是否设置缓存数据的过期时间
if(easyCacheBean.getExpire() == -1){
cache.put(cacheKey, result);
}else {
cache.put(cacheKey, result, easyCacheBean.getExpire());
}
}
} finally {
lock.unlock(lockBean);
}
return result;
}
public String getCacheKey(MethodInvocation invocation, EasyCacheBean easyCacheBean){
//获取到参数名称
String[] parameterNames = new DefaultParameterNameDiscoverer().getParameterNames(easyCacheBean.getMethod());
return ElParser.getKey(easyCacheBean.getCacheKey(), parameterNames, invocation.getArguments());
}
public String getLockKey(MethodInvocation invocation, EasyCacheBean easyCacheBean){
//获取到参数名称
String[] parameterNames = new DefaultParameterNameDiscoverer().getParameterNames(easyCacheBean.getMethod());
return ElParser.getKey(easyCacheBean.getLockBean().getKey(), parameterNames, invocation.getArguments());
}
}
其中invoke方法我们需要获取缓存key和锁的key,利用的是spring的el表达式解析。工具类如下
public class ElParser {
private static ExpressionParser parser = new SpelExpressionParser();
public static String getKey(String key,String[] paramNames,Object[] args) {
//#areaCode
Expression expression = parser.parseExpression(key);
StandardEvaluationContext context = new StandardEvaluationContext();
if(args.length <= 0) {
return null;
}
for (int i = 0; i < args.length; i++) {
context.setVariable(paramNames[i],args[i]);
}
return expression.getValue(context,String.class);
}
}
7.4 EasyCacheAdvisor
有了Pointcut和Advice,我们就可以组成Advisor了
@Component
public class EasyCacheAdvisor implements PointcutAdvisor {
@Autowired
private EasyCacheAdvice easyCacheAdvice;
@Override
public Pointcut getPointcut() {
return new EasyCachePointcut();
}
@Override
public Advice getAdvice() {
return easyCacheAdvice;
}
@Override
public boolean isPerInstance() {
return false;
}
}
至此,自定义缓存切面的组件就编写完成了。
项目结构
使用方法
在项目中某段测试方法上加注解
@EasyCache(cacheKey = "#name", expire = 60, lock = @Lock(key = "'test1'+#name"))
public String test1(String name){
return "hello" + name;
}
包装为springboot-starter
如果我们仅仅只是在内部项目中使用,那么上述过程足够了。但如果我们希望将其变为jar包,共享给公司其他组使用,我们可以包装为springboot-starter。
1.编写配置类
EasyCacheConfig
@Configuration
public class EasyCacheConfig {
@Bean
public EasyCacheAdvice easyCacheAdvice(){
return new EasyCacheAdvice();
}
@Bean
public EasyCacheAdvisor easyCacheAdvisor(){
return new EasyCacheAdvisor();
}
@Bean
public CacheManager defaultCacheManager(){
return new DefaultCacheManager();
}
@Bean
public LockManager defaultLockManager(){
return new DefaultLockManager();
}
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate();
template.setConnectionFactory(lettuceConnectionFactory);
//key序列化
template.setKeySerializer(new StringRedisSerializer());
//value序列化
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
template.afterPropertiesSet();
return template;
}
@Bean
public Lock redisLock(){
return new RedisLock();
}
@Bean
public Lock jdkLock(){
return new JdkLock();
}
@Bean
public Cache redisCache(){
return new RedisCache();
}
}
2.创建创建spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.sf.easycache.config.EasyCacheConfig
最终结构如下
3.maven install到本地仓库或者公司的私库
4.使用自定义的starter
在公司的项目中,引入打好的依赖
<dependency>
<groupId>com.sf</groupId>
<artifactId>easy-cache</artifactId>
<version>1.0.0</version>
</dependency>