Spring Boot缓存技术-----F04

Spring Boot****缓存技术*

1.Ehcache Redis 的对比

Ehcache

在java项目广泛的使用。它是一个开源的、设计于提高在数据从RDBMS中取出来的高花费、高延迟采取的一种缓存方案。正因为Ehcache具有健壮性(基于java开发)、被认证(具有apache 2.0 license)、充满特色,所以被用于大型复杂分布式web application的各个节点中。够简单就是Ehcache的一大特色,自然用起来 just so easy!

够快

Ehcache 的发行有一段时长了,经过几年的努力和不计其数的性能测试,Ehcache 终被设计于 large, high concurrency systems。

够简单

开发者提供的接口非常简单明了,从 Ehcache 的搭建到运用运行仅仅需要的是你宝贵的几分钟。其实很多编程者都不知道自己在用Ehcache,Ehcache被广泛的运用于其他的开源项目,比如:Hibernate。

够袖珍

关于这点的特性,官方给了一个很可爱的名字 small foot print,一般 Ehcache 的发布版本不会到2M, V2.2.3版本才668KB,目前最新版的V3.8.0版本也不过才1754KB。

够轻量

核心程序仅仅依赖 slf4j 这一个包,没有之一!

好扩展

Ehcache 提供了对大数据的内存和硬盘的存储,最近版本允许多实例、保存对象高灵活性、提供LRU、LFU、FIFO淘汰算法,基础属性支持热配置、支持的插件多。

监听器

缓存管理器监听器 (CacheManagerListener)和 缓存监听器(CacheEvenListener),做一些统计或数据一致性广播挺好用的。

Redis

支持持久化

Redis 的本地持久化支持两种方式:RDB和AOF。RDB 在redis.conf配置文件里配置持久化触发器,AOF指的是Redis每增加一条记录都会保存到持久化文件中(保存的是这条记录的生成命令)。

丰富的数据类型

Redis 支持 String 、List、Set、Sorted Set、hash 多种数据类型。

高性能

内存操作的级别是毫秒级的比硬盘操作秒级操作自然高效不少,减少了磁头寻道、数据读取、页面交换这些高开销的操作!这也是NOSQL冒出来的原因吧,应该是高性能是基于RDBMS的衍生产品,虽然RDBMS也具有缓存结构,但是始终在应用层面达不到我们的需求。

Replication

Redis 提供主从复制方案,跟 MySql 一样增量复制而且复制的实现都很相似,这个复制跟AOF有点类似复制的是新增记录命令,主库新增记录将新增脚本发送给从库,从库根据脚本生成记录,这个过程非常快,就看网络了,一般主从都是在同一个局域网,所以可以说 Redis 的主从近似及时同步,同时它还支持一主多从,动态添加从库,从库数量没有限制。

更新快

Redis 到目前为止已经发了大版本 5 个,小版本没算过。Redis 作者是个非常积极的人,无论是邮件提问还是论坛发帖,他都能及时耐心的为你解答,维护度很高。有人维护的话,让我们用的也省心和放心。目前作者对Redis 的主导开发方向是 Redis 的集群方向。

总结

Ehcache 直接在 jvm 虚拟机中缓存,速度快,效率高;但是缓存共享麻烦,集群分布式应用不方便。如果是单个应用或者对缓存访问要求很高的应用,用 Ehcache。

Redis 是通过 socket 访问到缓存服务,效率比 Ecache 低,比数据库要快很多,处理集群和分布式缓存方便,有成熟的方案。如果是大型系统,存在缓存共享、分布式部署、缓存内容很大的,建议用 Redis。

2.Ehcache

使用 Ehcache 缓存的步骤是:

  • 添加 Ehcache 组件和依赖
  • 添加 Ehcache 配置文件
  • 业务层使用 @Cacheable 处理缓存
  • 启动类使用 @EnableCaching 开启缓存

创建项目

image.png

添加组件依赖

我们需要在 pom.xml 中添加 cache 组件 和 ehcache 依赖

