Redis和数据库的结合

在实际的商用软件使用中,通常都是Redis和关系型数据配置使用,单纯使用Redis来存数据成本太高,并且其持久化和计算能力偏差,这两块无法和关系型数据相比较,而Redis和关系型数据库共存的场景就会带来另一个问题,就是在两者之间的数据一致性的问题,有太多的情况会导致Redis中的数据和关系型数据中的数据不一致,比如关系型数据库事务是完善的,Redis的事务没那么严格出现异常回滚,再比如Redis数据更新了但还没有像关系型数据库同步完成,再比如两个业务线的Redis都会同步关系型数据库数据,一边更新了而另一边就变成了脏数据等等

Redis和数据库读操作

从业务角度而言缓存不应该是永久的,这样极其容易造成脏数据的产生,而Redis也需要时进行垃圾回收给新数据腾出空间,因此一般来说应该加入一个超时时间,这样一旦数据超时,Redis就没法读出超时数据,这时候就会触发程序读取数据库,同步刷新缓存数据并设置超时时间,这样就完成了读操作,如下图所示

互联网应用主流框架整合之Spring缓存机制和Redis结合_cacheput


伪代码可以这样写

public DataObject readMethod(args){
	//尝试从Redis中读取数据
	DataObject data = getFromRedis(key);
	if (data != null){
		return data;
	}
	//从Redis读入不成功,从数据库中获取
	data = getFromDataBase();
	//写入Redis
	writeRedis(key, data);
	//设置key的超时时间为5分钟
	setRedisExprie(key,5);
	return data;
}

Redis和数据库写操作

写操作要考虑数据一致性的问题,所以首先应该从数据库中读取最新数据,然后对数据进行操作

互联网应用主流框架整合之Spring缓存机制和Redis结合_redis_02


写入数据是不能信任缓存的,从数据库中读取最新数据,然后进行业务操作,更新业务数据到数据库,再用新数据刷新Redis缓存,这样就完成了写操作,伪代码可以这样写

public DataObject writeMethod(args){
	//从数据库中读取最新数据
	DataObject dataObject = getFromDataBase(args);
	//执行业务逻辑
	execLogic(dataObject);
	//更新数据库数据
	updateDataBase(dataObject);
	//刷新缓存
	updateRedisData(key, dataObject);
	//设置超时时间
	setRedisExpire(key,5);
}

使用Spring的缓存机制整合Redis

首先建个项目的基本结构和必要基础数据,如下所示

<dependencies>
    <!-- Spring核心包 -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-core</artifactId>
      <version>5.2.1.RELEASE</version>
    </dependency>
    <!-- Spring Bean包 -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-beans</artifactId>
      <version>5.2.1.RELEASE</version>
    </dependency>
    <!-- Spring Context包 -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>5.2.1.RELEASE</version>
    </dependency>
    <!-- Spring Context支持包 -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context-support</artifactId>
      <version>5.2.1.RELEASE</version>
    </dependency>
    <!-- Spring 表达式包 -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-expression</artifactId>
      <version>5.2.1.RELEASE</version>
    </dependency>
    <!-- Spring面向切面(AOP)包 -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-aop</artifactId>
      <version>5.2.1.RELEASE</version>
    </dependency>
    <!-- Spring JDBC包 -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-jdbc</artifactId>
      <version>5.2.1.RELEASE</version>
    </dependency>

    <!-- dbcp2包 -->
    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-dbcp2</artifactId>
      <version>2.7.0</version>
    </dependency>
    <dependency>
      <groupId>org.aspectj</groupId>
      <artifactId>aspectjrt</artifactId>
      <version>1.9.5</version>
    </dependency>
    <dependency>
      <groupId>org.aspectj</groupId>
      <artifactId>aspectjweaver</artifactId>
      <version>1.9.5</version>
    </dependency>
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis-spring</artifactId>
      <version>2.0.3</version>
    </dependency>
    <!-- MyBatis包 -->
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      <version>3.5.3</version>
    </dependency>
    <!-- MySQL驱动包 -->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.29</version>
    </dependency>
    <!-- 实现slf4j接口并整合 -->
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>1.7.26</version>
    </dependency>
    <!-- Spring Web 和 MVC -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-web</artifactId>
      <version>5.2.1.RELEASE</version>
    </dependency>
    <!-- POJO的验证 -->
    <dependency>
      <groupId>org.hibernate.validator</groupId>
      <artifactId>hibernate-validator</artifactId>
      <version>6.1.0.Final</version>
    </dependency>
    <!-- EXCEL -->
    <dependency>
      <groupId>org.apache.poi</groupId>
      <artifactId>poi-ooxml</artifactId>
      <version>4.1.1</version>
    </dependency>


    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-lang3</artifactId>
      <version>3.9</version>
    </dependency>


    <dependency>
      <groupId>commons-fileupload</groupId>
      <artifactId>commons-fileupload</artifactId>
      <version>1.4</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>5.2.1.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-core</artifactId>
      <version>2.10.1</version>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.10.1</version>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-annotations</artifactId>
      <version>2.10.1</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-log4j12</artifactId>
      <version>1.7.26</version>
    </dependency>
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-core</artifactId>
      <version>2.11.2</version>
    </dependency>
    <!-- Spring Data Redis -->
    <dependency>
      <groupId>org.springframework.data</groupId>
      <artifactId>spring-data-redis</artifactId>
      <version>2.2.4.RELEASE</version>
    </dependency>
    <!-- Jedis -->
    <dependency>
      <groupId>redis.clients</groupId>
      <artifactId>jedis</artifactId>
      <version>3.2.0</version>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.13</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
