文档章节

Spring Boot + Mybatis + Redis as L2cache

梨加橙
 梨加橙
发布于 2017/08/08 09:46
字数 4446
阅读 182
收藏 6

1.背景

Spring-Boot因其提供了各种开箱即用的插件,使得它成为了当今最为主流的Java Web开发框架之一。Mybatis是一个十分轻量好用的ORM框架。Redis是当今十分主流的分布式key-value型数据库,在web开发中,我们常用它来缓存数据库的查询结果。

本篇博客将介绍如何使用Spring-Boot快速搭建一个Web应用,并且采用Mybatis作为我们的ORM框架。为了提升性能,我们将Redis作为Mybatis的二级缓存。为了测试我们的代码,我们编写了单元测试,并且用H2内存数据库来生成我们的测试数据。通过该项目,我们希望读者可以快速掌握现代化Java Web开发的技巧以及最佳实践。

本文的示例代码可在Github中下载:

https://github.com/JerryOrange/Spring-Boot-Mybatis-Redis-as-L2cache

2.Spring Boot

接下来,我们要编写Web API。假设我们的Web工程负责处理商家的产品(Product)。我们需要提供根据product id返回product信息的get接口和更新product信息的put接口。首先我们定义Product类,该类包括产品id,产品名称name以及价格price:

public class Product implements Serializable {

    //采用的redis序列化方式是默认的jdk序列化。所以数据库的查询对象实体需要实现Serializable接口。
    private static final long serialVersionUID = 1L;

    private long id;
    private String name;
    private long price;

    public long getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

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

    public long getPrice() {
        return price;
    }

    public void setPrice(long price) {
        this.price = price;
    }
}

然后我们需要定义Controller类。由于Spring Boot内部使用Spring MVC作为它的Web组件,所以我们可以通过注解的方式快速开发我们的接口类:

@RestController
@RequestMapping("/product")
public class ProductController {
    @Autowired
    private ProductService productService;

    @GetMapping("/{id}")
    public Product getProductInfo( @PathVariable("id") Long productId) {
        return productService.select(productId);
    }

    @PutMapping("/{id}")
    public Product updateProductInfo(
        @PathVariable("id") Long productId,
        @RequestBody Product newProduct) {
        Product product = productService.select(productId);
        if (product == null) {
            throw new ProductNotFoundException(productId);
        }
        product.setName(newProduct.getName());
        product.setPrice(newProduct.getPrice());
        productService.update(product);
        return product;
    }
}

我们简单介绍一下上述代码中所用到的注解的作用:

  • @RestController:表示该类为Controller,并且提供Rest接口,即所有接口的值以Json格式返回。该注解其实是@Controller@ResponseBody的组合注解,便于我们开发Rest API。
  • @RequestMapping@GetMapping@PutMapping:表示接口的URL地址。标注在类上的@RequestMapping注解表示该类下的所有接口的URL都以/product开头。@GetMapping表示这是一个Get HTTP接口,@PutMapping表示这是一个Put HTTP接口。
  • @PathVariable@RequestBody:表示参数的映射关系。假设有个Get请求访问的是/product/123,那么该请求会由getProductInfo方法处理,其中URL里的123会被映射到productId中。同理,如果是Put请求的话,请求的body会被映射到newProduct对象中。

这里我们只定义了接口,实际的处理逻辑还未完成,因为product的信息都存在数据库中。接下来我们将在项目中集成mybatis,并且与数据库做交互。

3.集成Mybatis

配置数据源

首先我们需要在配置文件中配置我们的数据源。我们采用mysql作为我们的数据库。这里我们采用yaml作为我们配置文件的格式。我们在resources目录下新建application.yml文件:

spring:
  # 数据库配置
  datasource:
    url: jdbc:mysql://{your_host}/{your_db}
    username: {your_username}
    password: {your_password}
    driver-class-name: org.gjt.mm.mysql.Driver

由于Spring Boot拥有自动配置的特性,我们不用新建一个DataSource的配置类,Sping Boot会自动加载配置文件并且根据配置文件的信息建立数据库的连接池,十分便捷。

推荐大家采用yaml作为配置文件的格式。xml显得冗长,properties没有层级结构,yaml刚好弥补了这两者的缺点。这也是Spring Boot默认就支持yaml格式的原因。

配置Mybatis

我们已经通过Spring Initializer在pom.xml中引入了mybatis-spring-boot-starte库,该库会自动帮我们初始化mybatis。首先我们在application.yml中填写mybatis的相关配置:

