在 Spring Boot 项目中使用 Redisson 实现双写一致性(即数据库和缓存的一致性),可以通过自定义注解和 AOP(面向切面编程)来简化代码并提高可维护性。以下是一个具体的案例,展示了如何使用自定义注解和 AOP 来实现这一目标。
实现步骤
1.添加依赖:
首先,确保你的项目中包含了 Redisson 和 Spring Boot 的依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.16.3</version>
</dependency>
2.配置 Redisson:
在 application.yml
中配置 Redisson 客户端连接到 Redis 服务器。
spring:
redis:
host: localhost
port: 6379
redisson:
singleServerConfig:
address: redis://${spring.redis.host}:${spring.redis.port}
3.添加配置类:
@Configuration
public class RedissonConfig {
@Value("${redisson.singleServerConfig.address}")
private String redisAddress;
@Bean(destroyMethod = "shutdown")
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer().setAddress(redisAddress);
return Redisson.create(config);
}
}
4.自定义注解:
创建一个自定义注解,另一个注解还用上篇的
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface CustomerCacheEvict {
/**
* 缓存名称
* @return
*/
String key() default "";
}
5.AOP 切面:
创建一个切面类 CustomCacheAspect
,使用 Redisson 的读写锁来实现缓存和数据库的一致性。
@Aspect
@Component
public class CustomCacheAspect {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private RedissonClient redissonClient;
@Around("@annotation(customCacheable)")
public Object cache(ProceedingJoinPoint joinPoint, CustomCacheable customCacheable) throws Throwable {
Object result;
String key = customCacheable.key();
long expireTime = customCacheable.expireTime();
//获取参数值
key = getKey(joinPoint, key);
// 加锁
RLock rLock = redissonClient.getReadWriteLock(key+":lock").readLock();
rLock.lock();
try {
// 尝试从缓存中获取数据
Object cachedValue = redisTemplate.opsForValue().get(key);
if (cachedValue != null) {
return cachedValue;
}
// 如果缓存中没有数据,则执行方法并将结果存入缓存
result = joinPoint.proceed();
if(expireTime > 0){
redisTemplate.opsForValue().set(key, result, expireTime, TimeUnit.SECONDS);
}else{
redisTemplate.opsForValue().set(key, result);
}
}finally{
if(rLock.isHeldByCurrentThread()){
rLock.unlock();
}
}
return result;
}
@Around("@annotation(customerCacheEvict)")
public Object cache(ProceedingJoinPoint joinPoint, CustomerCacheEvict customerCacheEvict) throws Throwable {
Object result;
String key = customerCacheEvict.key();
//获取参数值
key = getKey(joinPoint, key);
// 加锁
RLock wLock = redissonClient.getReadWriteLock(key + ":lock").writeLock();
wLock.lock();
try {
result = joinPoint.proceed();
try {
Thread.sleep(100);
}catch (Exception e){
e.printStackTrace();
}
redisTemplate.delete(key);
}finally{
if(wLock.isHeldByCurrentThread()){
wLock.forceUnlock();
}
}
return result;
}
/**
* 获取参数值
* @return
*/
private String getKey(ProceedingJoinPoint joinPoint, String key) {
Object[] args = joinPoint.getArgs();
// 构建缓存键
if (args != null && args.length > 0) {
key = key + ":" + args[0];
}
return key;
}
}
6.服务类:
在接口类中使用自定义注解。(实际加到service 层更好)
/**
* 根据id查询
*/
@GetMapping("/{id}")
@CustomCacheable(key = "user")
public UserTest getById(@PathVariable Long id) {
return userTestService.getById(id);
}
/**
* 修改
*/
@PutMapping("/{id}")
@CustomerCacheEvict(key="user")
public boolean update(@PathVariable Integer id, @RequestBody UserTest userTest) {
userTest.setId(id);
return userTestService.updateById(userTest);
}
通过以上步骤,我们使用自定义注解和 AOP 确保了数据库和 Redis 缓存之间的一致性。