package com.sr.pojo;

import java.io.Serializable;

import org.apache.ibatis.type.Alias;

// MyBatis别名
@Alias(value = "role")
public class Role implements Serializable {
    // 序列号
    private static final long serialVersionUID = 5107424510097186591L;

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

}

这个POJO实现了序列化Serializable接口,则可以将这个POJO通过Spring的序列化器保存为对应的编码,用于存储在Redis中,然后使用它的时候可以从Redis中读取,再进行反序列化;类上加了别名注解,MyBatis扫描机制会识别到它,然后定义Mapper用于操作数据库

<?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.sr.dao.RoleDao">

    <select id="getRole" resultType="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="role" useGeneratedKeys="true" keyProperty="id">
        insert into t_role (role_name, note) values(#{roleName}, #{note})
    </insert>

    <update id="updateRole" parameterType="role">
        update t_role set role_name = #{roleName}, note = #{note}
        where id = #{id}
    </update>

    <select id="findRoles" resultType="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>

然后需要一个MyBatis角色接口,以便使用Mapper文件

package com.sr.dao;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import com.sr.pojo.Role;

@Mapper
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);
}

@Mapper表示它是一个持久层的接口,未来可以通过MyBatis扫描机制将其装配到SpringIoC中,至此Dao层就完成了,接下来是角色服务接口RoleService ,如下所示

package com.sr.service;
import java.util.List;

import com.sr.pojo.Role;

public interface RoleService {
    public Role getRole(Long id);

    public int deleteRole(Long id);

    public Role insertRole(Role role);

    public Role updateRole(Role role);

    public List<Role> findRoles(String roleName, String note);

    int insertRoles(List<Role> roleList);
}

接下来配置数据库和MyBatis相关内容,如下所示

package com.sr.config;


import java.util.Properties;

import javax.sql.DataSource;

import org.apache.commons.dbcp2.BasicDataSourceFactory;
import org.apache.ibatis.annotations.Mapper;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.annotation.TransactionManagementConfigurer;


@ComponentScan("com.sr")
// 配置MyBatis映射器扫描规则
@MapperScan(
        basePackages = "com.sr",
        annotationClass = Mapper.class,
        sqlSessionFactoryRef = "sqlSessionFactory"
)
//使用事务驱动管理器
@EnableTransactionManagement
public class DataBaseConfig implements TransactionManagementConfigurer {

