Spring Boot中开启Redis Cache并使用缓存注解

前序工作

该文章为如下两个工作的后续内容,在该文章的操作之前需要首先完成redis的安装和配置,以及Spring Boot和Redis的整合:

开启Spring Redis Cache

在Spring Boot中开启Redis Cache的过程比较简单,首先在application.properities配置文件中加入如下的redis cache配置项:

# Spring Redis Cache
# 设置缓存类型,这里使用Redis作为缓存服务器
spring.cache.type=REDIS
# 定义cache名称,用于在缓存注解中引用,多个名称可以使用逗号分隔
spring.cache.cache-names=redisCache
# 允许保存空值
spring.cache.redis.cache-null-values=true
# 自定义缓存前缀
#spring.cache.redis.key-prefix=
# 是否使用前缀
spring.cache.redis.use-key-prefix=true
# 设置缓存失效时间,0或者默认为永远不失效
spring.cache.redis.time-to-live=0

上面的配置已经使用注释进行了说明,该配置其实是为缓存管理器CacheManager进行设置,这里将spring.cache.type设置为REDIS,即指定缓存管理器为RedisCacheManager。完成上述的配置,Spring Boot即会自动创建相应的缓存管理器来进行缓存的相关操作。

为了使用缓存管理器,还需要在Redis的配置类(或者整个项目的启动类)中加入驱动缓存的注解,这里继续Spring Boot集成Redis与使用RedisTemplate进行基本数据结构操作示例中配置类中添加该注解:

@Configuration
@EnableCaching // 开启Spring Redis Cache,使用注解驱动缓存机制
public class SpringRedisConfiguration {
    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 利用Bean生命周期使用PostConstruct注解自定义后初始化方法
     */
    @PostConstruct
    public void init() {
        initRedisTemplate();
    }

    /**
     * 设置RedisTemplate的序列化器
     */
    private void initRedisTemplate() {
        RedisSerializer stringSerializer = redisTemplate.getStringSerializer();
        // 将Key和其散列表数据类型的filed都修改为使用StringRedisSerializer进行序列化
        redisTemplate.setKeySerializer(stringSerializer);
        redisTemplate.setHashKeySerializer(stringSerializer);
    }
}

这样即完成了cache的开启,下面构建cache的相关测试逻辑来使用缓存注解操作缓存数据。

注,这里同时在springboot的配置文件中设置了默认的数据库隔离级别为读写提交,以避免在使用数据库事务使产生脏读:

# 设置默认的隔离级别为读写提交
spring.datasource.tomcat.default-transaction-isolation=2

创建测试逻辑

下面创建简单的java对象,以及相应的操作逻辑,来使用缓存注解对Redis的cache进行管理。

简单POJO对象

@Data
@Alias(value = "user")
public class User implements Serializable {
    // 开启Spring Redis Cache时,加入序列化
    private static final long serialVersionUID = -4947062488310146862L;

    private Long id;
    @NotNull(message = "用户名不能为空")
    private String userName;
    @NotNull(message = "备注不能为空")
    private String note;
    @NotNull(message = "性别不能为空")
    private SexEnum sex;
}

MyBatis映射接口及配置文件

MyBatis映射接口如下:

@Repository
public interface UserMapper {
    User getUserById(Long id);

    int insertUser(User user);

    int updateUserById(User user);

    int deleteUser(Long id);

    @Select("select * from t_user where user_name = #{userName}")
    List<User> getUsersByName(@Param("userName") String userName);
}

