使用Spring缓存注解操作Redis
为了进一步简化Redis的使用,Spring还提供了缓存注解,使用这些注解可以有效简化编程过程。
一、缓存管理器和缓存的启用
Spring在使用缓存注解前,需要配置缓存管理器,如缓存类型、超时时间等。
Spring可以支持多种缓存的使用,提供了缓存处理器的接口CacheManager和与之相关的类,使用Redis,主要就是以使用类RedisCacheManager为主。
在Spring Boot的starter机制中,允许我们通过配置文件生成缓存管理器,它提供的配置如下。
缓存管理器配置:
# SPRING CACHE (CacheProperties)
spring.cache.cache-names= # 如果由底层的缓存管理器支持创建,以逗号分隔的列表来缓存名称
spring.cache.caffeine.spec= # caffeine缓存配置细节
spring.cache.couchbase.expiration=0ms # couchbase缓存超时时间,默认是永不超时
spring.cache.ehcache.config= # 配置ehcache缓存初始化文件路径
spring.cache.infinispan.config= #infinispan缓存配置文件
spring.cache.jcache.config= #jcache缓存配置文件
spring.cache.jcache.provider= #jcache缓存提供者配置
spring.cache.redis.cache-null-values=true # 是否允许Redis缓存空值
spring.cache.redis.key-prefix= # Redis的键前缀
spring.cache.redis.time-to-live=0ms # 缓存超时时间戳,配置为0则不设置超时时间
spring.cache.redis.use-key-prefix=true # 是否启用Redis的键前缀
spring.cache.type= # 缓存类型,在默认的情况下,Spring会自动根据上下文探测
因为使用的是Redis,所以其他的缓存并不需要我们去关注,这里只是关注第1个和最后的5个配置项。下面我们可以在application.properties配置Redis的缓存管理器。
配置Redis缓存管理器:
spring.cache.type=REDIS
spring.cache.cache-names=redisCache
spring.cache.type:缓存类型,为Redis,Spring Boot会自动生成RedisCacheManager对象,
spring.cache.cache-names:缓存名称,多个名称可以使用逗号分隔,以便于缓存注解的引用。
启用缓存机制:加入驱动缓存的注解**@EnableCaching**
@SpringBootApplication(scanBasePackages = "com.springboot.chapter7")
@MapperScan(basePackages = "com.springboot.chapter7", annotationClass = Repository.class)
@EnableCaching
public class Chapter7Application {
......
}
二、开发缓存注解
1.配置文件配置
#数据库配置
spring.datasource.url=jdbc:mysql://localhost:3306/spring_boot_chapter7
spring.datasource.username=root
spring.datasource.password=123456
#可以不配置数据库驱动,Spring Boot会自己发现
#spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.tomcat.max-idle=10
spring.datasource.tomcat.max-active=50
spring.datasource.tomcat.max-wait=10000
spring.datasource.tomcat.initial-size=5
#设置默认的隔离级别为读写提交
spring.datasource.tomcat.default-transaction-isolation=2
#mybatis配置
mybatis.mapper-locations=classpath:com/springboot/chapter7/mapper/*.xml
mybatis.type-aliases-package=com.springboot.chapter7.pojo
#日志配置为DEBUG级别,这样日志最为详细
logging.level.root=DEBUG
logging.level.org.springframework=DEBUG
logging.level.org.org.mybatis=DEBUG
#Redis配置
spring.redis.jedis.pool.min-idle=5
spring.redis.jedis.pool.max-active=10
spring.redis.jedis.pool.max-idle=10
spring.redis.jedis.pool.max-wait=2000
spring.redis.port=6379
spring.redis.host=192.168.11.131
spring.redis.password=123456
#缓存配置
spring.cache.type=REDIS
spring.cache.cache-names=redisCache
2.用户POJO:需要实现了Serializable接口。
package com.springboot.chapter2.common;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
/**
* AppUser对象
*/
@Data
@TableName("tms_app_user")
public class AppUserEntity implements Serializable {
@TableId(value = "id")
private Integer id;
/**
* 姓名
**/
private String name;
/**
* 手机
**/
private String phone;
...
}
3.MyBatis用户操作接口
package com.springboot.chapter2.redis6;
import com.springboot.chapter2.common.AppUserEntity;
import org.apache.ibatis.annotations.*;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface RedisUserDao {
// 获取单个用户
@Select("select * from tms_app_user where id = #{id}")
AppUserEntity getUser(Long id);
// 保存用户
@Update("insert into tms_app_user (name,phone) Values ( #{user.name},#{user.phone})")
@Options(useGeneratedKeys = true, keyProperty = "user.id")
int insertUser(@Param("user") AppUserEntity user);
// 修改用户
@Update("update tms_app_user set name = #{user.name} where id =#{user.id}")
int updateUser(@Param("user") AppUserEntity user);
// 查询用户,指定MyBatis的参数名称
@Select("select * from tms_app_user where userName = #{userName} and phone = #{phone}")
List<AppUserEntity> findUsers(@Param("userName") String userName,
@Param("phone") String phone);
// 删除用户
@Delete("delete from tms_app_user where id =#{id}")
int deleteUser(Long id);
}
注解@Repository标识它是DAO层,将来可以定义扫描来使得这个接口被扫描为Spring的Bean装配到IoC容器中;
这里还可以看到增删查改的方法,通过它们就可以测试Spring的缓存注解了,
4.用户服务接口
package com.springboot.chapter2.redis6;
import com.springboot.chapter2.common.AppUserEntity;
import java.util.List;
public interface RedisUserService {
// 获取单个用户
AppUserEntity getUser(Long id);
// 保存用户
AppUserEntity insertUser(AppUserEntity user);
// 修改用户,指定MyBatis的参数名称
AppUserEntity updateUserName(Long id, String userName);
// 查询用户,指定MyBatis的参数名称
List<AppUserEntity> findUsers(String userName, String phone);
// 删除用户
int deleteUser(Long id);
}
5.用户实现类使用Spring缓存注解
package com.springboot.chapter2.redis6;
import com.springboot.chapter2.common.AppUserEntity;
import org.apache.ibatis.annotations.Select;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class RedisUserServiceImpl implements RedisUserService {
@Autowired
private RedisUserDao redisUserDao = null;
// 插入用户,最后MyBatis会回填id,取结果id缓存用户
@Override
@Transactional
@CachePut(value ="redisCache", key = "'redis_user_'+#result.id")
public AppUserEntity insertUser(AppUserEntity user) {
redisUserDao.insertUser(user);
return user;
}
// 获取id,取参数id缓存用户
@Override
@Cacheable(value ="redisCache", key = "'redis_user_'+#id")
public AppUserEntity getUser(Long id) {
return redisUserDao.getUser(id);
}
// 更新数据后,更新缓存,如果condition配置项使结果返回为null,不缓存
@Override
@Transactional
@CachePut(value ="redisCache",
condition="#result != 'null'", key = "'redis_user_'+#id")
public AppUserEntity updateUserName(Long id, String userName) {
// 此处调用getUser方法,该方法缓存注解失效,
// 所以这里还会执行SQL,将查询到数据库最新数据
AppUserEntity user =this.getUser(id);
if (user == null) {
return null;
}
user.setName(userName);
redisUserDao.updateUser(user);
return user;
}
// 命中率低,所以不采用缓存机制
@Override
public List<AppUserEntity> findUsers(String userName, String note) {
return redisUserDao.findUsers(userName, note);
}
// 移除缓存
@Override
@Transactional
@CacheEvict(value ="redisCache", key = "'redis_user_'+#id",
beforeInvocation = false)
public int deleteUser(Long id) {
return redisUserDao.deleteUser(id);
}
}
关注下注解@CachePut、@Cacheable和@CacheEvict。
•@CachePut表示将方法结果返回存放到缓存中。
•@Cacheable 表示先从缓存中通过定义的键查询,如果可以查询到数据,则返回,否则执行该方法,返回数据,并且将返回结果保存到缓存中。
•@CacheEvict 通过定义的键移除缓存,它有一个Boolean类型的配置项beforeInvocation,表示在方法之前或者之后移除缓存。因为其默认值为false,所以默认为方法后将缓存移除。
其次,关注下配置value =“redisCache”,因为我们在Spring Boot中配置了对应的缓存名称为“redisCache”,这样它就能够引用到对应的缓存了,而键配置项则是一个Spring EL,很多时候可以看到配置为’redis_user_’+#id,其中#id代表参数,它是通过参数名称来匹配,所以这样配置要求方法存在一个参数且名称为id,除此之外还可以这样引用参数,如#a[0]或者#p[0]代表第一个参数,#a[1]或者#p[1]代表第二个参数……但是这样引用可读性较差,所以我们一般不这么写,通过这样定义,
Spring就会用EL返回字符串作为键去存放缓存了。
再次,有时候我们希望使用返回结果的一些属性缓存数据,如insertUser方法。在插入数据库前,对应的用户是没有id的,而这个id值会在插入数据库后由MyBatis的机制回填,所以我们希望使用返回结果,这样使用#result就代表返回的结果对象了,它是一个User对象,所以#result.id是取出它的属性id,这样就可以引用这个由数据库生成的id了。
第四,看到updateUserName方法,从代码中可以看到方法,可能返回null。如果为null,则不需要缓存数据,所以在注解@CachePut中加入了condition配置项,它也是一个Spring EL表达式,这个表达式要求返回Boolean类型值,如果为true,则使用缓存操作,否则就不使用。这里的表达式为#result != ‘null’,意味着如果返回null,则方法结束后不再操作缓存。同样地,@Cacheable和@CacheEvict也具备这个配置项。
第五,在updateUserName方法里面我们先调用了getUser方法,因为是更新数据,所以需要慎重一些。一般我们不要轻易地相信缓存,因为缓存存在脏读的可能性,这是需要注意的,在需要更新数据时我们往往考虑先从数据库查询出最新数据,而后再进行操作。因此,这里使用了getUser方法,这里会存在一个误区,很多读者认为getUser方法因为存在了注解@Cacheable,所以会从缓存中读取数据,而从缓存中读取去更新数据,是一个比较危险的行为,因为缓存的数据可能存在脏数据,然后这里的事实是这个注解@Cacheable失效了,也就是说使用updateUserName方法调用getUser方法的逻辑,并不存在读取缓存的可能,它每次都会执行SQL查询数据。关于这个缓存注解失效的问题,在后续章节再给予说明,这里只是提醒读者,更新数据时应该谨慎一些,尽量避免读取缓存数据,因为缓存会存在脏数据的可能。
最后,我们看到findUsers方法,这个方法并没有使用缓存,因为查询结果随着用户给出的查询条件变化而变化,导致命中率很低。
对于命中率很低的场景,使用缓存并不能有效提供系统性能,所以这个方法并不采用缓存机制。此外,对于大数据量等消耗资源的数据,使用缓存也应该谨慎一些。
三、测试缓存注解
6.使用用户控制器测试缓存注解
package com.springboot.chapter2.redis6;
import com.springboot.chapter2.common.AppUserEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Controller
@RequestMapping("/user")
public class RedisUserController {
@Autowired
private RedisUserService userService = null;
@RequestMapping("/getUser")
@ResponseBody
public AppUserEntity getUser(Long id) {
return userService.getUser(id);
}
@RequestMapping("/insertUser")
@ResponseBody
public AppUserEntity insertUser(String userName, String phone) {
AppUserEntity user = new AppUserEntity();
user.setName(userName);
user.setPhone(phone);
userService.insertUser(user);
return user;
}
@RequestMapping("/findUsers")
@ResponseBody
public List<AppUserEntity> findUsers(String userName, String phone) {
return userService.findUsers(userName, phone);
}
@RequestMapping("/updateUserName")
@ResponseBody
public Map<String, Object> updateUserName(Long id, String userName) {
AppUserEntity user = userService.updateUserName(id, userName);
boolean flag = user != null;
String message = flag? "更新成功" : "更新失败";
return resultMap(flag, message);
}
@RequestMapping("/deleteUser")
@ResponseBody
public Map<String, Object> deleteUser(Long id) {
int result = userService.deleteUser(id);
boolean flag = result == 1;
String message = flag? "删除成功" : "删除失败";
return resultMap(flag, message);
}
private Map<String, Object> resultMap(boolean success, String message) {
Map<String, Object> result = new HashMap<String, Object>();
result.put("success", success);
result.put("message", message);
return result;
}
}
7.Spring Boot启动文件
package com.springboot.chapter2.redis6;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.stereotype.Repository;
import javax.annotation.PostConstruct;
@SpringBootApplication(scanBasePackages = "com.springboot.chapter2.redis6")
// 指定扫描的MyBatis Mapper
@MapperScan(basePackages = "com.springboot.chapter2.redis6", annotationClass = Repository.class)
// 使用注解驱动缓存机制
@EnableCaching
public class Redis6Application {
// 注入RedisTemplate
@Autowired
private RedisTemplate redisTemplate = null;
// 定义自定义后初始化方法
@PostConstruct
public void init() {
initRedisTemplate();
}
// 设置RedisTemplate的序列化器
private void initRedisTemplate() {
RedisSerializer stringSerializer = redisTemplate.getStringSerializer();
redisTemplate.setKeySerializer(stringSerializer);
redisTemplate.setHashKeySerializer(stringSerializer);
}
public static void main(String[] args) throws Exception {
SpringApplication.run(Redis6Application.class, args);
}
}
}
这里定义了MyBatis Mapper的扫描包,并限定了在标注有@Repository的接口才会被扫描,同时使用@EnableCaching驱动Spring缓存机制运行,并且通过@PostConstruct定义自定义初始化方法去自定义RedisTemplate的一些特性。
8.测试
http://localhost:8080/user/getUser?id=1
http://localhost:8080/user/insertUser?userName=%27aa%27&&phone=13826005211
http://localhost:8080/user/updateUserName?id=1&userName=%27aa%27
http://localhost:8080/user/deleteUser?id=200020
打开Redis客户端,可以查询到:
Redis缓存机制会使用#{cacheName}:#{key}的形式作为键保存数据,其次对于这个缓存是永远不超时的,会带来缓存不会被刷新的问题,需要根据业务来做相关处理。
四、缓存注解自调用失效问题
上面的updateUserName方法调用getUser方法中,getUser方法上的注解将会失效,原因:
Spring的缓存机制也是基于Spring AOP的原理,而在Spring中AOP是通过动态代理技术来实现的,
这里的updateUserName方法调用getUser方法是类内部的自调用,并不存在代理对象的调用,这样便不会出现AOP,也就不会使用到标注在getUser上的缓存注解去获取缓存的值了。
五、缓存脏数据说明
对于数据的读操作,一般而言是允许不是实时数据,如一些电商网站还存在一些排名榜单,而这个排名往往都不是实时的,它会存在延迟,其实对于查询是可以存在延迟的,也就是存在脏数据是允许的。
但是如果一个脏数据始终存在就说不通了,这样会造成数据失真比较严重。一般对于查询而言,我们可以规定一个时间,让缓存失效,在Redis中也可以设置超时时间,当缓存超过超时时间后,则应用不再能够从缓存中获取数据,而只能从数据库中重新获取最新数据,以保证数据失真不至于太离谱。对于那些要求实时性比较高的数据,我们可以把缓存时间设置得更少一些,这样就会更加频繁地刷新缓存,而不利的是会增加数据库的压力;对于那些要求不是那么高的,则可以使超时时间长一些,这样就可以降低数据库的压力。
对于数据的写操作,往往采取的策略就完全不一样,需要我们谨慎一些,一般会认为缓存不可信,所以会考虑从数据库中先读取最新数据,然后再更新数据,以避免将缓存的脏数据写入数据库中,导致出现业务问题。
缓存谈到了超时时间:在Spring Boot中,默认的RedisCacheManager会采用永不超时的机制,这样便不利于数据的及时更新,可以自定义缓存管理器来解决这个问题。
六、自定义缓存管理器
如果我们并不希望采用Spring Boot机制带来的键命名方式,也不希望缓存永不超时,这时我们可以自定义缓存管理器。在Spring中,我们有三种方法定制缓存管理器:
一种在使用时设置超时时间,例如:stringRedisTemplate.opsForValue().set(“name”,“aa”,2, TimeUnit.MINUTES),推荐这种。
一种是通过配置消除缓存键的前缀和自定义超时时间的属性来定制生成RedisCacheManager;
一种是不采用Spring Boot为我们生成的方式,而是完全通过自己的代码创建缓存管理器,尤其是当需要比较多自定义的时候,更加推荐你采用自定义的代码。
重置Redis缓存管理器
# 禁用前缀
spring.cache.redis.use-key-prefix=false
# 允许保存空值
#spring.cache.redis.cache-null-values=true
# 自定义前缀
#spring.cache.redis.key-prefix=
# 定义超时时间,单位毫秒
spring.cache.redis.time-to-live=600000
spring.cache.redis.use-key-prefix=false:消除了前缀的配置,
spring.cache.redis.time-to-live=600000:将超时时间设置为10 min(600000 ms),这样10 min过后Redis的键就会超时,就不能从Redis中读取到数据了,而只能重新从数据库读取数据,
这样就能有效刷新数据了。
经过上面的修改,清除Redis的数据,重启Spring Boot应用,重新测试控制器的getUser方法,然后在10 min内打开Redis客户端依次输入以下命令:
keys * #查看Redis存在的键值对
get redis_user_1 #获取id为1的用户信息
ttl redis_user_1 #查询键的剩余超时秒数
Spring Boot为我们自定义的前缀消失了,而我们也成功地设置了超时时间。
有时候,在自定义时可能存在比较多的配置,也可以不采用Spring Boot自动配置的缓存管理器,而是使用自定义的缓存管理器,
自定义缓存管理器:给IoC容器增加缓存管理器
// 注入连接工厂,由Spring Boot自动配置生成
@Autowired
private RedisConnectionFactory connectionFactory = null;
// 自定义Redis缓存管理器
@Bean(name = "redisCacheManager" )
public RedisCacheManager initRedisCacheManager() {
// Redis加锁的写入器
RedisCacheWriter writer= RedisCacheWriter.lockingRedisCacheWriter(connectionFactory);
// 启动Redis缓存的默认设置
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
// 设置JDK序列化器
config = config.serializeValuesWith(
SerializationPair.fromSerializer(new JdkSerializationRedisSerializer()));
// 禁用前缀
config = config.disableKeyPrefix();
//设置10 min超时
config = config.entryTtl(Duration.ofMinutes(10));
// 创建缓Redis存管理器
RedisCacheManager redisCacheManager = new RedisCacheManager(writer, config);
return redisCacheManager;
}
这里首先注入了RedisConnectionFactory对象,该对象是由Spring Boot自动生成的。
在创建Redis缓存管理器对象RedisCacheManager的时候,首先创建了带锁的RedisCacheWriter对象;
然后使用RedisCacheConfiguration对其属性进行配置,这里设置了禁用前缀,并且超时时间为10 min;
最后就通过RedisCacheWriter对象和RedisCacheConfiguration对象去构建RedisCacheManager对象了,这样就完成了Redis缓存管理器的自定义。