(ehcache 依赖可以不用添加,本案例添加是为了获取 ehcache 配置文件模板

pom.xml

 <dependencies>
        <!-- cache 组件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
        <!-- ehcache 依赖 -->
        <dependency>
            <groupId>net.sf.ehcache</groupId>
            <artifactId>ehcache</artifactId>
        </dependency>
        <!-- thymeleaf 组件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!-- web 组件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- mybatis 组件 -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.0</version>
        </dependency>
        <!-- mysql 数据库驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- druid 数据库连接池 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.19</version>
        </dependency>

        <!-- test 组件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <!-- build标签 常用于添加插件及编译配置 -->
    <build>
        <!-- 读取配置文件 -->
        <resources>
            <resource>
                <directory>src/main/resources</directory>
            </resource>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                    <include>**/*.properties</include>
                    <include>**/*.tld</include>
                </includes>
                <filtering>false</filtering>
            </resource>
        </resources>
    </build>

ehcache 配置文件

我们可以参考 ehcache 项目源码中的配置文件模板。

image.png

diskStore

diskStore 元素是可选的,非必须的。如果不使用磁盘存储,只需要将 diskStore 注释掉即可;如果使用,需要在 ehcache.xml 文件中的 ehcahce 元素下的定义一个 diskStore 元素并指定其 path 属性。

path 属性可以配置的目录有:

user.home(用户的家目录)

user.dir(用户当前的工作目录)

java.io.tmpdir(默认的临时目录)

ehcache.disk.store.dir(ehcache的配置目录)

绝对路径(如:D:\ehcache)

DiskStore 中驱除元素跟 MemoryStore 中驱除元素的规则是不一样的。当往 DiskStore 中添加元素且此时DiskStore 中的容量已经超出限制时将采用 LFU(最不常用)驱除规则将对应的元素进行删除,而且该驱除规则是不可配置的(通过 cache 中的 diskExpiryThreadIntervalSeconds 属性完成)。

缓存策略

name :缓存名称

maxElementsInMemory :内存缓存中最多可以存放的元素数量,若放入 Cache 中的元素超过这个数值,则有以下两种情况:

  1. 若 overflowToDisk=true,则会将 Cache 中多出的元素放入磁盘文件中
  1. 若 overflowToDisk=false,则根据 memoryStoreEvictionPolicy 策略替换 Cache 中原有的元素

eternal :缓存中对象是否永久有效,是否永驻内存,true时将忽略timeToIdleSeconds和 timeToLiveSeconds

timeToIdleSeconds :设置对象的空闲时间

timeToLiveSeconds :设置对象的存活时间

overflowToDisk :内容溢出是否写入磁盘

memoryStoreEvictionPolicy :内存存储与释放策略,即达到 maxElementsInMemory 限制时,Ehcache 会根据指定策略清理内存。共有三种策略:

LRU(最近最少使用)

LFU(最常用的)

FIFO(先进先出)

ehcache 中缓存的 3 种清空策略:

  1. FIFO: first in first out,这个是大家最熟的,先进先出,不多讲了
  2. LFU: Less Frequently Used,直白一点就是讲一直以来最少被使用的。缓存的元素有一个 hit 属性, hit 值最小的将会被清出缓存。
  3. LRU: Least Recently Used,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。

对于 EhCache 的配置文件也可以通过 application.properties 文件中使用 spring.cache.ehcache.config 属性来指定,比如: spring.cache.ehcache.config=classpath:config/ehcache.xml

resources/ehcache.xml

<?xml version="1.0" encoding="utf-8" ?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">

    <diskStore path="java.io.tmpdir"/>

    <!-- 默认缓存策略 -->
    <defaultCache
            maxElementsInMemory="10000"
            eternal="false"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            maxElementsOnDisk="10000000"
            diskExpiryThreadIntervalSeconds="120"
            memoryStoreEvictionPolicy="LRU">
        <persistence strategy="localTempSwap"/>
    </defaultCache>

    <!-- 自定义缓存策略 -->
    <cache name="userCache"
           maxElementsInMemory="10000"
           eternal="false"
           timeToIdleSeconds="120"
           timeToLiveSeconds="120"
           maxElementsOnDisk="10000000"
           diskExpiryThreadIntervalSeconds="120"
           memoryStoreEvictionPolicy="LRU">
        <persistence strategy="localTempSwap"/>
    </cache>

</ehcache>

properties 配置文件

# 配置数据库驱动
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=shsxt
# 配置数据库连接池
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
# 配置MyBatis数据返回类型别名(默认别名是类名)
mybatis.type-aliases-package=com.springboot.pojo
# 配置MyBatis Mapper映射文件
mybatis.mapper-locations=classpath:com/springboot/mapper/*.xml
# 配置SQL打印
# 指定某个包下的SQL打印
logging.level.com.springboot.mapper=debug
# 所有包下的SQL打印
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

添加 application.properties 全局配置文件,配置SQL打印。

SQL****文件

CREATE TABLE `user` (
`id` INT (11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR (255) DEFAULT NULL,
`age` INT (11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE = INNODB DEFAULT CHARSET = utf8mb4;

实体类

User.java

package com.springboot.pojo;

import java.io.Serializable;

public class User implements Serializable {

    private Integer id;
    private String name;
    private Integer age;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

Mapper 接口

UserMapper.java

public interface UserMapper {

    // 添加用户
    int insertUser(User user);

    // 查询用户
    List<User> selectUserList();

    // 根据主键查询用户
    User selectUserById(Integer id);

    // 修改用户
    int updateUser(User user);

    // 删除用户
    int deleteUser(Integer id);

}

映射配置文件

UserMapper.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">
<!-- namespace 必须是接口的完全限定名 -->
<mapper namespace="com.springboot.mapper.UserMapper">

    <!-- id 必须和接口中的方法名一致 -->
    <insert id="insertUser" parameterType="user">
        insert into user (name, age) values (#{name}, #{age})
    </insert>

    <!-- 查询所有用户 -->
    <select id="selectUserList" resultType="user">
        select id, name, age from user;
    </select>

    <!-- 根据主键查询用户 -->
    <select id="selectUserById" resultType="user">
        select id, name, age from user where id = #{id};
    </select>

    <!-- 修改用户 -->
    <update id="updateUser" parameterType="user">
        update user set name = #{name}, age = #{age} where id = #{id};
    </update>

    <!-- 删除用户 -->
    <delete id="deleteUser">
        delete from user where id = #{id}
    </delete>

</mapper>

业务层

业务层使用 @Cacheable 处理缓存

UserServiceI.java

public interface UserServiceI {

    int insertUser(User user);

    // 查询用户
    List<User> selectUserList();

    // 根据主键查询用户
    User selectUserById(Integer id);

    // 修改用户
    int updateUser(User user);

    // 删除用户
    int deleteUser(Integer id);

}

UserServiceImpl.java

@Service
@Transactional
@CacheConfig(cacheNames = "userCache")
public class UserServiceImpl implements UserServiceI {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private CacheManager cacheManager;

    // 清除缓存中以 userCache 缓存策略缓存的对象
    @CacheEvict(allEntries = true)
    @Override
    public int insertUser(User user) {
        return userMapper.insertUser(user);
    }

    @Cacheable
    @Override
    public List<User> selectUserList() {
        return userMapper.selectUserList();
    }

    @Cacheable(key = "#id")
    @Override
    public User selectUserById(Integer id) {
        return userMapper.selectUserById(id);
    }

    // 清除缓存中以 userCache 缓存策略缓存的对象
    @CacheEvict(allEntries = true)
    @Override
    public int updateUser(User user) {
        return userMapper.updateUser(user);
    }

    // 清除缓存中以 userCache 缓存策略缓存的对象
    //@CacheEvict(allEntries = true)
    @Override
    public int deleteUser(Integer id) {
        Cache cache = cacheManager.getCache("userCache");
        cache.clear();
        return userMapper.deleteUser(id);
    }

}


控制层

UserController.java

@Controller
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserServiceI userService;

    /**
     * 页面跳转
     */
    @RequestMapping("/{page}")
    public String page(@PathVariable String page) {
        return page;
    }

    /**
     * 添加用户
     */
    @PostMapping("/insertUser")
    public String insertUser(User user) {
        int result = userService.insertUser(user);
        return "success";
    }

    /**
     * 查询用户列表
     */
    @GetMapping("/selectUserList")
    public String selectUserList(Model model) {
        model.addAttribute("userList", userService.selectUserList());
        return "user-list";
    }

    /**
     * 修改用户跳转页面
     */
    @GetMapping("/edit/{id}")
    public String edit(@PathVariable Integer id, Model model) {
        model.addAttribute("user", userService.selectUserById(id));
        return "updateUser";
    }

    /**
     * 修改用户保存
     */
    @PostMapping("/updateUser")
    public String updateUser(User user) {
        userService.updateUser(user);
        return "success";
    }

    /**
     * 删除用户
     */
    @GetMapping("/deleteUser/{id}")
    public String deleteUser(@PathVariable Integer id) {
        userService.deleteUser(id);
        return "success";
    }

}