相应的映射文件如下(这里在操作MyBatis是同时使用了注解和映射文件,可以选择都是使用简单的注解或都使用映射文件进行实现) :

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org/DTD Mapper 3.0" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.zyt.springbootlearning.dao.UserMapper">

    <select id="getUserById" parameterType="long" resultType="user">
        select id, user_name as userName, sex, note from t_user where id = #{id}
    </select>

    <insert id="insertUser" parameterType="cn.zyt.springbootlearning.domain.User" useGeneratedKeys="true" keyProperty="id">
        insert into t_user (user_name, sex, note) values (#{userName}, #{sex}, #{note})
    </insert>

    <update id="updateUserById" parameterType="cn.zyt.springbootlearning.domain.User">
        update t_user set user_name=#{userName}, sex=#{sex}, note=#{note} where id=#{id}
    </update>

    <delete id="deleteUser" parameterType="long">
        delete from t_user where id=#{id}
    </delete>
</mapper>

Service类

创建相关的service类和其实现类:

public interface UserCacheService {
    User getUser(Long id);
    User insertUser(User user);
    User updateUserName(Long id, String userName);
    List<User> findUsers(String userName);
    int deleteUser(Long id);
}

其实现类如下:

@Service
public class UserCacheServiceImpl implements UserCacheService {

    @Autowired
    private UserMapper userMapper;

    /**
     * 使用Transactional注解开启事务
     * Cacheable注解表示先从缓存中通过定义的键值进行查询,如果查询不到则进行数据库查询并将查询结果保存到缓存中,其中:
     * value属性为spring application.properties配置文件中设置的缓存名称
     * key表示缓存的键值名称,其中id说明该方法需要一个名为id的参数
     */
    @Override
    @Transactional
    @Cacheable(value = "redisCache", key = "'redis_user_'+#id")
    public User getUser(Long id) {
        return userMapper.getUserById(id);
    }

    /**
     * CachePut注解表示将方法的返回结果存放到缓存中
     * value和key属性与上述意义一样,需要注意的是,key中的使用了result.id的方式
     * 这里的result表示该方法的返回值对象,即为user,id为取该对象的id属性值
     *
     * 这里在插入user时,传入的user参数是不存在id属性的,在mapper.xml文件中insertUser使用了如下的属性设置:
     * useGeneratedKeys="true" keyProperty="id"
     * 意味着,user的id属性会进行自增,并在use插入成功后会将指定的id属性进行回填,因此如下方法的返回值为带有id属性的完整user对象
     */
    @Override
    @Transactional
    @CachePut(value = "redisCache", key = "'redis_user_'+#result.id")
    public User insertUser(User user) {
        userMapper.insertUser(user);
        System.out.println("After insert, User is: " + user);
        return user;
    }

    /**
     * 这里在CachePut注解中使用了condition配置项,它是一个Spring的EL,这个表达式要求返回Boolean类型的值,如果为true
     * 则使用缓存操作,否则不使用。
     */
    @Override
    @Transactional
    @CachePut(value = "redisCache", condition = "#result != null ", key = "'redis_user_'+#id")
    public User updateUserName(Long id, String userName) {
        User user = userMapper.getUserById(id);
        if (user == null) {
            return null;
        }
        user.setUserName(userName);
        userMapper.updateUserById(user);
        return user;
    }

    /**
     * 命中率低,所以不采用缓存机制
     */
    @Override
    @Transactional
    public List<User> findUsers(String userName) {
        return userMapper.getUsersByName(userName);
    }

    /**
     * CacheEvict注解通过定义的键移除相应的缓存,beforeInvocation属性表示是在方法执行之前还是之后移除缓存,默认为false,即为方法之后
     * 移除缓存
     */
    @Override
    @Transactional
    @CacheEvict(value = "redisCache", key = "'redis_user_'+#id", beforeInvocation = false)
    public int deleteUser(Long id) {
        return userMapper.deleteUser(id);
    }
}

缓存注解@Cacheable,@CachePut,@CacheEvict的使用如上面代码中的注释部分所示。 

测试Controller类

创建相应的Controller以供测试:

@Controller
@RequestMapping("/user/cache")
public class UserCacheController {

    @Autowired
    private UserCacheService userCacheService;

    /**
     * 根据ID获取User
     */
    @RequestMapping("/getUser")
    @ResponseBody
    public CommonResult getUser(Long id) {
        User user = userCacheService.getUser(id);
        return new CommonResult(true, "获取成功", user);
    }

    /**
     * 插入一个新User
     */
    @RequestMapping("/insertUser")
    @ResponseBody
    public CommonResult insertUser(String userName, int sex, String note) {
        User user = new User(userName, sex, note);
        User resultUser = userCacheService.insertUser(user);
        return new CommonResult(true, "新增成功", resultUser);
    }

    /**
     * 根据Id查找用户并更新username
     */
    @RequestMapping("/updateUserName")
    @ResponseBody
    public CommonResult updateUserName(Long id, String userName) {
        User user = userCacheService.updateUserName(id, userName);
        boolean flag = user != null;
        String msg = flag ? "更新成功" : "更新失败";
        return new CommonResult(flag, msg, user);
    }

    /**
     * 根据Username查找UserList
     */
    @RequestMapping("/findUsers")
    @ResponseBody
    public CommonResult findUsers(String userName) {
        List<User> users = userCacheService.findUsers(userName);
        return new CommonResult(true, "查找成功", users);
    }

    /**
     * 删除用户
     */
    @RequestMapping("/deleteUser")
    @ResponseBody
    public CommonResult deleteUser(Long id) {
        int result = userCacheService.deleteUser(id);
        boolean flag = result == 1;
        String msg = flag ? "删除成功" : "删除失败";
        return new CommonResult(false, msg);

    }
}

Cache测试结果

insertUser测试

在进行测试之前,先通过redis-cli客户端命令行对数据库中的数据进行清除:

127.0.0.1:6379> flushall
OK
127.0.0.1:6379> keys *
(empty list or set)

下面对上述构建的几个不同的方法进行cache的测试,首先请求insertUser插入一条记录:

http://localhost:8080/user/cache/insertUser?userName=yitian_cache&sex=1&note=none

返回结果如下:

数据库中已经存在相应的数据,查看redis中的keys如下,可以看到将插入的相应的数据缓存到了Redis中:

127.0.0.1:6379> keys *
1) "redisCache::redis_user_26"