# mybatis配置
mybatis:
  # 配置映射类所在包名
  type-aliases-package: com.ncu.jerry.dao
  # 配置mapper xml文件所在路径,这里是一个数组
  mapper-locations:
    - mappers/ProductMapper.xml

然后,再在代码中定义ProductMapper类:

@Mapper //Spring Boot在初始化mybatis时会自动加载该mapper类。
public interface ProductDao {
    Product select(@Param("id") long id);

    void update(Product product);
}

这里,只要我们加上了@Mapper注解,Spring Boot在初始化mybatis时会自动加载该mapper类。

Spring Boot之所以这么流行,最大的原因是它自动配置的特性。开发者只需要关注组件的配置(比如数据库的连接信息),而无需关心如何初始化各个组件,这使得我们可以集中精力专注于业务的实现,简化开发流程。

访问数据库

完成了Mybatis的配置之后,我们就可以在我们的接口中访问数据库了。我们在ProductController下通过@Autowired引入mapper类,并且调用对应的方法实现对product的查询和更新操作,这里我们以查询接口为例:

@RestController
@RequestMapping("/product")
public class ProductController {
    @Autowired
    private ProductService productService;

    @GetMapping("/{id}")
    public Product getProductInfo( @PathVariable("id") Long productId) {
        return productService.select(productId);
    }
}

然后在你的mysql中插入几条product的信息,就可以运行该项目看看是否能够查询成功了。

至此,我们已经成功地在项目中集成了Mybatis,增添了与数据库交互的能力。但是这还不够,一个现代化的Web项目,肯定会上缓存加速我们的数据库查询。接下来,将介绍如何将Redis集成到Mybatis的二级缓存中,实现数据库查询的自动缓存。

4.集成Redis

配置Redis

同访问数据库一样,我们需要配置Redis的连接信息。在application.yml文件中增加如下配置:

  #配置Redis
  redis:
    # redis数据库索引(默认为0),我们使用索引为1的数据库
    database: 1
    # redis服务器地址(默认为localhost)
    host: localhost
    # redis端口(默认为6379)
    port: 6379
    # redis访问密码(默认为空)
    password:
    # redis连接超时时间(单位为毫秒)
    timeout: 0
    # redis连接池配置
    pool:
      # 最大可用连接数(默认为8,负数表示无限)
      max-active: 8
      # 最大空闲连接数(默认为8,负数表示无限)
      max-idle: 8
      # 最小空闲连接数(默认为0,该值只有为正数才有作用)
      min-idle: 0
      # 从连接池中获取连接最大等待时间(默认为-1,单位为毫秒,负数表示无限)
      max-wait: -1

 上述列出的都为常用配置,读者可以通过注释信息了解每个配置项的具体作用。由于我们在pom.xml中已经引入了spring-boot-starter-data-redis库,所以Spring Boot会帮我们自动加载Redis的连接,具体的配置类org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration。通过该配置类,我们可以发现底层默认使用Jedis库,并且提供了开箱即用的redisTemplatestringTemplate

将Redis作为二级缓存

Mybatis的二级缓存原理本文不再赘述,读者只要知道,Mybatis的二级缓存可以自动地对数据库的查询做缓存,并且可以在更新数据时同时自动地更新缓存。

实现Mybatis的二级缓存很简单,只需要新建一个类实现org.apache.ibatis.cache.Cache接口即可。

该接口共有以下五个方法:

  • String getId():mybatis缓存操作对象的标识符。一个mapper对应一个mybatis的缓存操作对象。
  • void putObject(Object key, Object value):将查询结果塞入缓存。
  • Object getObject(Object key):从缓存中获取被缓存的查询结果。
  • Object removeObject(Object key):从缓存中删除对应的key、value。只有在回滚时触发。一般我们也可以不用实现,具体使用方式请参考:org.apache.ibatis.cache.decorators.TransactionalCache
  • void clear():发生更新时,清除缓存。
  • int getSize():可选实现。返回缓存的数量。
  • ReadWriteLock getReadWriteLock():可选实现。用于实现原子性的缓存操作。

接下来,我们新建RedisCache类,实现Cache接口:

public class RedisCache implements Cache {
    private static final Logger logger = LoggerFactory.getLogger(RedisCache.class);

    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private final String id; // cache instance id
    private RedisTemplate redisTemplate;

    private static final long EXPIRE_TIME_IN_MINUTES = 30; // redis过期时间

    public RedisCache(String id) {
        if (id == null) {
            throw new IllegalArgumentException("Cache instances require an ID");
        }
        this.id = id;
    }

    /**
     * mybatis缓存操作对象的标识符。一个mapper对应一个mybatis的缓存操作对象
     */
    @Override
    public String getId() {
        return id;
    }