视图层

templates/register.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>注册用户</title>
</head>
<body>
    <form th:action="@{/user/insertUser}" method="post">
        姓名:<input name="name"/><br/>
        年龄:<input name="age"/><br/>
        <input type="submit" value="注册"/>
    </form>
</body>
</html>

templates/success.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>提示</title>
</head>
<body>
    成功
</body>
</html>

templates/updateUser.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>修改用户</title>
</head>
<body>
    <form th:action="@{/user/updateUser}" method="post">
        <input type="hidden" name="id" th:value="${user.id}"/>
        姓名:<input name="name" th:value="${user.name}"/><br/>
        年龄:<input name="age" th:value="${user.age}"/><br/>
        <input type="submit" value="修改"/>
    </form>
</body>
</html>

templates/user-list.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>用户列表</title>
</head>
<body>
    <table border="1" width="300px" cellspacing="0">
        <tr>
            <th>ID</th>
            <th>NAME</th>
            <th>AGE</th>
            <th>操作</th>
        </tr>
        <tr th:each="user : ${userList}">
            <td th:text="${user.id}"></td>
            <td th:text="${user.name}"></td>
            <td th:text="${user.age}"></td>
            <td>
                <a th:href="@{/user/edit/{id}(id=${user.id})}">修改</a>
                <a th:href="@{/user/deleteUser/{id}(id=${user.id})}">删除</a>
            </td>
        </tr>
    </table>