getUser测试 

此时如果对该id的user进行数据查询时:

http://localhost:8080/user/cache/getUser?id=26

会之前从redis缓存中获得数据,而不会发送SQL,如下:

2020-02-13 11:31:57.475 DEBUG 57559 --- [nio-8080-exec-2] s.s.w.c.SecurityContextPersistenceFilter : SecurityContextHolder now cleared, as request processing completed
2020-02-13 11:31:57.475 DEBUG 57559 --- [nio-8080-exec-2] o.a.tomcat.util.net.SocketWrapperBase    : Socket: [org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper@14322c76:org.apache.tomcat.util.net.NioChannel@4e7228f6:java.nio.channels.SocketChannel[connected local=/0:0:0:0:0:0:0:1:8080 remote=/0:0:0:0:0:0:0:1:53303]], Read from buffer: [0]
2020-02-13 11:31:57.475 DEBUG 57559 --- [nio-8080-exec-2] org.apache.tomcat.util.net.NioEndpoint   : Socket: [org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper@14322c76:org.apache.tomcat.util.net.NioChannel@4e7228f6:java.nio.channels.SocketChannel[connected local=/0:0:0:0:0:0:0:1:8080 remote=/0:0:0:0:0:0:0:1:53303]], Read direct from socket: [0]
2020-02-13 11:31:57.475 DEBUG 57559 --- [nio-8080-exec-2] o.apache.coyote.http11.Http11Processor   : Socket: [org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper@14322c76:org.apache.tomcat.util.net.NioChannel@4e7228f6:java.nio.channels.SocketChannel[connected local=/0:0:0:0:0:0:0:1:8080 remote=/0:0:0:0:0:0:0:1:53303]], Status in: [OPEN_READ], State out: [OPEN]

但如果测试从数据库中查询一个之前已经存在(但缓存中不存在的数据)时,例如id=1,可以从日志中看到sqlsession和transactional的构建和提交,此时会从数据库查询数据:

2020-02-13 11:37:25.944 DEBUG 57559 --- [nio-8080-exec-8] o.s.web.servlet.DispatcherServlet        : GET "/user/cache/getUser?id=1", parameters={masked}
2020-02-13 11:37:25.944 DEBUG 57559 --- [nio-8080-exec-8] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to cn.zyt.springbootlearning.controller.UserCacheController#getUser(Long)
2020-02-13 11:37:25.950 DEBUG 57559 --- [nio-8080-exec-8] o.s.j.d.DataSourceTransactionManager     : Creating new transaction with name [cn.zyt.springbootlearning.service.impl.UserCacheServiceImpl.getUser]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2020-02-13 11:37:25.972 DEBUG 57559 --- [nio-8080-exec-8] o.s.j.d.DataSourceTransactionManager     : Acquired Connection [HikariProxyConnection@519212670 wrapping com.mysql.jdbc.JDBC4Connection@1dd6a3e5] for JDBC transaction
2020-02-13 11:37:25.974 DEBUG 57559 --- [nio-8080-exec-8] o.s.j.d.DataSourceTransactionManager     : Switching JDBC Connection [HikariProxyConnection@519212670 wrapping com.mysql.jdbc.JDBC4Connection@1dd6a3e5] to manual commit
2020-02-13 11:37:26.001 DEBUG 57559 --- [nio-8080-exec-8] org.mybatis.spring.SqlSessionUtils       : Creating a new SqlSession
2020-02-13 11:37:26.003 DEBUG 57559 --- [nio-8080-exec-8] org.mybatis.spring.SqlSessionUtils       : Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5d084971]
2020-02-13 11:37:26.007 DEBUG 57559 --- [nio-8080-exec-8] o.m.s.t.SpringManagedTransaction         : JDBC Connection [HikariProxyConnection@519212670 wrapping com.mysql.jdbc.JDBC4Connection@1dd6a3e5] will be managed by Spring
2020-02-13 11:37:26.008 DEBUG 57559 --- [nio-8080-exec-8] c.z.s.dao.UserMapper.getUserById         : ==>  Preparing: select id, user_name as userName, sex, note from t_user where id = ? 
2020-02-13 11:37:26.021 DEBUG 57559 --- [nio-8080-exec-8] c.z.s.dao.UserMapper.getUserById         : ==> Parameters: 1(Long)
2020-02-13 11:37:26.053 DEBUG 57559 --- [nio-8080-exec-8] c.z.s.dao.UserMapper.getUserById         : <==      Total: 1
2020-02-13 11:37:26.053 DEBUG 57559 --- [nio-8080-exec-8] org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5d084971]
2020-02-13 11:37:26.053 DEBUG 57559 --- [nio-8080-exec-8] org.mybatis.spring.SqlSessionUtils       : Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5d084971]
2020-02-13 11:37:26.181 DEBUG 57559 --- [nio-8080-exec-8] org.mybatis.spring.SqlSessionUtils       : Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5d084971]
2020-02-13 11:37:26.181 DEBUG 57559 --- [nio-8080-exec-8] org.mybatis.spring.SqlSessionUtils       : Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5d084971]
2020-02-13 11:37:26.181 DEBUG 57559 --- [nio-8080-exec-8] o.s.j.d.DataSourceTransactionManager     : Initiating transaction commit