    DataSource dataSource = null;
    /**
     * 配置数据库
     *
     * @return 数据连接池
     */
    @Bean(name = "dataSource")
    public DataSource initDataSource() {
        if (dataSource != null) {
            return dataSource;
        }
        Properties props = new Properties();
        props.setProperty("driverClassName", "com.mysql.jdbc.Driver");
        props.setProperty("url", "jdbc:mysql://localhost:3306/ssm");
        props.setProperty("username", "root");
        props.setProperty("password", "xxxxxx");
        try {
            dataSource = BasicDataSourceFactory.createDataSource(props);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return dataSource;
    }

    /**
     * * 配置SqlSessionFactoryBean
     *
     * @return SqlSessionFactoryBean
     */
    @Bean(name = "sqlSessionFactory")
    public SqlSessionFactoryBean initSqlSessionFactory(@Autowired DataSource dataSource) {
        SqlSessionFactoryBean sqlSessionFactory = new SqlSessionFactoryBean();
        sqlSessionFactory.setDataSource(dataSource);
        // 配置MyBatis配置文件
        Resource resource = new ClassPathResource("mybatis-config.xml");
        sqlSessionFactory.setConfigLocation(resource);
        return sqlSessionFactory;
    }

    /**
     * * 通过自动扫描,发现MyBatis Mapper接口
     *
     * @return Mapper扫描器
     */
    @Bean
    public MapperScannerConfigurer initMapperScannerConfigurer() {
        MapperScannerConfigurer msc = new MapperScannerConfigurer();
        // 扫描包
        msc.setBasePackage("com.*");
        msc.setSqlSessionFactoryBeanName("sqlSessionFactory");
        // 区分注解扫描
        msc.setAnnotationClass(Mapper.class);
        return msc;
    }

    /**
     * 实现接口方法,注册注解事务,当@Transactional 使用的时候产生数据库事务
     */
    @Override
    @Bean(name = "annotationDrivenTransactionManager")
    public PlatformTransactionManager annotationDrivenTransactionManager() {
        DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
        transactionManager.setDataSource(initDataSource());
        return transactionManager;
    }

}

代码中在initSqlSessionFactory方法中,通过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>

    <!-- 定义别名 -->
    <typeAliases>
        <package name="com.sr.pojo"/>
    </typeAliases>

    <!-- 引入映射文件 -->
    <mappers>
        <mapper resource="RoleMapper.xml" />
    </mappers>

</configuration>

Spring缓存管理器

在Spring中提供了CacheManager接口来定义缓存管理器,不同的缓存可以通过实现这个接口来完成该对缓存的管理,在spring-data.redis.jar包中实现CacheManager接口的是类RedisCacheManager,因此需要在SpringIoC容器中装配它的实例,在Spring5.x版本这个实例是通过构造器模式来完成实例构建的,如下代码所示

package com.sr.config;


import java.time.Duration;

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.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheManager.RedisCacheManagerBuilder;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.jedis.JedisClientConfiguration;
import org.springframework.data.redis.connection.jedis.JedisClientConfiguration.JedisClientConfigurationBuilder;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair;

import redis.clients.jedis.JedisPoolConfig;

@Configuration
// 驱动缓存工作
@EnableCaching
public class RedisConfig {
    @Bean("redisPoolConfig")
    public JedisPoolConfig poolConfig() {
        JedisPoolConfig poolCfg = new JedisPoolConfig();
        // 最大空闲数
        poolCfg.setMaxIdle(50);
        // 最大连接数
        poolCfg.setMaxTotal(100);
        // 最大等待毫秒数
        poolCfg.setMaxWaitMillis(20000);
        return poolCfg;
    }
    /**
     * 创建Jedis连接工厂
     * @param jedisPoolConfig
     * @return 连接工厂
     */
    @Bean("redisConnectionFactory")
    public RedisConnectionFactory redisConnectionFactory(@Autowired JedisPoolConfig jedisPoolConfig) {
        // 独立Jedis配置
        RedisStandaloneConfiguration rsc = new RedisStandaloneConfiguration();
        // 设置Redis服务器
        rsc.setHostName("192.168.80.130");
        // 如需要密码,设置密码
        rsc.setPassword("abcdefg");
        // 端口
        rsc.setPort(6379);
        // 获得默认的连接池构造器
        JedisClientConfigurationBuilder jpcb = JedisClientConfiguration.builder();
        // 设置Redis连接池
        jpcb.usePooling().poolConfig(jedisPoolConfig);
        // 获取构建器
        JedisClientConfiguration jedisClientConfiguration = jpcb.build();
        // 创建连接工厂
        return new JedisConnectionFactory(rsc, jedisClientConfiguration);
    }
    /**
     * 创建RedisTemplate
     * @param connectionFactory Redis连接工厂
     * @return RedisTemplate对象
     */
    @Bean("redisTemplate")
    public RedisTemplate<String, Object> redisTemplate(@Autowired RedisConnectionFactory connectionFactory) {
        // 创建RedisTemplate
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        // 字符串和JDK序列化器
        RedisSerializer<String> strSerializer = RedisSerializer.string();
        RedisSerializer<Object> jdkSerializer = RedisSerializer.java();
        // 设置键值序列化器
        redisTemplate.setKeySerializer(strSerializer);
        redisTemplate.setValueSerializer(jdkSerializer);
        // 设置哈希字段和值序列化器
        redisTemplate.setHashKeySerializer(strSerializer);
        redisTemplate.setHashValueSerializer(jdkSerializer);
        // 给RedisTemplate设置连接工厂
        redisTemplate.setConnectionFactory(connectionFactory);
        return redisTemplate;
    }
    /**
     * 创建StringRedisTemplate
     * @param connectionFactory 连接工厂
     * @return StringRedisTemplate对象
     */
    @Bean("stringRedisTemplate")
    public StringRedisTemplate  stringRedisTemplate(@Autowired RedisConnectionFactory connectionFactory) {
        // 创建StringRedisTemplate对象
        StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
        // 设置连接工厂
        stringRedisTemplate.setConnectionFactory(connectionFactory);
        return stringRedisTemplate;
    }

