Redis-20Spring缓存机制整合Redis

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 Tair(兼容Redis),内存型 2GB
简介: Redis-20Spring缓存机制整合Redis

概述

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


Redis 和数据库读


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


流程图大致如下:


20181007215050243.png



伪代码大致如下

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 缓存中.

流程大致如下


20181007221331818.png


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

伪代码大致如下

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

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


使用 Spring 缓存机制整合 Redis

工程结构

20181007231345564.png


用到了

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


20181007224609134.png


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

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
相关文章
|
4月前
|
缓存 NoSQL 关系型数据库
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
|
3月前
|
人工智能 JSON 安全
Spring Boot实现无感刷新Token机制
本文深入解析在Spring Boot项目中实现JWT无感刷新Token的机制,涵盖双Token策略、Refresh Token安全性及具体示例代码,帮助开发者提升用户体验与系统安全性。
339 5
|
4月前
|
缓存 NoSQL Java
Redis+Caffeine构建高性能二级缓存
大家好,我是摘星。今天为大家带来的是Redis+Caffeine构建高性能二级缓存,废话不多说直接开始~
637 0
|
5月前
|
缓存 并行计算 PyTorch
PyTorch CUDA内存管理优化:深度理解GPU资源分配与缓存机制
本文深入探讨了PyTorch中GPU内存管理的核心机制,特别是CUDA缓存分配器的作用与优化策略。文章分析了常见的“CUDA out of memory”问题及其成因,并通过实际案例(如Llama 1B模型训练)展示了内存分配模式。PyTorch的缓存分配器通过内存池化、延迟释放和碎片化优化等技术,显著提升了内存使用效率,减少了系统调用开销。此外,文章还介绍了高级优化方法,包括混合精度训练、梯度检查点技术及自定义内存分配器配置。这些策略有助于开发者在有限硬件资源下实现更高性能的深度学习模型训练与推理。
926 0
|
2月前
|
JSON 前端开发 Java
Spring MVC 核心组件与请求处理机制详解
本文解析了 Spring MVC 的核心组件及请求流程,核心组件包括 DispatcherServlet(中央调度)、HandlerMapping(URL 匹配处理器)、HandlerAdapter(执行处理器)、Handler(业务方法)、ViewResolver(视图解析),其中仅 Handler 需开发者实现。 详细描述了请求执行的 7 步流程:请求到达 DispatcherServlet 后,经映射器、适配器找到并执行处理器,再通过视图解析器渲染视图(前后端分离下视图解析可省略)。 介绍了拦截器的使用(实现 HandlerInterceptor 接口 + 配置类)及与过滤器的区别
183 0
|
2月前
|
存储 缓存 NoSQL
Spring Cache缓存框架
Spring Cache是Spring体系下的标准化缓存框架,支持多种缓存(如Redis、EhCache、Caffeine),可独立或组合使用。其优势包括平滑迁移、注解与编程两种使用方式,以及高度解耦和灵活管理。通过动态代理实现缓存操作,适用于不同业务场景。
254 0
|
4月前
|
消息中间件 缓存 NoSQL
基于Spring Data Redis与RabbitMQ实现字符串缓存和计数功能(数据同步)
总的来说,借助Spring Data Redis和RabbitMQ,我们可以轻松实现字符串缓存和计数的功能。而关键的部分不过是一些"厨房的套路",一旦你掌握了这些套路,那么你就像厨师一样可以准备出一道道饕餮美食了。通过这种方式促进数据处理效率无疑将大大提高我们的生产力。
168 32
|
4月前
|
缓存 NoSQL 算法
Redis数据库的键值过期和删除机制
我们需要注意的是,虽然Redis提供了这么多高级的缓存机制,但在使用过程中,必须理解应用的特性,选择合适的缓存策略,才能最大化Redis的性能。因此,在设计和实施应用程序时,理解应用的数据访问模式,以及这些模式如何与Redis的缓存机制相互作用,尤为重要。
179 24
|
4月前
|
缓存 NoSQL Java
Redis:现代服务端开发的缓存基石与电商实践-优雅草卓伊凡
Redis:现代服务端开发的缓存基石与电商实践-优雅草卓伊凡
94 5
Redis:现代服务端开发的缓存基石与电商实践-优雅草卓伊凡
|
6月前
|
缓存 监控 NoSQL
Redis--缓存击穿、缓存穿透、缓存雪崩
缓存击穿、缓存穿透和缓存雪崩是Redis使用过程中可能遇到的常见问题。理解这些问题的成因并采取相应的解决措施,可以有效提升系统的稳定性和性能。在实际应用中,应根据具体场景,选择合适的解决方案,并持续监控和优化缓存策略,以应对不断变化的业务需求。
1050 29