</body>
</html>

启动类

启动类使用 @EnableCaching 开启缓存

App.java

@SpringBootApplication
// 扫描 mapper 接口和映射配置文件
@MapperScan("com.springboot.mapper")
// 开启缓存
@EnableCaching
public class App {

    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }

}


测试

数据库

image.png

访问:http://localhost:8080/user/selectUserList

image.png

删除数据库


image.png

再次查询,控制台无任何打印消息,依然可以查询到数据,缓存配置成功。

Cache 常用注解详解

@Cacheable

value 、 cacheNames :两个等同的参数( cacheNames 为Spring 4新增,作为 value 的别名),用于 指定缓存存储的集合名。由于Spring 4中新增了 @CacheConfig ,因此在Spring 3中原本必须有的 value 属性,也成为非必需项了。

key :缓存对象存储在Map集合中的key值,非必需,缺省按照函数的所有参数组合作为 key 值,若自己配置需使用 SpEL 表达式,比如: @Cacheable(key = "#p0") :使用函数第一个参数作为缓存的 key 值,在查询时如果key 存在,那么直接从缓存中将数据返回。更多关于SpEL表达式的详细内容可参考官方文档

UserServiceImpl.java

// 对当前查询的结果做缓存处理,不配置 cacheNames 属性时使用默认缓存策略
@Cacheable(cacheNames = "userCache")
@Override
public List<User> selectUserList() {
return userMapper.selectUserList();
} /
*
设置缓存的 key
#p0:使用第一个参数作为 key
#id:使用参数 id 作为 key
#user.id:使用参数 user 的 id 作为 key (user 是对象 id 是 user 的属性)
*/
@Cacheable(cacheNames = "userCache", key = "#id")
@Override
public User selectUserById(Integer id) {
return userMapper.selectUserById(id);
}