    /**
     * Put query result to redis
     * 将查询结果塞入缓存
     * @param key
     * @param value
     */
    @Override
    public void putObject(Object key, Object value) {
        RedisTemplate redisTemplate = getRedisTemplate();
        ValueOperations opsForValue = redisTemplate.opsForValue();
        opsForValue.set(key, value, EXPIRE_TIME_IN_MINUTES, TimeUnit.MINUTES);
        logger.debug("Put query result to redis");
    }

    /**
     * Get cached query result from redis
     * 从缓存中获取被缓存的查询结果
     * @param key
     * @return
     */
    @Override
    public Object getObject(Object key) {
        RedisTemplate redisTemplate = getRedisTemplate();
        ValueOperations opsForValue = redisTemplate.opsForValue();
        logger.debug("Get cached query result from redis");
        return opsForValue.get(key);
    }

    /**
     * Remove cached query result from redis
     * 从缓存中删除对应的key、value。只有在回滚时触发。
     * 一般我们也可以不用实现,具体使用方式请参考:
     * org.apache.ibatis.cache.decorators.TransactionalCache
     *
     * @param key
     * @return
     */
    @Override
    public Object removeObject(Object key) {
        RedisTemplate redisTemplate = getRedisTemplate();
        redisTemplate.delete(key);
        logger.debug("Remove cached query result from redis");
        return null;
    }

    /**
     * Clears this cache instance
     * 发生更新时,清除缓存
     */
    @Override
    public void clear() {
        RedisTemplate redisTemplate = getRedisTemplate();
        redisTemplate.execute((RedisCallback) connection -> {
            connection.flushDb();
            return null;
        });
        logger.debug("Clear all the cached query result from redis");
    }

    /**
     * This method is not used
     * 可选实现。返回缓存的数量
     * @return
     */
    @Override
    public int getSize() {
        return 0;
    }

    /**
     * 可选实现。用于实现原子性的缓存操作
     */
    @Override
    public ReadWriteLock getReadWriteLock() {
        return readWriteLock;
    }

    private RedisTemplate getRedisTemplate() {
        if (redisTemplate == null) {
            redisTemplate = ApplicationContextHolder.getBean("redisTemplate");
        }
        return redisTemplate;
    }
}

讲解一下上述代码中一些关键点

  1. 自定义实现的二级缓存,必须要有一个带id的构造函数,否则会报错。
  2. 我们使用Spring封装的redisTemplate来操作Redis。网上介绍redis做二级缓存的文章都是直接用jedis库,但是redisTemplate封装了底层的实现,未来如果我们不用jedis了,我们可以直接更换底层的库,而不用修改上层的代码。更方便的是,使用redisTemplate,我们不用关心redis连接的释放问题,否则新手很容易忘记释放连接而导致应用卡死。
  3. 需要注意的是,这里不能通过autowire的方式引用redisTemplate,因为RedisCache并不是Spring容器里的bean。所以我们需要手动地去调用容器的getBean方法来拿到这个bean。
  4. 我们采用的redis序列化方式是默认的jdk序列化。所以数据库的查询对象(比如Product类)需要实现Serializable接口。

这样,我们就实现了一个优雅的、科学的并且具有Spring Style的Redis缓存类。

开启二级缓存

接下来,我们需要在ProductMapper.xml中开启二级缓存:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ncu.jerry.dao.ProductDao">

    <!-- 开启基于redis的二级缓存 -->
    <cache type="com.ncu.jerry.util.RedisCache"/>
    <cache/>

    <select id="select" resultType="Product">
        SELECT * FROM products WHERE id = #{id} LIMIT 1
    </select>

    <update id="update" parameterType="Product" flushCache="true">
        UPDATE products SET name = #{name}, price = #{price} WHERE id = #{id} LIMIT 1
    </update>
</mapper>

<cache type="com.ncu.jerry.util.RedisCache"/>表示开启基于redis的二级缓存,并且在update语句中,我们设置flushCachetrue,这样在更新product信息时,能够自动失效缓存(本质上调用的是clear方法)。

Mybatis默认对二级缓存是关闭的,一级缓存默认开启;

使用二级缓存需要注意事项:

二级缓存是建立在同一个namespace下的,如果对表的操作查询可能有多个namespace,那么得到的数据就是错误的。

举个简单的例子,订单和订单详情,orderMapper、orderDetailMapper、在查询订单详情时我们需要把订单信息也查询出来,那么这个订单详情的信息被二级缓存在orderDetailMapper的namespace中,这个时候有人要修改订单的基本信息,那就是在orderMapper的namespace下修改,他是不会影响到orderDetailMapper的缓存的,那么你再次查找订单详情时,拿到的是缓存的数据,这个数据其实已经是过时的。

