该文章为如下两个工作的后续内容,在该文章的操作之前需要首先完成redis的安装和配置,以及Spring Boot和Redis的整合:
在Spring Boot中开启Redis Cache的过程比较简单,首先在application.properities配置文件中加入如下的redis cache配置项:
# Spring Redis Cache
# 设置缓存类型,这里使用Redis作为缓存服务器
spring.cache.type=REDIS
# 定义cache名称,用于在缓存注解中引用,多个名称可以使用逗号分隔
spring.cache.cache-names=redisCache
# 允许保存空值
spring.cache.redis.cache-null-values=true
# 自定义缓存前缀
#spring.cache.redis.key-prefix=
# 是否使用前缀
spring.cache.redis.use-key-prefix=true
# 设置缓存失效时间,0或者默认为永远不失效
spring.cache.redis.time-to-live=0
上面的配置已经使用注释进行了说明,该配置其实是为缓存管理器CacheManager进行设置,这里将spring.cache.type设置为REDIS,即指定缓存管理器为RedisCacheManager。完成上述的配置,Spring Boot即会自动创建相应的缓存管理器来进行缓存的相关操作。
为了使用缓存管理器,还需要在Redis的配置类(或者整个项目的启动类)中加入驱动缓存的注解,这里继续Spring Boot集成Redis与使用RedisTemplate进行基本数据结构操作示例中配置类中添加该注解:
@Configuration
@EnableCaching // 开启Spring Redis Cache,使用注解驱动缓存机制
public class SpringRedisConfiguration {
@Autowired
private RedisTemplate redisTemplate;
/**
* 利用Bean生命周期使用PostConstruct注解自定义后初始化方法
*/
@PostConstruct
public void init() {
initRedisTemplate();
}
/**
* 设置RedisTemplate的序列化器
*/
private void initRedisTemplate() {
RedisSerializer stringSerializer = redisTemplate.getStringSerializer();
// 将Key和其散列表数据类型的filed都修改为使用StringRedisSerializer进行序列化
redisTemplate.setKeySerializer(stringSerializer);
redisTemplate.setHashKeySerializer(stringSerializer);
}
}
这样即完成了cache的开启,下面构建cache的相关测试逻辑来使用缓存注解操作缓存数据。
注,这里同时在springboot的配置文件中设置了默认的数据库隔离级别为读写提交,以避免在使用数据库事务使产生脏读:
# 设置默认的隔离级别为读写提交
spring.datasource.tomcat.default-transaction-isolation=2
下面创建简单的java对象,以及相应的操作逻辑,来使用缓存注解对Redis的cache进行管理。
@Data
@Alias(value = "user")
public class User implements Serializable {
// 开启Spring Redis Cache时,加入序列化
private static final long serialVersionUID = -4947062488310146862L;
private Long id;
@NotNull(message = "用户名不能为空")
private String userName;
@NotNull(message = "备注不能为空")
private String note;
@NotNull(message = "性别不能为空")
private SexEnum sex;
}
MyBatis映射接口如下:
@Repository
public interface UserMapper {
User getUserById(Long id);
int insertUser(User user);
int updateUserById(User user);
int deleteUser(Long id);
@Select("select * from t_user where user_name = #{userName}")
List getUsersByName(@Param("userName") String userName);
}
相应的映射文件如下(这里在操作MyBatis是同时使用了注解和映射文件,可以选择都是使用简单的注解或都使用映射文件进行实现) :
insert into t_user (user_name, sex, note) values (#{userName}, #{sex}, #{note})
update t_user set user_name=#{userName}, sex=#{sex}, note=#{note} where id=#{id}
delete from t_user where id=#{id}
创建相关的service类和其实现类:
public interface UserCacheService {
User getUser(Long id);
User insertUser(User user);
User updateUserName(Long id, String userName);
List findUsers(String userName);
int deleteUser(Long id);
}
其实现类如下:
@Service
public class UserCacheServiceImpl implements UserCacheService {
@Autowired
private UserMapper userMapper;
/**
* 使用Transactional注解开启事务
* Cacheable注解表示先从缓存中通过定义的键值进行查询,如果查询不到则进行数据库查询并将查询结果保存到缓存中,其中:
* value属性为spring application.properties配置文件中设置的缓存名称
* key表示缓存的键值名称,其中id说明该方法需要一个名为id的参数
*/
@Override
@Transactional
@Cacheable(value = "redisCache", key = "'redis_user_'+#id")
public User getUser(Long id) {
return userMapper.getUserById(id);
}
/**
* CachePut注解表示将方法的返回结果存放到缓存中
* value和key属性与上述意义一样,需要注意的是,key中的使用了result.id的方式
* 这里的result表示该方法的返回值对象,即为user,id为取该对象的id属性值
*
* 这里在插入user时,传入的user参数是不存在id属性的,在mapper.xml文件中insertUser使用了如下的属性设置:
* useGeneratedKeys="true" keyProperty="id"
* 意味着,user的id属性会进行自增,并在use插入成功后会将指定的id属性进行回填,因此如下方法的返回值为带有id属性的完整user对象
*/
@Override
@Transactional
@CachePut(value = "redisCache", key = "'redis_user_'+#result.id")
public User insertUser(User user) {
userMapper.insertUser(user);
System.out.println("After insert, User is: " + user);
return user;
}
/**
* 这里在CachePut注解中使用了condition配置项,它是一个Spring的EL,这个表达式要求返回Boolean类型的值,如果为true
* 则使用缓存操作,否则不使用。
*/
@Override
@Transactional
@CachePut(value = "redisCache", condition = "#result != null ", key = "'redis_user_'+#id")
public User updateUserName(Long id, String userName) {
User user = userMapper.getUserById(id);
if (user == null) {
return null;
}
user.setUserName(userName);
userMapper.updateUserById(user);
return user;
}
/**
* 命中率低,所以不采用缓存机制
*/
@Override
@Transactional
public List findUsers(String userName) {
return userMapper.getUsersByName(userName);
}
/**
* CacheEvict注解通过定义的键移除相应的缓存,beforeInvocation属性表示是在方法执行之前还是之后移除缓存,默认为false,即为方法之后
* 移除缓存
*/
@Override
@Transactional
@CacheEvict(value = "redisCache", key = "'redis_user_'+#id", beforeInvocation = false)
public int deleteUser(Long id) {
return userMapper.deleteUser(id);
}
}
缓存注解@Cacheable,@CachePut,@CacheEvict的使用如上面代码中的注释部分所示。
创建相应的Controller以供测试:
@Controller
@RequestMapping("/user/cache")
public class UserCacheController {
@Autowired
private UserCacheService userCacheService;
/**
* 根据ID获取User
*/
@RequestMapping("/getUser")
@ResponseBody
public CommonResult getUser(Long id) {
User user = userCacheService.getUser(id);
return new CommonResult(true, "获取成功", user);
}
/**
* 插入一个新User
*/
@RequestMapping("/insertUser")
@ResponseBody
public CommonResult insertUser(String userName, int sex, String note) {
User user = new User(userName, sex, note);
User resultUser = userCacheService.insertUser(user);
return new CommonResult(true, "新增成功", resultUser);
}
/**
* 根据Id查找用户并更新username
*/
@RequestMapping("/updateUserName")
@ResponseBody
public CommonResult updateUserName(Long id, String userName) {
User user = userCacheService.updateUserName(id, userName);
boolean flag = user != null;
String msg = flag ? "更新成功" : "更新失败";
return new CommonResult(flag, msg, user);
}
/**
* 根据Username查找UserList
*/
@RequestMapping("/findUsers")
@ResponseBody
public CommonResult findUsers(String userName) {
List users = userCacheService.findUsers(userName);
return new CommonResult(true, "查找成功", users);
}
/**
* 删除用户
*/
@RequestMapping("/deleteUser")
@ResponseBody
public CommonResult deleteUser(Long id) {
int result = userCacheService.deleteUser(id);
boolean flag = result == 1;
String msg = flag ? "删除成功" : "删除失败";
return new CommonResult(false, msg);
}
}
在进行测试之前,先通过redis-cli客户端命令行对数据库中的数据进行清除:
127.0.0.1:6379> flushall
OK
127.0.0.1:6379> keys *
(empty list or set)
下面对上述构建的几个不同的方法进行cache的测试,首先请求insertUser插入一条记录:
http://localhost:8080/user/cache/insertUser?userName=yitian_cache&sex=1¬e=none
返回结果如下:
数据库中已经存在相应的数据,查看redis中的keys如下,可以看到将插入的相应的数据缓存到了Redis中:
127.0.0.1:6379> keys *
1) "redisCache::redis_user_26"
此时如果对该id的user进行数据查询时:
http://localhost:8080/user/cache/getUser?id=26
会之前从redis缓存中获得数据,而不会发送SQL,如下:
2020-02-13 11:31:57.475 DEBUG 57559 --- [nio-8080-exec-2] s.s.w.c.SecurityContextPersistenceFilter : SecurityContextHolder now cleared, as request processing completed
2020-02-13 11:31:57.475 DEBUG 57559 --- [nio-8080-exec-2] o.a.tomcat.util.net.SocketWrapperBase : Socket: [org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper@14322c76:org.apache.tomcat.util.net.NioChannel@4e7228f6:java.nio.channels.SocketChannel[connected local=/0:0:0:0:0:0:0:1:8080 remote=/0:0:0:0:0:0:0:1:53303]], Read from buffer: [0]
2020-02-13 11:31:57.475 DEBUG 57559 --- [nio-8080-exec-2] org.apache.tomcat.util.net.NioEndpoint : Socket: [org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper@14322c76:org.apache.tomcat.util.net.NioChannel@4e7228f6:java.nio.channels.SocketChannel[connected local=/0:0:0:0:0:0:0:1:8080 remote=/0:0:0:0:0:0:0:1:53303]], Read direct from socket: [0]
2020-02-13 11:31:57.475 DEBUG 57559 --- [nio-8080-exec-2] o.apache.coyote.http11.Http11Processor : Socket: [org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper@14322c76:org.apache.tomcat.util.net.NioChannel@4e7228f6:java.nio.channels.SocketChannel[connected local=/0:0:0:0:0:0:0:1:8080 remote=/0:0:0:0:0:0:0:1:53303]], Status in: [OPEN_READ], State out: [OPEN]
但如果测试从数据库中查询一个之前已经存在(但缓存中不存在的数据)时,例如id=1,可以从日志中看到sqlsession和transactional的构建和提交,此时会从数据库查询数据:
2020-02-13 11:37:25.944 DEBUG 57559 --- [nio-8080-exec-8] o.s.web.servlet.DispatcherServlet : GET "/user/cache/getUser?id=1", parameters={masked}
2020-02-13 11:37:25.944 DEBUG 57559 --- [nio-8080-exec-8] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to cn.zyt.springbootlearning.controller.UserCacheController#getUser(Long)
2020-02-13 11:37:25.950 DEBUG 57559 --- [nio-8080-exec-8] o.s.j.d.DataSourceTransactionManager : Creating new transaction with name [cn.zyt.springbootlearning.service.impl.UserCacheServiceImpl.getUser]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2020-02-13 11:37:25.972 DEBUG 57559 --- [nio-8080-exec-8] o.s.j.d.DataSourceTransactionManager : Acquired Connection [HikariProxyConnection@519212670 wrapping com.mysql.jdbc.JDBC4Connection@1dd6a3e5] for JDBC transaction
2020-02-13 11:37:25.974 DEBUG 57559 --- [nio-8080-exec-8] o.s.j.d.DataSourceTransactionManager : Switching JDBC Connection [HikariProxyConnection@519212670 wrapping com.mysql.jdbc.JDBC4Connection@1dd6a3e5] to manual commit
2020-02-13 11:37:26.001 DEBUG 57559 --- [nio-8080-exec-8] org.mybatis.spring.SqlSessionUtils : Creating a new SqlSession
2020-02-13 11:37:26.003 DEBUG 57559 --- [nio-8080-exec-8] org.mybatis.spring.SqlSessionUtils : Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5d084971]
2020-02-13 11:37:26.007 DEBUG 57559 --- [nio-8080-exec-8] o.m.s.t.SpringManagedTransaction : JDBC Connection [HikariProxyConnection@519212670 wrapping com.mysql.jdbc.JDBC4Connection@1dd6a3e5] will be managed by Spring
2020-02-13 11:37:26.008 DEBUG 57559 --- [nio-8080-exec-8] c.z.s.dao.UserMapper.getUserById : ==> Preparing: select id, user_name as userName, sex, note from t_user where id = ?
2020-02-13 11:37:26.021 DEBUG 57559 --- [nio-8080-exec-8] c.z.s.dao.UserMapper.getUserById : ==> Parameters: 1(Long)
2020-02-13 11:37:26.053 DEBUG 57559 --- [nio-8080-exec-8] c.z.s.dao.UserMapper.getUserById : <== Total: 1
2020-02-13 11:37:26.053 DEBUG 57559 --- [nio-8080-exec-8] org.mybatis.spring.SqlSessionUtils : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5d084971]
2020-02-13 11:37:26.053 DEBUG 57559 --- [nio-8080-exec-8] org.mybatis.spring.SqlSessionUtils : Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5d084971]
2020-02-13 11:37:26.181 DEBUG 57559 --- [nio-8080-exec-8] org.mybatis.spring.SqlSessionUtils : Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5d084971]
2020-02-13 11:37:26.181 DEBUG 57559 --- [nio-8080-exec-8] org.mybatis.spring.SqlSessionUtils : Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5d084971]
2020-02-13 11:37:26.181 DEBUG 57559 --- [nio-8080-exec-8] o.s.j.d.DataSourceTransactionManager : Initiating transaction commit
注,如上的日志输出为DEBUG级别,需要在application.properties配置文件中进行如下设置:
# 日志配置 logging.level.root=DEBUG logging.level.org.springframework=DEBUG logging.level.org.org.mybatis=DEBUG
以查看详细的日志输出。
当数据查询成功时,同样会将结果存放到redis缓存中:
127.0.0.1:6379> keys *
1) "redisCache::redis_user_1"
2) "redisCache::redis_user_26"
同样的,如果使用updateUserName方法对user进行更新时,也会将redis中不存在的缓存数据加入到缓存中:
http://localhost:8080/user/cache/updateUserName?id=23&userName=yitian_new
成功更新完成后,redis中数据如下:
127.0.0.1:6379> keys *
1) "redisCache::redis_user_1"
2) "redisCache::redis_user_26"
3) "redisCache::redis_user_23"
但需要注意的是,在更新数据时缓存中的数据有可能是脏数据,所以在updateUserName方法中首先对数据库的数据进行了获取,然后在对该数据进行更新,从而避免之前从缓存中读取可以过时的数据。这一点需要注意。
由于在使用findUser查询用户列表时,缓存的命中率会很低(因为查询的参数可能存在很大差异),所以这里没有设置缓存的使用。请求如下:
http://localhost:8080/user/cache/findUsers?userName=yitian
返回结果如下:
在对指定id的用户进行删除时,通过上述缓存注解的使用,会在方法调用完成后将缓存中的数据删除,这里使用上面添加的id=26的数据进行测试:
http://localhost:8080/user/cache/deleteUser?id=26
删除成功后redis中的结果如下:
127.0.0.1:6379> keys *
1) "redisCache::redis_user_1"
2) "redisCache::redis_user_23"
注意,在缓存注解中设置的value名会用于缓存的匹配,所以该名称需要在插入和删除时保持一致,否则在删除数据时不会匹配到正确的缓存数据,导致缓存删不掉。
使用以上的缓存管理器的配置时,默认缓存的名称为{cacheName}::#{key}的形式,并且缓存永不失效。在application.properties文件中可以进行相应的配置:
# 自定义缓存前缀
spring.cache.redis.key-prefix=
# 是否使用前缀
spring.cache.redis.use-key-prefix=false
# 设置缓存失效时间,0或者默认为永远不失效
spring.cache.redis.time-to-live=600000
上面的设置即是将缓存前缀去掉, 只使用key作为缓存名,同时将缓存失效时间设置为600s,即10分钟。这样10分钟过后,redis的键就会超时,缓存会在数据操作时进行更新。
在对Spring Boot中缓存管理器进行设置时,除了如上使用配置文件的方式,还可以通过自定义缓存管理器来创建需要的缓存管理器并进行设置,当需要的自定义设置比较多时,推荐使用这种方式。
在上述的SpringRedisConfiguration.java配置类中进行如下定义:
/**
* 自定义RedisCacheManager
*/
// @Bean(name = "redisCacheManager")
public RedisCacheManager initRedisCacheManager() {
// 获取Redis加锁的写入器
RedisCacheWriter writer = RedisCacheWriter.lockingRedisCacheWriter(redisConnectionFactory);
// 启动Redis缓存的默认设置
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
// 设置JDK序列化器
config = config.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(new JdkSerializationRedisSerializer()));
// 自定义设置:禁用前缀
config = config.disableKeyPrefix();
// 设置失效时间
config = config.entryTtl(Duration.ofMinutes(10));
// 创建Redis缓存管理器
RedisCacheManager redisCacheManager = new RedisCacheManager(writer, config);
return redisCacheManager;
}