@CacheConfig

@CacheConfig is a class-level annotation that allows to share the cache names.

主要用于配置某些类中会用到的一些共用的缓存配置,比如本案例中我们多次使用到

@Cacheable(cacheNames= "userCache") 我们便可以在该类上添加 @CacheConfig(cacheNames = "userCache") 注解,方法上只需要使用 @Cacheable 即可。如果在方法上使用别的缓存名称,那么依然以方法的缓存名称为准。

@CacheEvict

配置于函数上,通常用在写操作方法上,用来从缓存中移除相应数据。除了同 @Cacheable 一样的参数之外,它还有下面两个参数:

allEntries :非必需,默认为 false。当为 true 时,会移除所有数据;

beforeInvocation :非必需,默认为 false,会在调用方法之后移除数据。当为 true 时,会在调用方法之前移除数据。

// 清除缓存中以 userCache 缓存策略缓存的对象
@CacheEvict(cacheNames = "userCache", allEntries = true)
@Override
public int insertUser(User user) {
return userMapper.insertUser(user);
} 
// 如果该类配置了@CacheConfig(cacheNames = "userCache"),可以简写
@CacheEvict(allEntries = true)
@Override
public int updateUser(User user) {
return userMapper.updateUser(user);
} 
// 如果该类配置了@CacheConfig(cacheNames = "userCache"),可以简写
@CacheEvict(allEntries = true)
@Override
public int deleteUserById(Integer id) {
return userMapper.deleteUserById(id);
}

当我们执行写操作时,缓存的内容会被清除,查询时会重新查询关系数据库再次放入缓存。

CacheManager

我们还可以通过 CacheManager 对象来管理缓存。

UserServiceImpl.java

@Autowired
private CacheManager cacheManager;
@Override
public int deleteUserById(Integer id) {
// 清除缓存中以 userCache 缓存策略缓存的对象
cacheManager.getCache("userCache").clear();
return userMapper.deleteUserById(id);
}j

3.Redis

Redis 是一个开源的使用 ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。no-sql 型的数据库。

2008年,意大利一家创业公司Merzia的创始人Salvatore Sanfilippo为了避免MySQL的低性能,亲自定做一个数据库,并于2009年开发完成,这个就是Redis。

从2010年3月15日起,Redis的开发工作由VMware主持。

从2013年5月开始,Redis的开发由Pivotal赞助。

安装****Redis

下载地址:http://redis.io/

将 redis.tar.gz 上传至服务器,解压 tar zxvf redis.tar.gz ;

安装依赖: yum -y install gcc-c++ autoconf automake ;

创建安装目录 mkdir -p /usr/local/redis ;

切换至解压目录 cd redis ;预编译 make ;

安装 make PREFIX=/usr/local/redis install ;

安装成功如下图:


image.png

redis-cli:客户端

redis-server:服务端

修改配置文件并启动

复制解压目录下 redis.conf 至安装目录 /usr/local/redis/bin

修改 redis.conf,将 daemonize 修改为 yes(后台启动);

注释掉 bind127.0.0.1 使所有的 ip 访问 redis,若是想指定多个 ip 访问,并不是全部的 ip 访问,可以 bind设置;

添加访问认证 requirepass root ;

处理防火墙;

启动时,指定配置文件路径即可 bin/redis-server bin/redis.conf ;

安装可视化客户端访问:

image.png

Spring Data Redis

Spring Data Redis 是 Spring 大家族的一部分,提供了在 Srping 应用中通过简单的配置访问 Redis 服务,对Reids 底层开发包(Jedis,JRedis,RJC)进行了高度封装,RedisTemplate 提供了 Redis 各种操作、异常处理及序列化,支持发布订阅,对 Redis Sentinel 和 Redis Cluster 支持,并对 Spring 3.1 cache进行了实现。

案例中我们分别讲解 Lettuce 和 Jedis 两种实现, Lettuce 和 Jedis 的都是连接 Redis Server 的客户端程序。

因为 Spring Boot2.0 之后,底层默认不再采用 Jedis 作为实现了。而是采用效率更高,线程更安全的 Lettuce客户端。