根据以上,想要使用二级缓存时需要想好两个问题:

1)对该表的操作与查询都在同一个namespace下,其他的namespace如果有操作,就会发生数据过时。

2)对关联表的查询,关联的所有表的操作都必须在同一个namespace。

总之,操作与查询在同一个namespace下的查询才能缓存,其他namespace下的查询都可能出现问题。

5.测试

配置H2内存数据库

至此我们已经完成了所有代码的开发,接下来我们需要书写单元测试代码来测试我们代码的质量。我们刚才开发的过程中采用的是mysql数据库,而一般我们在测试时经常采用的是内存数据库。这里我们使用H2作为我们测试场景中使用的数据库。

要使用H2也很简单,只需要跟使用mysql时配置一下即可。在application.yml文件中:

# 测试环境下的配置
spring:
  profiles: test
  # 数据库配置
  datasource:
    url: jdbc:h2:mem:test
    username: root
    password: 123456
    driver-class-name: org.h2.Driver
    # schema.sql用于存放我们的建表语句,data.sql用于存放insert的数据
    # 初始化所需要的表结构以及数据,并在测试结束时销毁
    schema: classpath:schema.sql
    data: classpath:data.sql

为了避免和默认的配置冲突,我们用---另起一段,并且用profiles: test表明这是test环境下的配置。然后只要在我们的测试类中加上@ActiveProfiles(profiles = "test")注解来启用test环境下的配置,这样就能一键从mysql数据库切换到h2数据库。

在上述配置中,schema.sql用于存放我们的建表语句,data.sql用于存放insert的数据。这样当我们测试时,h2就会读取这两个文件,初始化我们所需要的表结构以及数据,然后在测试结束时销毁,不会对我们的mysql数据库产生任何影响。这就是内存数据库的好处。另外,别忘了在pom.xml中将h2的依赖的scope设置为test。

使用Spring Boot就是这么简单,无需修改任何代码,轻松完成数据库在不同环境下的切换。

编写测试代码

因为我们是通过Spring Initializer初始化的项目,所以已经有了一个测试类——SpringbootApplicationTests 

Spring Boot提供了一些方便我们进行Web接口测试的工具类,比如TestRestTemplate。然后在配置文件中我们将log等级调成DEBUG,方便观察调试日志。具体的测试代码如下:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) //测试类中的注解,来启用test环境下的配置,一键从mysql数据库切换到h2数据库。
@ActiveProfiles(profiles = "test")
public class SpringbootApplicationTests {
    @LocalServerPort
    private int port;
    @Autowired
    private TestRestTemplate restTemplate; //Web接口测试的工具类
    @Test
    public void test() {
        long productId = 1;
        Product product = restTemplate.getForObject("http://localhost:" + port + "/product/" + productId, Product.class);
        assertThat(product.getPrice()).isEqualTo(200);
        Product newProduct = new Product();
        long newPrice = new Random().nextLong();
        newProduct.setName("new name");
        newProduct.setPrice(newPrice);
        restTemplate.put("http://localhost:" + port + "/product/" + productId, newProduct);
        Product testProduct = restTemplate.getForObject("http://localhost:" + port + "/product/" + productId, Product.class);
        assertThat(testProduct.getPrice()).isEqualTo(newPrice);
    }
}

在上述测试代码中:

  1. 我们首先调用get接口,通过assert语句判断是否得到了预期的对象。此时该product对象会存入redis中。
  2. 然后我们调用put接口更新该product对象,此时redis缓存会失效。
  3. 最后我们再次调用get接口,判断是否获取到了新的product对象。如果获取到老的对象,说明缓存失效的代码执行失败,代码存在错误,反之则说明我们代码是OK的。

书写单元测试是一个良好的编程习惯。虽然会占用你一定的时间,但是当你日后需要做一些重构工作时,你就会感激过去写过单元测试的自己。

6.总结

使用Spring Boot快速搭建一个Web应用,并且采用Mybatis作为ORM框架。为了提升性能,将Redis作为Mybatis的二级缓存。

Spring Boot因其提供了各种开箱即用的插件,使得它成为了当今最为主流的Java Web开发框架之一。 通过 Spring Boot,创建新的 Spring 应用变得非常容易。

Mybatis是一个十分轻量好用的ORM框架。Redis是当今十分主流的分布式key-value型数据库,在web开发中,常用被用来缓存数据库的查询结果。