	@Bean(name = "redisCacheManager")
  	public CacheManager initRedisCacheManager(@Autowired RedisConnectionFactory redisConnectionFactory) {
        // 获取Redis缓存默认配置
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
        // 构建Redis缓存管理器
        RedisCacheManager cacheManager = RedisCacheManagerBuilder.fromConnectionFactory(redisConnectionFactory)
                // 定义缓存管理器名称和配置,这样可以后续进行引用
                .withCacheConfiguration("redisCacheManager", config)
                .build();
        return cacheManager;
    }
}

注解@EnableCaching表示将启动Spring缓存机制, @Bean(name = "redisCacheManager")则是定义缓存管理器,配置了缓存管理器后,Spring允许用注解的方式使用缓存(XML也可以使用缓存管理器但用的不多,避免XML泛滥),有几个比较重要的注解,如下表格所示

互联网应用主流框架整合之Spring缓存机制和Redis结合_redis_03


@Cacheable@CachePut都可以缓存键值对,方式略有不同,他们只能运用于有返回值的方法中;删除缓存Key的@CacheEvict注解则可以用到void方法上,上述注解都能被标注到类和方法上,如果标注在类上则对所有方法生效,如果标注到方法上则对被标注的方法生效;一般而言对于查询更多使用注解@Cacheable,对于插入和修改使用注解@CachePut,对于删除则是CacheEvict

@Cacheable和@CachePut

互联网应用主流框架整合之Spring缓存机制和Redis结合_spring_04


@Cacheable@CachePut两个注解的配置项比较接近,如上表所示其中value和key最为常用,value是个数组可以引用多个缓存管理器,key则是缓存中的键,它只是Spring表达式,通过Spring表达式可以自定义缓存的key,为了自定义key则需要知道Spring表达式和缓存注解之间的约定,如下表所示

互联网应用主流框架整合之Spring缓存机制和Redis结合_缓存_05


通过这些约定引用方法的参数和返回值内容,使其能够注入key定义的Spring表达式的结果中,这样就能使用对应的参数和返回值作为缓存的Key了,然后再看一下实际使用这些内容实现RoleService服务接口的实现类,如下所示

package com.sr.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.sr.dao.RoleDao;
import com.sr.pojo.Role;
import com.sr.service.RoleService;

@Service
public class RoleServiceImpl implements RoleService {

    // 角色DAO,方便执行SQL
    @Autowired
    private RoleDao roleDao = null;

