这里用一个示例通过注解的方式整合 Spring 和 Redis,要特别注意 Redis 缓存和数据库一致性的问题。
数据缓存往往会在 Redis 上设置超时时间,当设置 Redis 的数据超时后, Redis 就没法读出数据了 , 这个时候就会触发程序读取数据库 , 然后将读取的数据库数据写入 Redis (此时会给 Redis 重设超时时间 ),这样程序在读取的过程中就能按一定的时间间隔刷新数据了。
流程图大致如下:
伪代码大致如下
public Data getData(args){ // 从Redis中获取数据 Data data = getDataFromRedis(key); // 如果redis中没有数据,从db中加载并写入redis if( data = null) // 从数据库中读取数据 data = getDataFromDataBase(); // 重新写入 Redis,以便后续从Redis中读取 write2Redis(key ,data); // 设置 Re dis 的超时时间为 5 分钟 setRedisExpireTime(5); } }
上面的伪代码完成了上图所描述的过程。这样每当读取 Redis 数据超过 5 分钟, Redis就不能读到超时数据了,只能重新从 Redis 中读取,保证了一定的实时性,也避免了多次访问数据库造成的系统性能低下的情况。
写操作要考虑数据一致的问题,尤其是那些重要的业务数据,所以首先应该考虑从数据库中读取最新的数据,然后对数据进行操作,最后把数据写入 Redis 缓存中.
流程大致如下
写入业务数据,先从数据库中读取最新数据,然后进行业务操作,更新业务数据到数据库后,再将数据刷新到 Redis 缓存中,这样就完成了一次写操作。这样的操作就能避免将脏数据写入数据库中,这类问题在操作时要注意。
伪代码大致如下
public void writeData(args){ //从数据库里读取最新数据 DataObject dataObject = getFromDataBase(args); //执行业务逻辑 execLogic(dataObject); //更新数据库数据 updateDataBase(dataObject ); // 刷新 Red is 缓存 updateRedisData(dataObject ) ; }
上面的伪代码完成了上图所描述的过程。首先,从数据库中读取最新的数据,以规避缓存中的脏数据问题,执行了逻辑,修改了部分业务数据。然后,把这些数据保存到数据库里,最后,刷新这些数据到 Redis 中。
用到了
需要将上述3者整合起来
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>ssm_anno_redis</groupId> <artifactId>ssm_anno_redis</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>ssm_anno_redis</name> <url>http://maven.apache.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!-- 单元测试 --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <!-- 1.日志 --> <!-- 实现slf4j接口并整合 --> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.1</version> </dependency> <!-- 2.数据库 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.38</version> <scope>runtime</scope> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-dbcp2</artifactId> <version>2.5.0</version> </dependency> <!-- DAO: MyBatis --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.4.2</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.3.1</version> </dependency> <!-- 4.Spring --> <!-- 1)Spring核心 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> </dependency> <!-- 2)Spring DAO层 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> </dependency> <!-- redis客户端:Jedis --> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.9.0</version> </dependency> <!-- spring-data-redis --> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-redis</artifactId> <version>1.8.15.RELEASE</version> </dependency> </dependencies> <!-- 在dependencyManagement中引入spring-framework-bom来确保所有的spring模块都使用统一的版本. 添加spring-framework-bom后,就不需要配置每个依赖的版本号了,方便管理与升级 --> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-framework-bom</artifactId> <version>4.3.9.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <finalName>ssm_anno_redis</finalName> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.7.0</version> <configuration> <source>1.8</source> <target>1.8</target> <encoding>UTF8</encoding> </configuration> </plugin> </plugins> </build> </project>
mysql 部署在CentOS6.5 ,版本为5.7 , 地址为192.168.31.66 。 同样的Redis也部署在这台主机上,单节点。
drop database artisan; create database artisan; use artisan; create table t_role ( id int(12) auto_increment, role_name varchar(60) not null, note varchar(256) null, primary key(id) );
package com.artisan.ssm_redis.domain; import java.io.Serializable; public class Role implements Serializable { private static final long serialVersionUID = -4381384997344901377L; private Long id; private String roleName; private String note; /**** setter and getter ****/ public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getRoleName() { return roleName; } public void setRoleName(String roleName) { this.roleName = roleName; } public String getNote() { return note; } public void setNote(String note) { this.note = note; } @Override public String toString() { return "Role [id=" + id + ", roleName=" + roleName + ", note=" + note + "]"; } }
该类实现了 Serializable 接口,这说明这个类支持序列化,这样就可以通过 Spring的序列化器,将其保存为对应的编码,缓存到 Redis 中,也可以通过 Redis 读回那些编码,反序列化为对应的 Java 对象。
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.artisan.ssm_redis.dao.RoleDao"> <select id="getRole" resultType="com.artisan.ssm_redis.domain.Role"> select id, role_name as roleName, note from t_role where id = #{id} </select> <delete id="deleteRole"> delete from t_role where id=#{id} </delete> <insert id="insertRole" parameterType="com.artisan.ssm_redis.domain.Role" useGeneratedKeys="true" keyProperty="id"> insert into t_role (role_name, note) values(#{roleName}, #{note}) </insert> <update id="updateRole" parameterType="com.artisan.ssm_redis.domain.Role"> update t_role set role_name = #{roleName}, note = #{note} where id = #{id} </update> <select id="findRoles" resultType="com.artisan.ssm_redis.domain.Role"> select id, role_name as roleName, note from t_role <where> <if test="roleName != null"> role_name like concat('%', #{roleName}, '%') </if> <if test="note != null"> note like concat('%', #{note}, '%') </if> </where> </select> </mapper>
Mapper映射文件完成后,需要一个 MyBatis 角色接口 , 以便使用这样的一个映射文件,
package com.artisan.ssm_redis.dao; import java.util.List; import org.apache.ibatis.annotations.Param; import org.springframework.stereotype.Repository; import com.artisan.ssm_redis.domain.Role; @Repository public interface RoleDao { public Role getRole(Long id); public int deleteRole(Long id); public int insertRole(Role role); public int updateRole(Role role); public List<Role> findRoles(@Param("roleName") String roleName, @Param("note") String note); }
注解@Repository 表示它是一个持久层的接口。通过扫描和注解联合定义 DAO 层,就完成了mappe的内容。
定义角色服务接口( RoleService ),因为要在接口实现类中加入 Spring 缓存注解,以驱动不同的行为,所里这里仅仅先将接口定义出来
package com.artisan.ssm_redis.service; import java.util.List; import com.artisan.ssm_redis.domain.Role; public interface RoleService { public Role getRole(Long id); public int deleteRole(Long id); public Role insertRole(Role role); public int updateRole(Role role); public List<Role> findRoles(String roleName, String note); public int insertRoles(List<Role> roleList); }
RootConfig.java
package com.artisan.ssm_redis.config; import java.io.IOException; import java.util.Properties; import javax.sql.DataSource; import org.apache.commons.dbcp2.BasicDataSourceFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.mapper.MapperScannerConfigurer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.stereotype.Repository; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.TransactionManagementConfigurer; @Configuration // 定义Spring 扫描的包 @ComponentScan("com.artisan.ssm_redis*") // 使用事务驱动管理器 @EnableTransactionManagement // 实现接口TransactionManagementConfigurer,这样可以配置注解驱动事务 public class RootConfig implements TransactionManagementConfigurer { private DataSource dataSource = null; /** * * * @Title: initDataSource * * @Description: 配置数据库 * * * @return: DataSource * @throws IOException */ @Bean(name = "dataSource") public DataSource initDataSource() throws IOException { if (dataSource != null) { return dataSource; } Properties props = new Properties(); props.load(RootConfig.class.getClassLoader().getResourceAsStream("jdbc.properties")); props.setProperty("driverClassName", props.getProperty("jdbc.driver")); props.setProperty("url", props.getProperty("jdbc.url")); props.setProperty("username", props.getProperty("jdbc.username")); props.setProperty("password", props.getProperty("jdbc.password")); try { dataSource = BasicDataSourceFactory.createDataSource(props); } catch (Exception e) { e.printStackTrace(); } return dataSource; } /** * * * @Title: initSqlSessionFactory * * @Description: 配置SqlSessionFactoryBean * * * @return: SqlSessionFactoryBean * @throws IOException */ @Bean(name = "sqlSessionFactory") public SqlSessionFactoryBean initSqlSessionFactory() throws IOException { SqlSessionFactoryBean sqlSessionFactory = new SqlSessionFactoryBean(); sqlSessionFactory.setDataSource(initDataSource()); // 配置MyBatis配置文件 Resource resource = new ClassPathResource("mybatis/mybatis-config.xml"); sqlSessionFactory.setConfigLocation(resource); return sqlSessionFactory; } /** * * * @Title: initMapperScannerConfigurer * * @Description: 通过自动扫描,发现MyBatis Mapper接口 * * * @return: MapperScannerConfigurer Mapper扫描器 */ @Bean public MapperScannerConfigurer initMapperScannerConfigurer() { MapperScannerConfigurer msc = new MapperScannerConfigurer(); // 扫描包 msc.setBasePackage("com.artisan.ssm_redis"); msc.setSqlSessionFactoryBeanName("sqlSessionFactory"); // 区分注解扫描 msc.setAnnotationClass(Repository.class); return msc; } /** * 实现接口方法,注册注解事务,当@Transactional 使用的时候产生数据库事务 */ @Override @Bean(name = "annotationDrivenTransactionManager") public PlatformTransactionManager annotationDrivenTransactionManager() { DataSourceTransactionManager transactionManager = null; try { transactionManager = new DataSourceTransactionManager(); transactionManager.setDataSource(initDataSource()); } catch (IOException e) { e.printStackTrace(); } return transactionManager; } }
在 SqlSessionFactoryBean 的 定义中引入了关于 MyBatis 的 一 个配置文件mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <mappers> <mapper resource="mapper/RoleMapper.xml"/> </mappers> </configuration>
写到这里就可以开始进行Dao和Service层的单元测试了,比较简单这里省略了先。。。
因为我们主要是整合Spring Cache和 Redis. 所以Spring Cache的基础知识 请参阅我的专栏 Spring-Cache手札
在 Spring 项目 中它提供了接口 CacheManager 来定义缓存管理器 , 这样各个不同 的缓存就可以实现它来提供管理器的功能了,而在 spring-data-redis.jar 包中 实现 CacheManager接口的则是 RedisCacheManager, 因此要定义 RedisCacheManager 的 Bean , 不过在此之前要先定义 RedisTemplate。
下面使用注解驱动RedisCacheManager 定义
package com.artisan.ssm_redis.config; import java.util.ArrayList; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import redis.clients.jedis.JedisPoolConfig; /**** imports ****/ @Configuration @EnableCaching public class RedisConfig { @Bean(name = "redisTemplate") public RedisTemplate initRedisTemplate() { JedisPoolConfig poolConfig = new JedisPoolConfig(); // 最大空闲数 poolConfig.setMaxIdle(50); // 最大连接数 poolConfig.setMaxTotal(100); // 最大等待毫秒数 poolConfig.setMaxWaitMillis(20000); // 创建Jedis连接工厂 JedisConnectionFactory connectionFactory = new JedisConnectionFactory(poolConfig); connectionFactory.setHostName("192.168.31.66"); connectionFactory.setPort(6379); // 调用后初始化方法,没有它将抛出异常Cannot get Jedis connection connectionFactory.afterPropertiesSet(); // 自定Redis序列化器 RedisSerializer jdkSerializationRedisSerializer = new JdkSerializationRedisSerializer(); RedisSerializer stringRedisSerializer = new StringRedisSerializer(); // 定义RedisTemplate,并设置连接工程 RedisTemplate redisTemplate = new RedisTemplate(); redisTemplate.setConnectionFactory(connectionFactory); // 设置序列化器 redisTemplate.setDefaultSerializer(stringRedisSerializer); redisTemplate.setKeySerializer(stringRedisSerializer); redisTemplate.setValueSerializer(jdkSerializationRedisSerializer); redisTemplate.setHashKeySerializer(stringRedisSerializer); redisTemplate.setHashValueSerializer(jdkSerializationRedisSerializer); return redisTemplate; } @Bean(name = "redisCacheManager") public CacheManager initRedisCacheManager(@Autowired RedisTemplate redisTempate) { RedisCacheManager cacheManager = new RedisCacheManager(redisTempate); // 设置超时时间为10分钟,单位为秒 cacheManager.setDefaultExpiration(600); // 设置缓存名称 List<String> cacheNames = new ArrayList<String>(); cacheNames.add("redisCacheManager"); cacheManager.setCacheNames(cacheNames); return cacheManager; } }
@EnableCaching 表示 Spring IoC 容器启动了缓存机制。
对于 RedisTemplate 的定义实例和 XML 的方式差不多。
注意,在创建 Jedis 连接工厂(JedisConnectionFactory )后,要自己调用其 afterPropertiesSet 方法 , 因为这里不是单独自定义一个 Spring Bean,而是在 XML方式中是单独 自定义的 .这个类实现了 InitializingBean 接口,按照 Spring Bean 的生命周期,它会被 Spring IoC 容器自己调用,而这里的注解方式没有定义 Spring Bean,因此需要自己调用.
字符串定义了 key (包括 hash 数据结构),而值则使用了序列化,这样就能够保存 Java对象了。缓存管理器 RedisCacheManager 定义了默认的超时时间为 10 分钟,这样就可以在一定的时间间隔后重新从数据库中读取数据了,而名称则定义为 redisCacheManager, 名称是为了方便后面注解引用的 。
package com.artisan.ssm_redis.service.impl; import java.util.List; 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.Isolation; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import com.artisan.ssm_redis.dao.RoleDao; import com.artisan.ssm_redis.domain.Role; import com.artisan.ssm_redis.service.RoleService; @Service public class RoleServiceImpl implements RoleService { // 自动注入 @Autowired private RoleDao roleDao; /** * 使用@Cacheable定义缓存策略 当缓存中有值,则返回缓存数据,否则访问方法得到数据 通过value引用缓存管理器,通过key定义键 * * @param id * 角色编号 * @return 角色 */ @Override @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED) @Cacheable(value = "redisCacheManager", key = "'redis_role_'+#id") public Role getRole(Long id) { return roleDao.getRole(id); } /** * 使用@CachePut则表示无论如何都会执行方法,最后将方法的返回值再保存到缓存中 * 使用在插入数据的地方,则表示保存到数据库后,会同期插入到Redis缓存中 * * @param role * 角色对象 * @return 角色对象(会回填主键) */ @Override @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED) @CachePut(value = "redisCacheManager", key = "'redis_role_'+#result.id") public Role insertRole(Role role) { roleDao.insertRole(role); return role; } /** * 使用@CachePut,表示更新数据库数据的同时,也会同步更新缓存 * * @param role * 角色对象 * @return 影响条数 */ @Override @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED) @CachePut(value = "redisCacheManager", key = "'redis_role_'+#role.id") public int updateRole(Role role) { return roleDao.updateRole(role); } }
因为 getRole 方法是一个查询方法,所以使用@Cacheable 注解,这样在 Spring 的调用中,它就会先查询 Redis , 看看是否存在对应的值,那么采用什么 key 去查询呢?注解中的key 属性,它配置的'redis_role_'+#id
, 这样 Spring EL 就会计算返回 一个 key ,比如参数id 为 1L , 其 key 计算结果就为 redis_role_1
。以一个 key 去访问 Redis ,如果有返回值,则不再执行方法,如果没有则访问方法 , 返回角色信息,然后通过 key 去保存数据到 Redis 中 。
先执行 insertRole 方法才能把对应的信息保存到 Redis 中,所以采用的是注解@CachePut。由于主键是由数据库生成,所以无法从参数中读取,但是可以从结果中读取,那么 #result.id
的写法就会返回方法返回的角色 id。而这个角色 id 是通过数据库生成,然后由 MyBatis 进行回填得到的 ,这样就可以在 Redis 中新增一个 key , 然后保存对应的对象了。
对于 updateRole 方法而言,采用的是注解@CachePut,由于对象有所更新,所以要在方法之后更新 Redis 的数据,以保证数据的一致性。这里直接读取参数的 id ,所以表达式写#role.id
,这样就可以引入角色参数的 id 了 。在方法结束后,它就会去更新 Redis 对应的 key 的值了。
为此可以提供一个 log4j .properties 文件来监控整个过程:
log4j.rootLogger=DEBUG , stdout log4j.logger.org.mybatis=DEBUG log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%5p %d %C: %m%n
写下单元测试来验证下
package com.artisan.ssm_redis.service; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import com.artisan.ssm_redis.config.RedisConfig; import com.artisan.ssm_redis.config.RootConfig; import com.artisan.ssm_redis.domain.Role; public class RoleServiceTest { public static void main(String[] args) { // 使用注解Spring IoC容器 ApplicationContext ctx = new AnnotationConfigApplicationContext(RootConfig.class, RedisConfig.class); // 获取角色服务类 RoleService roleService = ctx.getBean(RoleService.class); Role role = new Role(); role.setRoleName("role_name_1"); role.setNote("role_note_1"); // 插入角色 roleService.insertRole(role); // 获取角色 Role role2 = roleService.getRole(role.getId()); System.out.println(role2.toString()); // 更新角色 role2.setNote("role_note_1_update"); roleService.updateRole(role2); System.out.println(role2.toString()); // 删除角色 // roleService.deleteRole(role2.getId()); } }
这里将关于数据库和 Redis 的相关配置通过注解 Spring IoC 容器加载进来 , 这样就可以用 Spring 操作这些资源了,然后执行插入 、 获取 、 更新角色的方法 ,日志如下
23:59:14.151 [main] DEBUG org.mybatis.spring.SqlSessionUtils - Creating a new SqlSession 23:59:14.161 [main] DEBUG org.mybatis.spring.SqlSessionUtils - Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7674b62c] 23:59:14.171 [main] DEBUG org.mybatis.spring.transaction.SpringManagedTransaction - JDBC Connection [1168924571, URL=jdbc:mysql://192.168.31.66:3306/artisan?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT, UserName=root@192.168.31.137, MySQL Connector Java] will be managed by Spring 23:59:14.171 [main] DEBUG com.artisan.ssm_redis.dao.RoleDao.insertRole - ==> Preparing: insert into t_role (role_name, note) values(?, ?) 23:59:14.221 [main] DEBUG com.artisan.ssm_redis.dao.RoleDao.insertRole - ==> Parameters: role_name_1(String), role_note_1(String) 23:59:14.221 [main] DEBUG com.artisan.ssm_redis.dao.RoleDao.insertRole - <== Updates: 1 23:59:14.231 [main] DEBUG org.mybatis.spring.SqlSessionUtils - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7674b62c] 23:59:14.321 [main] DEBUG org.springframework.data.redis.core.RedisConnectionUtils - Opening RedisConnection 23:59:14.464 [main] DEBUG org.springframework.data.redis.core.RedisConnectionUtils - Closing Redis Connection 23:59:14.464 [main] DEBUG org.mybatis.spring.SqlSessionUtils - Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7674b62c] 23:59:14.464 [main] DEBUG org.mybatis.spring.SqlSessionUtils - Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7674b62c] 23:59:14.474 [main] DEBUG org.mybatis.spring.SqlSessionUtils - Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7674b62c] 23:59:14.474 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Initiating transaction commit 23:59:14.474 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Committing JDBC transaction on Connection [1168924571, URL=jdbc:mysql://192.168.31.66:3306/artisan?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT, UserName=root@192.168.31.137, MySQL Connector Java] 23:59:14.474 [main] DEBUG org.springframework.jdbc.datasource.DataSourceUtils - Resetting isolation level of JDBC Connection [1168924571, URL=jdbc:mysql://192.168.31.66:3306/artisan?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT, UserName=root@192.168.31.137, MySQL Connector Java] to 4 23:59:14.484 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Releasing JDBC Connection [1168924571, URL=jdbc:mysql://192.168.31.66:3306/artisan?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT, UserName=root@192.168.31.137, MySQL Connector Java] after transaction 23:59:14.484 [main] DEBUG org.springframework.jdbc.datasource.DataSourceUtils - Returning JDBC Connection to DataSource 23:59:14.484 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Creating new transaction with name [com.artisan.ssm_redis.service.impl.RoleServiceImpl.getRole]: PROPAGATION_REQUIRED,ISOLATION_READ_COMMITTED; '' 23:59:14.484 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Acquired Connection [1280429864, URL=jdbc:mysql://192.168.31.66:3306/artisan?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT, UserName=root@192.168.31.137, MySQL Connector Java] for JDBC transaction 23:59:14.484 [main] DEBUG org.springframework.jdbc.datasource.DataSourceUtils - Changing isolation level of JDBC Connection [1280429864, URL=jdbc:mysql://192.168.31.66:3306/artisan?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT, UserName=root@192.168.31.137, MySQL Connector Java] to 2 23:59:14.484 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Switching JDBC Connection [1280429864, URL=jdbc:mysql://192.168.31.66:3306/artisan?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT, UserName=root@192.168.31.137, MySQL Connector Java] to manual commit 23:59:14.484 [main] DEBUG org.springframework.data.redis.core.RedisConnectionUtils - Opening RedisConnection 23:59:14.484 [main] DEBUG org.springframework.data.redis.core.RedisConnectionUtils - Closing Redis Connection 23:59:14.484 [main] DEBUG org.springframework.data.redis.core.RedisConnectionUtils - Opening RedisConnection 23:59:14.494 [main] DEBUG org.springframework.data.redis.core.RedisConnectionUtils - Closing Redis Connection 23:59:14.584 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Initiating transaction commit 23:59:14.584 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Committing JDBC transaction on Connection [1280429864, URL=jdbc:mysql://192.168.31.66:3306/artisan?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT, UserName=root@192.168.31.137, MySQL Connector Java] 23:59:14.594 [main] DEBUG org.springframework.jdbc.datasource.DataSourceUtils - Resetting isolation level of JDBC Connection [1280429864, URL=jdbc:mysql://192.168.31.66:3306/artisan?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT, UserName=root@192.168.31.137, MySQL Connector Java] to 4 23:59:14.594 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Releasing JDBC Connection [1280429864, URL=jdbc:mysql://192.168.31.66:3306/artisan?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT, UserName=root@192.168.31.137, MySQL Connector Java] after transaction 23:59:14.594 [main] DEBUG org.springframework.jdbc.datasource.DataSourceUtils - Returning JDBC Connection to DataSource Role [id=30, roleName=role_name_1, note=role_note_1] 23:59:14.594 [main] DEBUG org.springframework.transaction.annotation.AnnotationTransactionAttributeSource - Adding transactional method 'com.artisan.ssm_redis.service.impl.RoleServiceImpl.updateRole' with attribute: PROPAGATION_REQUIRED,ISOLATION_READ_COMMITTED; '' 23:59:14.594 [main] DEBUG org.springframework.cache.annotation.AnnotationCacheOperationSource - Adding cacheable method 'updateRole' with attribute: [Builder[public int com.artisan.ssm_redis.service.impl.RoleServiceImpl.updateRole(com.artisan.ssm_redis.domain.Role)] caches=[redisCacheManager] | key=''redis_role_'+#role.id' | keyGenerator='' | cacheManager='' | cacheResolver='' | condition='' | unless=''] 23:59:14.594 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Creating new transaction with name [com.artisan.ssm_redis.service.impl.RoleServiceImpl.updateRole]: PROPAGATION_REQUIRED,ISOLATION_READ_COMMITTED; '' 23:59:14.594 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Acquired Connection [90346768, URL=jdbc:mysql://192.168.31.66:3306/artisan?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT, UserName=root@192.168.31.137, MySQL Connector Java] for JDBC transaction 23:59:14.594 [main] DEBUG org.springframework.jdbc.datasource.DataSourceUtils - Changing isolation level of JDBC Connection [90346768, URL=jdbc:mysql://192.168.31.66:3306/artisan?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT, UserName=root@192.168.31.137, MySQL Connector Java] to 2 23:59:14.604 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Switching JDBC Connection [90346768, URL=jdbc:mysql://192.168.31.66:3306/artisan?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT, UserName=root@192.168.31.137, MySQL Connector Java] to manual commit 23:59:14.604 [main] DEBUG org.mybatis.spring.SqlSessionUtils - Creating a new SqlSession 23:59:14.604 [main] DEBUG org.mybatis.spring.SqlSessionUtils - Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@737a135b] 23:59:14.604 [main] DEBUG org.mybatis.spring.transaction.SpringManagedTransaction - JDBC Connection [90346768, URL=jdbc:mysql://192.168.31.66:3306/artisan?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT, UserName=root@192.168.31.137, MySQL Connector Java] will be managed by Spring 23:59:14.604 [main] DEBUG com.artisan.ssm_redis.dao.RoleDao.updateRole - ==> Preparing: update t_role set role_name = ?, note = ? where id = ? 23:59:14.604 [main] DEBUG com.artisan.ssm_redis.dao.RoleDao.updateRole - ==> Parameters: role_name_1(String), role_note_1_update(String), 30(Long) 23:59:14.604 [main] DEBUG com.artisan.ssm_redis.dao.RoleDao.updateRole - <== Updates: 1 23:59:14.604 [main] DEBUG org.mybatis.spring.SqlSessionUtils - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@737a135b] 23:59:14.604 [main] DEBUG org.springframework.data.redis.core.RedisConnectionUtils - Opening RedisConnection 23:59:14.614 [main] DEBUG org.springframework.data.redis.core.RedisConnectionUtils - Closing Redis Connection 23:59:14.614 [main] DEBUG org.mybatis.spring.SqlSessionUtils - Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@737a135b] 23:59:14.614 [main] DEBUG org.mybatis.spring.SqlSessionUtils - Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@737a135b] 23:59:14.614 [main] DEBUG org.mybatis.spring.SqlSessionUtils - Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@737a135b] 23:59:14.614 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Initiating transaction commit 23:59:14.614 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Committing JDBC transaction on Connection [90346768, URL=jdbc:mysql://192.168.31.66:3306/artisan?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT, UserName=root@192.168.31.137, MySQL Connector Java] 23:59:14.614 [main] DEBUG org.springframework.jdbc.datasource.DataSourceUtils - Resetting isolation level of JDBC Connection [90346768, URL=jdbc:mysql://192.168.31.66:3306/artisan?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT, UserName=root@192.168.31.137, MySQL Connector Java] to 4 23:59:14.614 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Releasing JDBC Connection [90346768, URL=jdbc:mysql://192.168.31.66:3306/artisan?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT, UserName=root@192.168.31.137, MySQL Connector Java] after transaction 23:59:14.614 [main] DEBUG org.springframework.jdbc.datasource.DataSourceUtils - Returning JDBC Connection to DataSource Role [id=30, roleName=role_name_1, note=role_note_1_update]
从日志可以看到,先插入了一个角色对象,所以有 insert 语旬的执行,跟着可以看到Redis 连接的打开和关闭, Spring 将值保存到 Redis 中 。
对于 getRole 方法,则没有看到 SQL的执行,因为使用@Cacheable 注解后,它先在 Redis 上查找,找到数据就返回了,所以这里中断了我们本可以看到的 Redis 连接的闭合。
对于 updateRole 方法而言,则是先去执行SQL , 更新数据后 , 再执行 Redis 的命令,这样更新到数据库的数据就和 Redis 的数据同步了。
因为在缓存管理器中设置了超时时间为 10 分钟,所以如果10 分钟后再用相同的 id去调用 getRole 方法,它就会通过调用方法将数据从数据库中取回了。 这里可自行验证。
继续看删除和其他的方法
/** * 使用@CacheEvict删除缓存对应的key * * @param id * 角色编号 * @return 返回删除记录数 */ @Override @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED) @CacheEvict(value = "redisCacheManager", key = "'redis_role_'+#id") public int deleteRole(Long id) { return roleDao.deleteRole(id); }
在方法执行完成后会移除对应的缓存,也就是还可以从方法内读取到缓存服务器中的数据。如果属性 beforelnvocation 声明为 true,则在方法前删除缓存数据,这样就不能在方法中读取缓存数据了,只是这个值的默认值为 false,所以默认的情况下只会在方法后执行删除缓存。
不适用续存的方法:
@Override @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED) public List<Role> findRoles(String roleName, String note) { return roleDao.findRoles(roleName, note); }
findRoles方法我们这里没加缓存,使用缓存的前提一一高命中率,由于这里根据角色名称和备注查找角色信息,该方法的返回值会根据查询条件而多样化,导致其不确定和命中率低下,对于这样的场景,使用缓存并不能有效提高性能,所以这样的场景,就不再使用缓存了。
自调用失效问题:
@Override @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED) public int insertRoles(List<Role> roleList) { for (Role role : roleList) { // 同一类的方法调用自己方法,产生自调用[插入:失效]问题 this.insertRole(role); } return roleList.size(); }
在 insertRoles 方法中调用了同一个类中带有注解@CachePut 的 insertRole 方法,然而 Spring 并没有把对应新增的角色保存到 Redis 缓存上 ,因为缓存注解也是基于 SpringAOP 实现的 ,对于 SpringAOP 的基础是动态代理技术,也就是只有代理对象的相互调用,AOP 才有拦截的功能,才能执行缓存注解提供的功能。而这里的自调用是没有代理对象存在的 ,所以其注解功能也就失效了 。