Demo中使用Spring Boot快速搭建一个Web应用,并且采用Mybatis作为我们的ORM框架。为了提升性能,将Redis作为Mybatis的二级缓存。并编写了单元测试,用H2内存数据库来生成我们的测试数据。通过该项目,我们希望读者可以快速掌握现代化Java Web开发的技巧以及最佳实践。

数据库二级缓存在Redis中的存储方式:Redis会自动的将Sql+条件+Hash等当做key值,而将查询结果作为value,只有请求中的所有参数都符合,那么就会自动获取redis中的二级缓存。

使用 YAML 表示配置属性(application.yml)相对于属性文件来说,YAML 是一个更好的配置文件格式。YAML 在 Ruby on Rails 中得到了很好的应用。SpringApplication 类也提供了对 YAML 配置文件的支持,只需要添加对 SnakeYAML 的依赖即可。

局限性:Mybatis的二级缓存只能通过flush整个数据库来实现缓存失效。

本文转载自:http://www.kissyu.org/

共有 人打赏支持
梨加橙
粉丝 34
博文 56
码字总数 43402
作品 0
南昌
后端工程师
私信 提问
Spring Boot学习笔记

多模块开发 [SpringBoot学习]-IDEA创建Gradle多Module结构的SpringBoot项目 RabbitMQ RabbitMQ 安装 linux安装RabbitMQ详细教程 Ubuntu 16.04 RabbitMq 安装与运行(安装篇) ubantu安装...

OSC_fly
2018/07/26
0
0
fast-spring-boot 快速开发项目

Introduction fast-spring-boot 集成Spring Boot 2.1,Mybatis,Mybatis Plus,Druid,FastJson,Redis,Rabbit MQ,Kafka等,可使用代码生成器快速开发项目,使用maven assembly打包发布 项目主页 ......

geekidea
2018/12/10
0
0
新书上架:《Spring Boot 开发实战》(基于 Kotlin + Gradle + Spring Boot 2.0 的企业级服务端开发实战)

新书上架:《Spring Boot 开发实战》 — 基于 Kotlin + Gradle + Spring Boot 2.0 的企业级服务端开发实战 京东下单链接 https://item.jd.com/31178320122.html 天猫下单链接 https://detail...

程序员诗人
2018/08/05
0
0
新书上架:《Spring Boot 开发实战》基于 Kotlin + Gradle + Spring Boot 2.0 的企业级服务端开发实战

新书上架:《Spring Boot 开发实战》 — 基于 Kotlin + Gradle + Spring Boot 2.0 的企业级服务端开发实战 京东下单链接 https://item.jd.com/31178320122.html 天猫下单链接 https://detail...

程序员诗人
2018/08/22
0
0
UDF 1.0 发布,基于 Spring Boot 的脚手架

更新: 添加动态数据源组件 添加redis缓存组件 添加分布式ID的datacenterId和workerId的动态分配策略 修改部分bug 添加mybatis组件 UDF 是基于 Spring Boot / Spring Cloud 的基础项目、脚手...

wangkang80
2017/09/08
2.3K
7

没有更多内容

加载失败,请刷新页面

加载更多

深入理解Spring MVC 思想

目录 一、前言 二、spring mvc 核心类与接口 三、spring mvc 核心流程图 四、spring mvc DispatcherServlet说明 五、spring mvc 父子上下文的说明 六、springMVC-mvc.xml 配置文件片段讲解 ...

呵呵哒灬
42分钟前
1
0
数据库技术-Mysql主从复制与数据备份

数据库技术-Mysql 主从复制的原理: MySQL中数据复制的基础是二进制日志文件(binary log file)。一台MySQL数据库一旦启用二进制日志后,其作为master,它的数据库中所有操作都会以“事件”...

须臾之余
昨天
12
0
Git远程仓库——GitHub的使用(一)

Git远程仓库——GitHub的使用(一) 一 、 Git远程仓库 由于你的本地仓库和GitHub仓库之间的传输是通过SSH加密的,所以需要一下设置: 步骤一、 创建SSH key 在用户主目录下,看看有没有.ss...

lwenhao
昨天
2
0
SpringBoot 整合

springBoot 整合模板引擎 SpringBoot 整合Mybatis SpringBoot 整合redis SpringBoot 整合定时任务 SpringBoot 整合拦截器...

细节探索者
昨天
1
0
第二个JAVA应用

第二个JAVA应用 方法一:配置文件: # cd /usr/local/tomcat/conf/# vim server.xml</Host> <Host name="www.wangzb.cc" appBase="/data/wwwroot/www.wangzb.cc" //引用所......

wzb88
昨天
0
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部