Redis和数据库的结合
在实际的商用软件使用中,通常都是Redis和关系型数据配置使用,单纯使用Redis来存数据成本太高,并且其持久化和计算能力偏差,这两块无法和关系型数据相比较,而Redis和关系型数据库共存的场景就会带来另一个问题,就是在两者之间的数据一致性的问题,有太多的情况会导致Redis中的数据和关系型数据中的数据不一致,比如关系型数据库事务是完善的,Redis的事务没那么严格出现异常回滚,再比如Redis数据更新了但还没有像关系型数据库同步完成,再比如两个业务线的Redis都会同步关系型数据库数据,一边更新了而另一边就变成了脏数据等等
Redis和数据库读操作
从业务角度而言缓存不应该是永久的,这样极其容易造成脏数据的产生,而Redis也需要时进行垃圾回收给新数据腾出空间,因此一般来说应该加入一个超时时间,这样一旦数据超时,Redis就没法读出超时数据,这时候就会触发程序读取数据库,同步刷新缓存数据并设置超时时间,这样就完成了读操作,如下图所示
伪代码可以这样写
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和数据库写操作
写操作要考虑数据一致性的问题,所以首先应该从数据库中读取最新数据,然后对数据进行操作
写入数据是不能信任缓存的,从数据库中读取最新数据,然后进行业务操作,更新业务数据到数据库,再用新数据刷新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泛滥),有几个比较重要的注解,如下表格所示
@Cacheable
和@CachePut
都可以缓存键值对,方式略有不同,他们只能运用于有返回值的方法中;删除缓存Key的@CacheEvict
注解则可以用到void方法上,上述注解都能被标注到类和方法上,如果标注在类上则对所有方法生效,如果标注到方法上则对被标注的方法生效;一般而言对于查询更多使用注解@Cacheable
,对于插入和修改使用注解@CachePut
,对于删除则是CacheEvict
@Cacheable和@CachePut
@Cacheable
和@CachePut
两个注解的配置项比较接近,如上表所示其中value和key最为常用,value是个数组可以引用多个缓存管理器,key则是缓存中的键,它只是Spring表达式,通过Spring表达式可以自定义缓存的key,为了自定义key则需要知道Spring表达式和缓存注解之间的约定,如下表所示
通过这些约定引用方法的参数和返回值内容,使其能够注入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
主要用于删除缓存对应的键值对,其属性配置如下表所示
其中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();
}