文章目录

  • 概述
  • Redis 和数据库读
  • Redis和数据库写
  • 使用 Spring 缓存机制整合 Redis
  • 工程结构
  • pom.xml
  • DB Script & Redis Address
  • POJO类
  • 搭建MyBatis环境
  • RoleMapper.xml
  • RoleDao接口
  • Service层接口
  • 基于Java类的配置定义数据库和相关的扫描内容
  • Spring的缓存管理器
  • Service层整合缓存

概述

这里用一个示例通过注解的方式整合 Spring 和 Redis,要特别注意 Redis 缓存和数据库一致性的问题。


Redis 和数据库读

数据缓存往往会在 Redis 上设置超时时间,当设置 Redis 的数据超时后, Redis 就没法读出数据了 , 这个时候就会触发程序读取数据库 , 然后将读取的数据库数据写入 Redis (此时会给 Redis 重设超时时间 ),这样程序在读取的过程中就能按一定的时间间隔刷新数据了。

流程图大致如下:

Redis-20Spring缓存机制整合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 缓存中.

流程大致如下

Redis-20Spring缓存机制整合Redis_sql_02

写入业务数据,先从数据库中读取最新数据,然后进行业务操作,更新业务数据到数据库后,再将数据刷新到 Redis 缓存中,这样就完成了一次写操作。这样的操作就能避免将脏数据写入数据库中,这类问题在操作时要注意。

伪代码大致如下

public  void writeData(args){
//从数据库里读取最新数据
DataObject dataObject = getFromDataBase(args);
//执行业务逻辑
execLogic(dataObject);
//更新数据库数据
updateDataBase(dataObject );
// 刷新 Red is 缓存
updateRedisData(dataObject ) ;
}

上面的伪代码完成了上图所描述的过程。首先,从数据库中读取最新的数据,以规避缓存中的脏数据问题,执行了逻辑,修改了部分业务数据。然后,把这些数据保存到数据库里,最后,刷新这些数据到 Redis 中。


使用 Spring 缓存机制整合 Redis

工程结构

Redis-20Spring缓存机制整合Redis_sql_03

用到了

  • Spring
  • Spring Cache
  • Mybatis
  • Redis

需要将上述3者整合起来


pom.xml

<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>

DB Script & Redis Address

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)
);

Redis-20Spring缓存机制整合Redis_spring_04


POJO类

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 对象。


搭建MyBatis环境

RoleMapper.xml

<?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>

RoleDao接口

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的内容。


Service层接口

定义角色服务接口( 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);
}

基于Java类的配置定义数据库和相关的扫描内容

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的缓存管理器

在 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, 名称是为了方便后面注解引用的 。


Service层整合缓存

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 才有拦截的功能,才能执行缓存注解提供的功能。而这里的自调用是没有代理对象存在的 ,所以其注解功能也就失效了 。