    /**
     * 使用@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 Role updateRole(Role role) {
        roleDao.updateRole(role);
        return role;
    }

    /**
     * 使用@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);
    }

    @Override
    @Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRED)
    public List<Role> findRoles(String roleName, String note) {
        return roleDao.findRoles(roleName, note);
    }

    @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();
    }
}
  • getRole方法:因为它是一个查询方法,所以使用注解@Cacheable这样在Spring的调用,就会先查询Redis,看看是否存在对应的值,那么采用什么key查询呢? 是由注解中的key属性定义的,它配置的是'redis_role_'+#id, Spring EL会计算返回一个key,以参数id为1L为例,此时key计算结果为redis_role_1,可以以它为key访问Redis,如果Redis存在数据,那么就返回,不再执行getRole方法,否则就继续执行getRole方法,最后返回值再以redis_role_1为key保存到Redis中,以后通过这个key访问Redis数据
  • insertRole方法:这里需要先执行方法,最后才能把返回的信息保存到Redis中,所以采用的是@CachePut,由于主键由数据库生成,所以无法从参数中读取,但是可以从结果中读取,#result.id的写法就是获取方法返回的角色id,而这个id是通过数据库生成然后由MyBatis回填得到的,这样就可以在Redis中新增一个key然后保存对应的对象了
  • udpateRole方法:采用注解@CachePut,由于对象有所更新,所以要在方法之后更新Redis数据,以保证数据一致性,直接读取参数id,表达式写成#role.id,可以引入角色参数的id,在方法结束后它会更新Redis对应的key的值

测试代码如下所示

package com.sr.main;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import com.sr.config.DataBaseConfig;
import com.sr.config.RedisConfig;
import com.sr.pojo.Role;
import com.sr.service.RedisTemplateService;
import com.sr.service.RoleService;

public class SrMain {

    public static void main(String[] args) {
        testCache1();
    }

    public static void testCache1() {
        // 使用注解Spring IoC容器
        ApplicationContext ctx = new AnnotationConfigApplicationContext(DataBaseConfig.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 getRole = roleService.getRole(role.getId());
        getRole.setNote("role_note_1_update");
        // 更新角色
        roleService.updateRole(getRole);
        System.out.println("id = " + getRole.getId());
    }
}
@CacheEvict

注解@CacheEvict主要用于删除缓存对应的键值对,其属性配置如下表所示

互联网应用主流框架整合之Spring缓存机制和Redis结合_redis_06


其中value和key与之前的注解@Cacheable@CachePut是一致的,属性allEntries要求删除缓存服务器中所有的缓存,这个时候指定的key是不会生效的,beforeInvocation属性指定在方法前或者方法后删除缓存

/**
     * 使用@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);
    }

如代码所示,它在方法执行完成后删除缓存,也就是说它可以从方法内读取到缓存服务器中的数据,如果将注解@CacheEvict的属性声明为true,则在方法前删除缓存数据,这样就不能在方法中读取缓存数据了,只是这个属性默认为false,所以只会在方法执行完成后删除缓存

不适用缓存的方法
@Override
    @Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRED)
    public List<Role> findRoles(String roleName, String note) {
        return roleDao.findRoles(roleName, note);
    }

这里根据角色和备注查询角色信息,所以该方法的返回值具有不确定性,并且命中率很低,对于这样的场景使用缓存并不能有效的提高性能,此外返回的结果写的概率大于读的概率也没必要使用缓存,还有如果返回的结果严重消耗内存,也需要考虑是否使用缓存

自调用失效问题
@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中,也就是缓存注解失效了,因为缓存注解是基于Spring AOP实现的,而Spring AOP的基础是动态代理技术,也就是只有被代理对象调用,AOP才有拦截的功能,才能执行缓存注解提供的功能,而这里的自调用是没有代理对象的,所以注解功能失效

Redis缓存管理器配置RedisCacheConfiguration

之前的代码使用默认的配置来创建RedisCacheConfiguration对象的,这样存在一个问题,必须序列化器、超时时间等也都是默认的,RedisCacheConfiguration这个配置类源码中的构造函数如下所示

/**
 * 构造函数,用于初始化Redis缓存配置
 * 
 * @param ttl 缓存过期时间,null表示永不过期
 * @param cacheNullValues 是否缓存空值,有助于减少某些场景下的数据库查询次数
 * @param usePrefix 是否使用键前缀,可以增强缓存键的可读性和维护性
 * @param keyPrefix 缓存键的前缀,用于区分不同业务的缓存
 * @param keySerializationPair 键的序列化对,指定如何序列化和反序列化缓存键
 * @param valueSerializationPair 值的序列化对,指定如何序列化和反序列化缓存值
 * @param conversionService 转换服务,用于在序列化和反序列化过程中转换数据类型
 */
private RedisCacheConfiguration(Duration ttl, Boolean cacheNullValues, Boolean usePrefix, CacheKeyPrefix keyPrefix, RedisSerializationContext.SerializationPair<String> keySerializationPair, RedisSerializationContext.SerializationPair<?> valueSerializationPair, ConversionService conversionService) {
    this.ttl = ttl;
    this.cacheNullValues = cacheNullValues;
    this.usePrefix = usePrefix;
    this.keyPrefix = keyPrefix;
    this.keySerializationPair = keySerializationPair;
    this.valueSerializationPair = valueSerializationPair;
    this.conversionService = conversionService;
}

该构造函数使用private修饰,则我们不能用new来创建对象,RedisCacheConfiguration提供了静态方法,如下代码所示

@Bean(name = "redisCacheManager")
    public CacheManager initRedisCacheManager(@Autowired RedisConnectionFactory redisConnectionFactory) {
        // 创建两个序列化器对
        SerializationPair<String> strSerializer = SerializationPair.fromSerializer(RedisSerializer.string());
        SerializationPair<Object> jdkSerializer = SerializationPair.fromSerializer(RedisSerializer.java());
        RedisCacheConfiguration config = RedisCacheConfiguration
                // 获取默认配置
                .defaultCacheConfig()
                // 设置超时时间
                .entryTtl(Duration.ofMinutes(30L))
                // 禁用前缀
                .disableKeyPrefix()
                // 自定义前缀
                // .prefixKeysWith("prefix")
                // 设置key序列化器
                .serializeKeysWith(strSerializer)
                // 设置value序列化器
                .serializeValuesWith(jdkSerializer)
                // 不缓冲空值
                .disableCachingNullValues();
        // 构建Redis缓存管理器
        RedisCacheManager cacheManager = RedisCacheManagerBuilder.fromConnectionFactory(redisConnectionFactory)
                        // 定义缓存管理器名称和配置,这样可以后续进行引用
                        .withCacheConfiguration("redisCacheManager", config)
                        .build();
        return cacheManager;
    }

一条方法链构建了一个新的RedisCacheConfiguration对象,如此便解决了默认配置的问题

RedisTemplate实例

很多时候,我们需要用一些更为高级的缓存服务器的API,例如Redis流水线、事务、和Lua脚本等,则可以使用如下方式,首先定义一个接口,代码如下

package com.sr.service;

public interface RedisTemplateService {
    /**
     * 执行多个命令
     */
    public void execMultiCommand();

