JDK动态代理实现Redis降级

需求说明

防止Redis服务不可用导致服务不可用,保证业务正常流程,在Redis服务崩掉后,走数据库进行加锁解锁操作,需要尽量不修改原有Redis代码

原有redis示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Slf4j
@Service
public class OldRedisService{

@Autowired
private RedisTemplate redisTemplate;

public Boolean set(String key, long expireTime){
log.info("进入redis方法内部");
return redisTemplate.opsForValue().setIfAbsent(key,1,expireTime, TimeUnit.SECONDS);
}

}

JDK动态代理实现

增加A接口

1
2
3
public interface RedisServiceI {
Boolean set(String key,long expireTime);
}

原有redis类实现A

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Slf4j
@Service
@Primary
public class OldRedisService implements RedisServiceI{

@Autowired
private RedisTemplate redisTemplate;

@Override
public Boolean set(String key, long expireTime){
log.info("进入redis方法内部");
return redisTemplate.opsForValue().setIfAbsent(key,1,expireTime, TimeUnit.SECONDS);
}

}

增加MySQL加锁类实现A接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Slf4j
@Service
public class DatabaseService implements RedisServiceI {

@Autowired
private TCacheInfoMapper tCacheInfoMapper;

@Override
public Boolean set(String key, long expireTime) {
log.info("进入database内部");
TCacheInfo tCacheInfo = new TCacheInfo();
tCacheInfo.setId(1515155L);
tCacheInfo.setCreateTime(LocalDateTime.now());
tCacheInfo.setUpdateTime(LocalDateTime.now());
tCacheInfo.setCacheKey(key);
tCacheInfo.setCacheValue(key);
tCacheInfo.setExpireTime(LocalDateTime.now());
tCacheInfo.setVersion(1);
return tCacheInfoMapper.insert(tCacheInfo) > 0;
}
}

对原有类进行代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Slf4j
@Component
public class GlobalConfiguration implements BeanPostProcessor {

@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof RedisServiceI && "oldRedisService".equals(beanName)) {
bean = getProxyBean(bean);
}
return bean;
}

private Object getProxyBean(Object bean) {
return Proxy
.newProxyInstance(
this.getClass().getClassLoader(),
bean.getClass().getInterfaces(),
new DynamicProxyHandler(bean)
);
}

}

调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Slf4j
public class DynamicProxyHandler implements InvocationHandler {

Object redisBean;
public DynamicProxyHandler(Object redisBean) {
this.redisBean = redisBean;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
log.info("代理开始执行:"+method.getName());
Object obj = null;
try {
//调用原有方法
obj = method.invoke(redisBean, args);
}catch (InvocationTargetException e){
if(e.getTargetException() instanceof BussinessException){
throw new BussinessException(ErrorCode.CA000001,"自定义的异常,需要抛出让全局异常处理");
}
//执行数据库加锁类的方法
RedisServiceI bean = (RedisServiceI) SpringContextUtil.getContext().getBean("databaseService");
return method.invoke(bean, args);
}
log.info("代理结束执行");
return obj;
}
}

使用方式

1
2
@Autowired
private RedisServiceI redisServiceI;

因为上面OldRedisService类使用了@Primary注解,所以会优先被注入到接口中

优化点

  • 上面的方式有个缺点,这样修改后,原来的OldRedisService类就不能被注入了,因为已经被代理了。可以优化一下,不代理原有的OldRedisService类,而是新写一个类,也实现RedisServiceI,新类所有方法全部调用OldRedisService,且使用@Primary保证被优先注入接口,最后代理这个新写的类即可。
  • 其实还有一个锁的数据一致性问题,在Redis加锁后,Redis崩了,是无法同步到数据库中的,这个处理比较麻烦,如果对业务要求比较高需要注意一下