Jedis 是一个优秀的基于 Java 语言的 Redis 客户端,但是,其不足也很明显:Jedis 在实现上是直接连接 Redis-Server,在多个线程间共享一个 Jedis 实例时是线程不安全的,如果想要在多线程场景下使用 Jedis,需要使用连接池,每个线程都使用自己的 Jedis 实例,当连接数量增多时,会消耗较多的物理资源。

Lettuce 则完全克服了其线程不安全的缺点:Lettuce 是基于 Netty 的连接实例(StatefulRedisConnection)

Lettuce 是一个可伸缩的线程安全的 Redis 客户端,支持同步、异步和响应式模式。多个线程可以共享一个连接实例,而不必担心多线程并发问题。它基于优秀 Netty NIO 框架构建,支持 Redis 的高级功能,如Sentinel,集群,流水线,自动重新连接和 Redis 数据模型。

3.1Lettuce

我们先来讲 Spring Boot2.x 版本以后的默认方式 Lettuce。

创建项目

创建Spring Boot项目,选择 Web 组件 Redis 组件。需要手动添加 commons-pool2 对象池依赖。 !
image.png

pom.xml

<!-- spring data redis 组件 -->
<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- commons-pool2 对象池依赖 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <!-- web 组件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
            <!-- test 组件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>



properties 配置文件

application.properties

# 最大连接数,默认8
spring.redis.lettuce.pool.max-active=1024
# 最大连接阻塞等待时间,单位毫秒,默认-1
spring.redis.lettuce.pool.max-wait=10000
# 最大空闲连接,默认8
spring.redis.lettuce.pool.max-idle=200
# 最小空闲连接,默认0
spring.redis.lettuce.pool.min-idle=5
# 连接超时时间
spring.redis.timeout=10000
# Redis服务器地址
spring.redis.host=192.168.190.10
# Redis服务器端口
spring.redis.port=6379
# Redis服务器密码
spring.redis.password=root
# 选择哪个库,默认0库
spring.redis.database=0


自定义模板

默认情况下的模板 RedisTemplate<Object, Object> ,默认序列化使用的是 JdkSerializationRedisSerializer ,存储二进制字节码。这时需要自定义模板,当自定义模板后又想存储String 字符串时,可以使用 StringRedisTemplate 的方式,他们俩并不冲突。

序列化问题:

要把 domain object 做为 key-value 对保存在 redis 中,就必须要解决对象的序列化问题。Spring Data Redis给我们提供了一些现成的方案:

JdkSerializationRedisSerializer 使用JDK提供的序列化功能。 优点是反序列化时不需要提供类型信息(class),但缺点是序列化后的结果非常庞大,是JSON格式的5倍左右,这样就会消耗 Redis 服务器的大量内存。

Jackson2JsonRedisSerializer 使用 Jackson 库将对象序列化为JSON字符串。优点是速度快,序列化后的字符串短小精悍。但缺点也非常致命,那就是此类的构造函数中有一个类型参数,必须提供要序列化对象的类型信息(.class对象)。 通过查看源代码,发现其只在反序列化过程中用到了类型信息。

GenericJackson2JsonRedisSerializer 通用型序列化,这种序列化方式不用自己手动指定对象的Class。

RedisConfigForLettuce.java

@Configuration
public class RedisConfigForLettuce {
// 重写 RedisTemplate 序列化
@Bean
public RedisTemplate<String, Object> redisTemplate(
LettuceConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 为 String 类型 key 设置序列化器
template.setKeySerializer(new StringRedisSerializer());
// 为 String 类型 value 设置序列化器
template.setValueSerializer(new
GenericJackson2JsonRedisSerializer());
// 为 Hash 类型 key 设置序列化器
template.setHashKeySerializer(new StringRedisSerializer());
// 为 Hash 类型 value 设置序列化器
template.setHashValueSerializer(new
GenericJackson2JsonRedisSerializer());
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}

debug 模式运行 RedisConnectionFactory 信息如下:

image.png

实体类

user.java

package com.springboot.pojo;

import java.io.Serializable;

public class User implements Serializable {

