Java Web现代化开发:Spring Boot + Mybatis + Redis二级缓存

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
云数据库 RDS MySQL Serverless,0.5-2RCU 50GB
云数据库 RDS MySQL Serverless,价值2615元额度,1个月
简介: 背景Spring-Boot因其提供了各种开箱即用的插件,使得它成为了当今最为主流的Java Web开发框架之一。Mybatis是一个十分轻量好用的ORM框架。

背景

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

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

本文的示例代码可在Github中下载:https://github.com/Lovelcp/spring-boot-mybatis-with-redis/tree/master

环境

开发环境:mac 10.11

ide:Intellij 2017.1

jdk:1.8

Spring-Boot:1.5.3.RELEASE

Redis:3.2.9

Mysql:5.7

Spring-Boot

新建项目

首先,我们需要初始化我们的Spring-Boot工程。通过Intellij的Spring Initializer,新建一个Spring-Boot工程变得十分简单。首先我们在Intellij中选择New一个Project:

img_f5dc6cf1447629850b7037017c854a75.jpe

然后在选择依赖的界面,勾选Web、Mybatis、Redis、Mysql、H2:

img_092deacf54c4974d3b4555a1c6c37e0f.jpe

新建工程成功之后,我们可以看到项目的初始结构如下图所示:

img_b82c4ed5927a4e7713a3c41928f9daa4.jpe

Spring Initializer已经帮我们自动生成了一个启动类——SpringBootMybatisWithRedisApplication。该类的代码十分简单:


@SpringBootApplication

publicclassSpringBootMybatisWithRedisApplication{

publicstaticvoidmain(String[] args){

SpringApplication.run(SpringBootMybatisWithRedisApplication.class, args);

}

}

@SpringBootApplication注解表示启用Spring Boot的自动配置特性。好了,至此我们的项目骨架已经搭建成功,感兴趣的读者可以通过Intellij启动看看效果。

新建API接口

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


publicclassProductimplementsSerializable{

privatestaticfinallongserialVersionUID =1435515995276255188L;

privatelongid;

privateString name;

privatelongprice;

// getters setters

}

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

@RestController

@RequestMapping("/product")

publicclassProductController{

@GetMapping("/{id}")

publicProductgetProductInfo(

@PathVariable("id")

                    Long productId) {

// TODO

returnnull;

    }

@PutMapping("/{id}")

publicProductupdateProductInfo(

@PathVariable("id")

                    Long productId,

            @RequestBody

                    Product newProduct) {

// TODO

returnnull;

    }

}

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

@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,并且与数据库做交互。

集成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.wooyoo.learning.dao.domain

# 配置mapper xml文件所在路径,这里是一个数组

  mapper-locations:

    -mappers/ProductMapper.xml

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

@Mapper

publicinterfaceProductMapper{

Productselect(

@Param("id")

longid);

voidupdate(Product product);

}

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

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

访问数据库

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

@RestController

@RequestMapping("/product")

publicclassProductController{

@Autowired

privateProductMapper productMapper;

@GetMapping("/{id}")

publicProductgetProductInfo(

@PathVariable("id")

                    Long productId) {

returnproductMapper.select(productId);

    }

// 避免篇幅过长,省略updateProductInfo的代码

}

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

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

集成Redis

配置Redis

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

spring:

  redis:

# redis数据库索引(默认为0),我们使用索引为3的数据库,避免和其他数据库冲突

    database:3

# 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库,并且提供了开箱即用的redisTemplate和stringTemplate。

将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接口:

publicclassRedisCacheimplementsCache{

privatestaticfinalLogger logger = LoggerFactory.getLogger(RedisCache.class);

privatefinalReadWriteLock readWriteLock =newReentrantReadWriteLock();

privatefinalString id;// cache instance id

privateRedisTemplate redisTemplate;

privatestaticfinallongEXPIRE_TIME_IN_MINUTES =30;// redis过期时间

publicRedisCache(String id){

if(id ==null) {

thrownewIllegalArgumentException("Cache instances require an ID");

        }

this.id = id;

    }

@Override

publicStringgetId(){

returnid;

    }

/**

    * Put query result to redis

    *

*@paramkey

*@paramvalue

    */

@Override

@SuppressWarnings("unchecked")

publicvoidputObject(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

    *

*@paramkey

*@return

    */

@Override

publicObjectgetObject(Object key){

        RedisTemplate redisTemplate = getRedisTemplate();

        ValueOperations opsForValue = redisTemplate.opsForValue();

logger.debug("Get cached query result from redis");

returnopsForValue.get(key);

    }

/**

    * Remove cached query result from redis

    *

*@paramkey

*@return

    */

@Override

@SuppressWarnings("unchecked")

publicObjectremoveObject(Object key){

        RedisTemplate redisTemplate = getRedisTemplate();

        redisTemplate.delete(key);

logger.debug("Remove cached query result from redis");

returnnull;

    }

/**

    * Clears this cache instance

    */

@Override

publicvoidclear(){

        RedisTemplate redisTemplate = getRedisTemplate();

        redisTemplate.execute((RedisCallback) connection -> {

            connection.flushDb();

returnnull;

        });

logger.debug("Clear all the cached query result from redis");

    }

@Override

publicintgetSize(){

return0;

    }

@Override

publicReadWriteLockgetReadWriteLock(){

returnreadWriteLock;

    }

privateRedisTemplategetRedisTemplate(){

if(redisTemplate ==null) {

redisTemplate = ApplicationContextHolder.getBean("redisTemplate");

        }

returnredisTemplate;

    }

}

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

自己实现的二级缓存,必须要有一个带id的构造函数,否则会报错。

我们使用Spring封装的redisTemplate来操作Redis。网上所有介绍redis做二级缓存的文章都是直接用jedis库,但是笔者认为这样不够Spring Style,而且,redisTemplate封装了底层的实现,未来如果我们不用jedis了,我们可以直接更换底层的库,而不用修改上层的代码。更方便的是,使用redisTemplate,我们不用关心redis连接的释放问题,否则新手很容易忘记释放连接而导致应用卡死。

需要注意的是,这里不能通过autowire的方式引用redisTemplate,因为RedisCache并不是Spring容器里的bean。所以我们需要手动地去调用容器的getBean方法来拿到这个bean,具体的实现方式请参考Github中的代码。

我们采用的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">

<!-- 开启基于redis的二级缓存 -->

        SELECT * FROM products WHERE id = #{id} LIMIT 1

        UPDATE products SET name = #{name}, price = #{price} WHERE id = #{id} LIMIT 1

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

测试

配置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: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初始化的项目,所以已经有了一个测试类——SpringBootMybatisWithRedisApplicationTests。

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

@RunWith(SpringRunner.class)

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)