    /**
     * 执行Redis事务
     */
    public void execTransaction();

    /**
     * 执行Redis流水线
     */
    public void execPipeline();
}

实现类如下所示

package com.sr.service.impl;


import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import com.sr.service.RedisTemplateService;

@Service
public class RedisTemplateServiceImpl implements RedisTemplateService {

    @Autowired
    private StringRedisTemplate redisTemplate = null;

    /**
     * 使用SessionCallback接口实现多个命令在一个Redis连接中执行
     */
    @Override
    public void execMultiCommand() {
        // 使用Java 8 lambda表达式
        SessionCallback session = ops -> {
            ops.boundValueOps("key1").set("abc");
            ops.boundHashOps("hash").put("hash-key-1", "hash-value-1");
            return ops.boundValueOps("key1").get();
        };

        String value = (String) redisTemplate.execute(session);
        System.out.println(value);
    }

    /**
     * 使用SessionCallback接口实现事务在一个Redis连接中执行
     */
    @Override
    public void execTransaction() {
        // 使用Java 8 lambda表达式
        SessionCallback session = ops -> {
            // 监控
            ops.watch("key1");
            // 开启事务
            ops.multi();
            // 注意,命令都不会被马上执行,只会放到Redis的队列中,只会返回为null
            ops.boundValueOps("key1").set("abc");
            ops.boundHashOps("hash").put("hash-key-1", "hash-value-1");
            ops.opsForValue().get("key1");
            // 执行exec方法后会触发事务执行,返回结果,存放到list中
            List result = ops.exec();
            return result;
        };
        List list = (List) redisTemplate.execute(session);
        System.out.println(list);
    }

    /**
     * 执行流水线,将多个命令一次性发送给Redis服务器
     */
    @Override
    public void execPipeline() {
        // 使用匿名类实现
        SessionCallback session = new SessionCallback() {
            @Override
            public Object execute(RedisOperations ops)
                    throws DataAccessException {
                // 在流水线下,命令不会马上返回结果,结果是一次性执行后返回的
                ops.opsForValue().set("key1", "value1");
                ops.opsForHash().put("hash", "key-hash-1", "value-hash-1");
                ops.opsForValue().get("key1");
                return null;
            };
        };
        List list = redisTemplate.executePipelined(session);
        System.out.println(list);
    }

}

测试方法如下

public static void redistemplateservice() {
        // 使用注解Spring IoC容器
        ApplicationContext ctx = new AnnotationConfigApplicationContext(DataBaseConfig.class, RedisConfig.class);
        RedisTemplateService redisTemplateService = ctx.getBean(RedisTemplateService.class);
        redisTemplateService.execMultiCommand();
        redisTemplateService.execPipeline();
        redisTemplateService.execPipeline();
    }