    private Integer id;
    private String username;
    private Integer age;

    public User() {
    }

    public User(Integer id, String username, Integer age) {
        this.id = id;
        this.username = username;
        this.age = age;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", age=" + age +
                '}';
    }

}

测试类

SpringbootRedisApplicationTests.java

@RunWith(SpringRunner.class)
@SpringBootTest(classes = {App.class})
public class SpringbootRedisApplicationTests {
    @Autowired
    private RedisTemplate<String,Object> redisTemplate;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Test
    public void testSet() {
        User user = new User();
        user.setId(1);
        user.setUsername("张三");
        user.setAge(18);
        redisTemplate.opsForValue().set("user:" + user.getId(), user);

        User u = (User) redisTemplate.opsForValue().get("user:1");
        System.out.println(u);
    }

    @Test
    public void testGet() {
        User user = (User) redisTemplate.opsForValue().get("user:1");
        System.out.println(user);
    }

}

结果

image.png
3.2Jedis

创建项目

创建Spring Boot项目,选择 Web 组件 Redis 组件。需要手动添加 commons-pool2 对象池依赖,排除 Lettuce依赖,添加 Jedis 依赖。

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.7.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.springboot</groupId>
    <artifactId>springboot-redis</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot-redis</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <!--
                1.x 的版本默认采用的连接池技术是 Jedis,
                2.0 以上版本默认连接池是 Lettuce,
                如果采用 Jedis,需要排除 Lettuce 的依赖。
             -->
         <exclusions>
             <exclusion>
                      <groupId>io.lettuce</groupId>
                         <artifactId>lettuce-core</artifactId>
               </exclusion>
          </exclusions>
        </dependency>
        <!-- jedis 依赖 -->
      <dependency>
     <groupId>redis.clients</groupId>
      <artifactId>jedis</artifactId>
     </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>


</project>


properties 配置文件

application.properties

# 最大连接数,默认8
spring.redis.lettuce.pool.max-active=1024
# 最大连接阻塞等待时间,单位毫秒,默认-1
spring.redis.lettuce.pool.max-wait=10000
# 最大空闲连接,默认8
spring.redis.lettuce.pool.max-idle=200
# 最小空闲连接,默认0
spring.redis.lettuce.pool.min-idle=5
# 连接超时时间
spring.redis.timeout=10000
# Redis服务器地址
spring.redis.host=192.168.190.10
# Redis服务器端口
spring.redis.port=6379
# Redis服务器密码
spring.redis.password=root
# 选择哪个库,默认0库
spring.redis.database=0


自定义模板

RedisConfigForJedis.java

//@Configuration
public class RedisConfigForJedis {

    // 重写 RedisTemplate 序列化
    //@Bean

    public RedisTemplate<String, Object> redisTemplate(
            JedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        // 为 String 类型 key 设置序列化器
        template.setKeySerializer(new StringRedisSerializer());
        // 为 String 类型 value 设置序列化器
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        // 为 Hash 类型 key 设置序列化器
        template.setHashKeySerializer(new StringRedisSerializer());
        // 为 Hash 类型 value 设置序列化器
        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }


}


debug 模式运行 RedisConnectionFactory 信息如下:

image.png

实体类

User.java

测试类

SpringbootRedisApplicationTests.java

@RunWith(SpringRunner.class)
@SpringBootTest(classes = {App.class})
public class SpringbootRedisApplicationTests {
    @Autowired
    private RedisTemplate<String,Object> redisTemplate;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Test
    public void testSet() {
        User user = new User();
        user.setId(1);
        user.setUsername("张三");
        user.setAge(18);
        redisTemplate.opsForValue().set("user:" + user.getId(), user);

        User u = (User) redisTemplate.opsForValue().get("user:1");
        System.out.println(u);
    }