@ActiveProfiles(profiles ="test")

publicclassSpringBootMybatisWithRedisApplicationTests{

@LocalServerPort

privateintport;

@Autowired

privateTestRestTemplate restTemplate;

@Test

publicvoidtest(){

longproductId =1;

Product product = restTemplate.getForObject("http://localhost:"+ port +"/product/"+ productId, Product.class);

assertThat(product.getPrice()).isEqualTo(200);

Product newProduct =newProduct();

longnewPrice =newRandom().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);

    }

}

在上述测试代码中:

我们首先调用get接口,通过assert语句判断是否得到了预期的对象。此时该product对象会存入redis中。

然后我们调用put接口更新该product对象,此时redis缓存会失效。

最后我们再次调用get接口,判断是否获取到了新的product对象。如果获取到老的对象,说明缓存失效的代码执行失败,代码存在错误,反之则说明我们代码是OK的。

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

查看测试结果

我们在Intellij中点击执行测试用例,测试结果如下:

img_30af3bb97de9f4dbc1016f9c52aa9324.jpe

真棒,显示的是绿色,说明测试用例执行成功了。

总结

本篇文章介绍了如何通过Spring Boot、Mybatis以及Redis快速搭建一个现代化的Web项目,并且同时介绍了如何在Spring Boot下优雅地书写单元测试来保证我们的代码质量。当然这个项目还存在一个问题,那就是mybatis的二级缓存只能通过flush整个DB来实现缓存失效,这个时候可能会把一些不需要失效的缓存也给失效了,所以具有一定的局限性。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
1天前
|
存储 消息中间件 缓存
Redis缓存技术详解
【5月更文挑战第6天】Redis是一款高性能内存数据结构存储系统,常用于缓存、消息队列、分布式锁等场景。其特点包括速度快(全内存存储)、丰富数据类型、持久化、发布/订阅、主从复制和分布式锁。优化策略包括选择合适数据类型、设置过期时间、使用Pipeline、开启持久化、监控调优及使用集群。通过这些手段,Redis能为系统提供高效稳定的服务。
|
2天前
|
缓存 监控 NoSQL
Redis缓存雪崩及应对策略
缓存雪崩是分布式系统中一个常见但危险的问题,可以通过合理的缓存策略和系统设计来降低发生的概率。采用多层次的缓存架构、缓存预热、合理的缓存失效时间等措施,都可以有效应对缓存雪崩,提高系统的稳定性和性能。在实际应用中,及时发现并解决潜在的缓存雪崩问题,是保障系统可用性的关键一环。
30 14
|
5天前
|
XML 监控 安全
18:面向切面编程-Java Spring
18:面向切面编程-Java Spring
24 5
|
5天前
|
缓存 NoSQL Java
17:缓存机制-Java Spring
17:缓存机制-Java Spring
19 5
|
5天前
|
Java 数据库连接 数据库
16:事务-Java Spring
16:事务-Java Spring
21 5
|
5天前
|
SQL Java 数据库连接
15:MyBatis对象关系与映射结构-Java Spring
15:MyBatis对象关系与映射结构-Java Spring
24 4
|
5天前
|
缓存 NoSQL Java
优化Redis缓存:解决性能瓶颈和容量限制
优化Redis缓存:解决性能瓶颈和容量限制
16 0
|
5天前
|
存储 缓存 NoSQL
Redis缓存满了怎么办?
选择哪种方法取决于您的应用需求和数据访问模式。需要根据实际情况来决定如何处理Redis缓存满的情况。
29 1
|
NoSQL Java 测试技术
redis入门及java操作
redis 命令可以去菜鸟教程http://www.runoob.com/redis/redis-tutorial.html 或者以下地址去学习http://www.cnblogs.com/huangxincheng/category/755864.html redis在linux下的安装及启动 下载地址:http://redis.io/download,下载最新文档版本
1600 0
|
27天前
|
存储 NoSQL 算法
09- Redis分片集群中数据是怎么存储和读取的 ?
Redis分片集群使用哈希槽分区算法,包含16384个槽(0-16383)。数据存储时,通过CRC16算法对key计算并模16383,确定槽位,进而分配至对应节点。读取时,根据槽位找到相应节点直接操作。
59 12