注,如上的日志输出为DEBUG级别,需要在application.properties配置文件中进行如下设置:

# 日志配置
logging.level.root=DEBUG
logging.level.org.springframework=DEBUG
logging.level.org.org.mybatis=DEBUG

以查看详细的日志输出。 

 当数据查询成功时,同样会将结果存放到redis缓存中:

127.0.0.1:6379> keys *
1) "redisCache::redis_user_1"
2) "redisCache::redis_user_26"

updateUserName测试 

同样的,如果使用updateUserName方法对user进行更新时,也会将redis中不存在的缓存数据加入到缓存中:

http://localhost:8080/user/cache/updateUserName?id=23&userName=yitian_new

成功更新完成后,redis中数据如下:

127.0.0.1:6379> keys *
1) "redisCache::redis_user_1"
2) "redisCache::redis_user_26"
3) "redisCache::redis_user_23"

但需要注意的是,在更新数据时缓存中的数据有可能是脏数据,所以在updateUserName方法中首先对数据库的数据进行了获取,然后在对该数据进行更新,从而避免之前从缓存中读取可以过时的数据。这一点需要注意。 

findUser测试

由于在使用findUser查询用户列表时,缓存的命中率会很低(因为查询的参数可能存在很大差异),所以这里没有设置缓存的使用。请求如下:

http://localhost:8080/user/cache/findUsers?userName=yitian

返回结果如下:

deleteUser测试

在对指定id的用户进行删除时,通过上述缓存注解的使用,会在方法调用完成后将缓存中的数据删除,这里使用上面添加的id=26的数据进行测试:

http://localhost:8080/user/cache/deleteUser?id=26

删除成功后redis中的结果如下:

127.0.0.1:6379> keys *
1) "redisCache::redis_user_1"
2) "redisCache::redis_user_23"

注意,在缓存注解中设置的value名会用于缓存的匹配,所以该名称需要在插入和删除时保持一致,否则在删除数据时不会匹配到正确的缓存数据,导致缓存删不掉。 

自定义缓存管理器

更新Redis缓存管理器配置

使用以上的缓存管理器的配置时,默认缓存的名称为{cacheName}::#{key}的形式,并且缓存永不失效。在application.properties文件中可以进行相应的配置:

# 自定义缓存前缀
spring.cache.redis.key-prefix=
# 是否使用前缀
spring.cache.redis.use-key-prefix=false
# 设置缓存失效时间,0或者默认为永远不失效
spring.cache.redis.time-to-live=600000

上面的设置即是将缓存前缀去掉, 只使用key作为缓存名,同时将缓存失效时间设置为600s,即10分钟。这样10分钟过后,redis的键就会超时,缓存会在数据操作时进行更新。

自定义缓存管理器

在对Spring Boot中缓存管理器进行设置时,除了如上使用配置文件的方式,还可以通过自定义缓存管理器来创建需要的缓存管理器并进行设置,当需要的自定义设置比较多时,推荐使用这种方式。

在上述的SpringRedisConfiguration.java配置类中进行如下定义:

    /**
     * 自定义RedisCacheManager
     */
//    @Bean(name = "redisCacheManager")
    public RedisCacheManager initRedisCacheManager() {
        // 获取Redis加锁的写入器
        RedisCacheWriter writer = RedisCacheWriter.lockingRedisCacheWriter(redisConnectionFactory);
        // 启动Redis缓存的默认设置
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
        // 设置JDK序列化器
        config = config.serializeValuesWith(
                RedisSerializationContext.SerializationPair.fromSerializer(new JdkSerializationRedisSerializer()));
        
        // 自定义设置:禁用前缀
        config = config.disableKeyPrefix();
        // 设置失效时间
        config = config.entryTtl(Duration.ofMinutes(10));
        // 创建Redis缓存管理器
        RedisCacheManager redisCacheManager = new RedisCacheManager(writer, config);
        return redisCacheManager;
    }
发布了296 篇原创文章 · 获赞 35 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/yitian_z/article/details/104291898