    @Test
    public void testGet() {
        User user = (User) redisTemplate.opsForValue().get("user:1");
        System.out.println(user);
    }

}


结果

image.png
3.3Sentinel

Redis 哨兵是为 Redis 提供一个高可靠解决方案,Redis 主节点挂掉会自动帮我们提升从为主,对一定程序上的错误可以不需要人工干预自行解决。哨兵功能还有监视、事件通知、配置功能等。以下是哨兵的功能列表:

监控:不间断的检查主从服务是否如预期一样正常工作;

事件通知:对被监视的 Redis 实例的异常,能通知系统管理员,或者以API接口通知其他应用程序;

智能援救:当被监视的主服务异常时,哨兵会智能的把某个从服务提升为主服务,同时其他从服务与新的主服务之间的关系将得到重新的配置。应用程序将通过redis服务端重新得到新的主服务的地址并重新建立连接;

配置服务:客户端可连接哨兵的接口,获得主从服务的相关信息,如果发生改变,哨兵新通知客户端。

Spring Boot 也提供了对于哨兵连接的配置,关于哨兵主从服务,我们先来看看本案例使用的环境。


image.png

properties 配置文件

application.properties

# 最大连接数,默认8
spring.redis.lettuce.pool.max-active=1024
# 最大连接阻塞等待时间,单位毫秒,默认-1
spring.redis.lettuce.pool.max-wait=10000
# 最大空闲连接,默认8
spring.redis.lettuce.pool.max-idle=200
# 最小空闲连接,默认0
spring.redis.lettuce.pool.min-idle=5
# 连接超时时间
spring.redis.timeout=10000
# Redis服务器地址
spring.redis.host=192.168.18.10
# Redis服务器端口,哨兵模式下不一定非要配置为主节点,只要是主从环境中任何一个节点即可
spring.redis.port=6379
# Redis服务器密码
spring.redis.password=root
# 选择哪个库,默认0库
spring.redis.database=0
# 哨兵主从服务
# 主节点名称
spring.redis.sentinel.master=mymaster
# 主从服务器地址
spring.redis.sentinel.nodes=192.168.18.10:26379,192.168.18.10:26380,192.168.18.10:26381

@Bean

除了使用配置文件或者使用 @Bean 配置 Sentinel

Lettuce 配置 Sentinel

@Configuration
public class RedisConfigForLettuce {

    /**
     * Lettuce
     */
    //@Bean
    public RedisConnectionFactory lettuceConnectionFactory() {
        RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration()
                .master("mymaster")// 主节点名称
                .sentinel("192.168.10.10", 26379)
                .sentinel("192.168.10.10", 26380)
                .sentinel("192.168.10.10", 26381);
        sentinelConfig.setPassword("root");// 设置密码
        return new LettuceConnectionFactory(sentinelConfig);
    }

    // 重写 RedisTemplate 序列化
    @Bean
    public RedisTemplate<String, Object> redisTemplate(
            LettuceConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        // 为 String 类型 key 设置序列化器
        template.setKeySerializer(new StringRedisSerializer());
        // 为 String 类型 value 设置序列化器
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        // 为 Hash 类型 key 设置序列化器
        template.setHashKeySerializer(new StringRedisSerializer());
        // 为 Hash 类型 value 设置序列化器
        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

}


Jedis 配置 Sentinel

package com.springboot.config;

//@Configuration
public class RedisConfigForJedis {

    /**
     * Jedis
     */
    //@Bean
    /*
    public RedisConnectionFactory jedisConnectionFactory() {
        RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration()
                .master("mymaster")// 主节点名称
                .sentinel("192.168.10.10", 26379)
                .sentinel("192.168.10.10", 26380)
                .sentinel("192.168.10.10", 26381);
        sentinelConfig.setPassword("root");
        return new JedisConnectionFactory(sentinelConfig);
    }
     */

    // 重写 RedisTemplate 序列化
    //@Bean
    /*
    public RedisTemplate<String, Object> redisTemplate(
            JedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        // 为 String 类型 key 设置序列化器
        template.setKeySerializer(new StringRedisSerializer());
        // 为 String 类型 value 设置序列化器
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        // 为 Hash 类型 key 设置序列化器
        template.setHashKeySerializer(new StringRedisSerializer());
        // 为 Hash 类型 value 设置序列化器
        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }
    */

}


测试类

测试同上

不需要配置主节点,哨兵可以可以自动查找

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容