欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

springboot+mybatis+redis 二级缓存问题实例详解

程序员文章站 2023-12-02 20:11:16
前言 什么是mybatis二级缓存? 二级缓存是多个sqlsession共享的,其作用域是mapper的同一个namespace。 即,在不同的sqlsession中...

前言

什么是mybatis二级缓存?

二级缓存是多个sqlsession共享的,其作用域是mapper的同一个namespace。

即,在不同的sqlsession中,相同的namespace下,相同的sql语句,并且sql模板中参数也相同的,会命中缓存。

第一次执行完毕会将数据库中查询的数据写到缓存,第二次会从缓存中获取数据将不再从数据库查询,从而提高查询效率。

mybatis默认没有开启二级缓存,需要在全局配置(mybatis-config.xml)中开启二级缓存。

本文讲述的是使用redis作为缓存,与springboot、mybatis进行集成的方法。

1、pom依赖

使用springboot redis集成包,方便redis的访问。redis客户端选用jedis。

另外读写kv缓存会进行序列化,所以引入了一个序列化包。      

 <dependency>
   <groupid>org.springframework.boot</groupid>
   <artifactid>spring-boot-starter-redis</artifactid>
  </dependency>
  <dependency>
   <groupid>redis.clients</groupid>
   <artifactid>jedis</artifactid>
   <version>2.8.0</version>
  </dependency>
  <dependency>
   <groupid>com.alibaba</groupid>
   <artifactid>fastjson</artifactid>
   <version>1.2.19</version>
  </dependency>

依赖搞定之后,下一步先调通redis客户端。

2、redis访问使用的bean

增加configuration,配置jedisconnectionfactory bean,留待后面使用。

一般来讲,也会生成了redistemplate bean,但是在接下来的场景没有使用到。

@configuration
public class redisconfig {
 @value("${spring.redis.host}")
 private string host;
 // 篇幅受限,省略了
 @bean
 public jedispoolconfig getredisconfig(){
  jedispoolconfig config = new jedispoolconfig();
  config.setmaxidle(maxidle);
  config.setmaxtotal(maxtotal);
  config.setmaxwaitmillis(maxwaitmillis);
  config.setminidle(minidle);
  return config;
 }
 @bean(name = "jedisconnectionfactory")
 public jedisconnectionfactory getconnectionfactory(){
  jedisconnectionfactory factory = new jedisconnectionfactory();
  jedispoolconfig config = getredisconfig();
  factory.setpoolconfig(config);
  factory.sethostname(host);
  factory.setport(port);
  factory.setdatabase(database);
  factory.setpassword(password);
  factory.settimeout(timeout);
  return factory;
 }
 @bean(name = "redistemplate")
 public redistemplate<?, ?> getredistemplate(){
  redistemplate<?,?> template = new stringredistemplate(getconnectionfactory());
  return template;
 }
}

这里使用@value读入了redis相关配置,有更简单的配置读取方式(@configurationproperties(prefix=...)),可以尝试使用。

redis相关配置如下

#redis
spring.redis.host=10.93.84.53
spring.redis.port=6379
spring.redis.password=bigdata123
spring.redis.database=15
spring.redis.timeout=0
spring.redis.pool.maxtotal=8
spring.redis.pool.maxwaitmillis=1000
spring.redis.pool.maxidle=8
spring.redis.pool.minidle=0

redis客户端的配置含义,这里不再讲解了。pool相关的一般都和性能有关,需要根据并发量权衡句柄、内存等资源进行设置。

redis客户端设置好了,我们开始配置redis作为mybatis的缓存。

3、mybatis cache

这一步是最为关键的一步。实现方式是实现mybatis的一个接口org.apache.ibatis.cache.cache。

这个接口设计了写缓存,读缓存,销毁缓存的方式,和访问控制读写锁。

我们实现实现cache接口的类是mybatisrediscache。

mybatisrediscache.java

public class mybatisrediscache implements cache {
 private static jedisconnectionfactory jedisconnectionfactory;
 private final string id;
 private final readwritelock readwritelock = new reentrantreadwritelock();
 public mybatisrediscache(final string id) {
  if (id == null) {
   throw new illegalargumentexception("cache instances require an id");
  }
  this.id = id;
 }
 @override
 public void clear() {
  redisconnection connection = null;
  try {
   connection = jedisconnectionfactory.getconnection();
   connection.flushdb();
   connection.flushall();
  } catch (jedisconnectionexception e) {
   e.printstacktrace();
  } finally {
   if (connection != null) {
    connection.close();
   }
  }
 }
 @override
 public string getid() {
  return this.id;
 }
 @override
 public object getobject(object key) {
  object result = null;
  redisconnection connection = null;
  try {
   connection = jedisconnectionfactory.getconnection();
   redisserializer<object> serializer = new jdkserializationredisserializer();
   result = serializer.deserialize(connection.get(serializer.serialize(key)));
  } catch (jedisconnectionexception e) {
   e.printstacktrace();
  } finally {
   if (connection != null) {
    connection.close();
   }
  }
  return result;
 }
 @override
 public readwritelock getreadwritelock() {
  return this.readwritelock;
 }
 @override
 public int getsize() {
  int result = 0;
  redisconnection connection = null;
  try {
   connection = jedisconnectionfactory.getconnection();
   result = integer.valueof(connection.dbsize().tostring());
  } catch (jedisconnectionexception e) {
   e.printstacktrace();
  } finally {
   if (connection != null) {
    connection.close();
   }
  }
  return result;
 }
 @override
 public void putobject(object key, object value) {
  redisconnection connection = null;
  try {
   connection = jedisconnectionfactory.getconnection();
   redisserializer<object> serializer = new jdkserializationredisserializer();
   connection.set(serializer.serialize(key), serializer.serialize(value));
  } catch (jedisconnectionexception e) {
   e.printstacktrace();
  } finally {
   if (connection != null) {
    connection.close();
   }
  }
 }
 @override
 public object removeobject(object key) {
  redisconnection connection = null;
  object result = null;
  try {
   connection = jedisconnectionfactory.getconnection();
   redisserializer<object> serializer = new jdkserializationredisserializer();
   result = connection.expire(serializer.serialize(key), 0);
  } catch (jedisconnectionexception e) {
   e.printstacktrace();
  } finally {
   if (connection != null) {
    connection.close();
   }
  }
  return result;
 }
 public static void setjedisconnectionfactory(jedisconnectionfactory jedisconnectionfactory) {
  mybatisrediscache.jedisconnectionfactory = jedisconnectionfactory;
 }
}

注意:

可以看到,这个类并不是由spring虚拟机管理的类,但是,其中有一个静态属性jedisconnectionfactory需要注入一个spring bean,也就是在redisconfig中生成的bean。

在一个普通类中使用spring虚拟机管理的bean,一般使用springboot自省的springcontextaware。

这里使用了另一种方式,静态注入的方式。这个方式是通过rediscachetransfer来实现的。

4、静态注入

rediscachetransfer.java

@component
public class rediscachetransfer {
 @autowired
 public void setjedisconnectionfactory(jedisconnectionfactory jedisconnectionfactory) {
  mybatisrediscache.setjedisconnectionfactory(jedisconnectionfactory);
 }
}

可以看到rediscachetransfer是一个springboot bean,在容器创建之初进行初始化的时候,会注入jedisconnectionfactory bean给setjedisconnectionfactory方法的传参。

而setjedisconnectionfactory通过调用静态方法设置了类mybatisrediscache的静态属性jedisconnectionfactory。

这样就把spring容器管理的jedisconnectionfactory注入到了静态域。

到这里,代码基本已经搞定,下面是一些配置。主要有(1)全局开关;(2)namespace作用域开关;(3)model实例序列化。

5、mybatis二级缓存的全局开关

前面提到过,默认二级缓存没有打开,需要设置为true。这是全局二级缓存的开关。

mybatis的全局配置。

<?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>
 <!-- 全局参数 -->
 <settings>
  <!-- 使全局的映射器启用或禁用缓存。 -->
  <setting name="cacheenabled" value="true"/>
 </settings>
</configuration>

全局配置的加载在datasource中可以是这样的。

bean.setmapperlocations(new pathmatchingresourcepatternresolver().getresources("classpath:mybatis-mapper/*.xml"));

指定了mapper.xml的存放路径,在mybatis-mapper路径下,所有后缀是.xml的都会读入。

bean.setconfiglocation(new classpathresource("mybatis-config.xml"));

指定了mybatis-config.xml的存放路径,直接放在resource目录下即可。

@bean(name = "moonlightsqlsessionfactory")
 @primary
 public sqlsessionfactory moonlightsqlsessionfactory(@qualifier("moonlightdata") datasource datasource) throws exception {
  sqlsessionfactorybean bean = new sqlsessionfactorybean();
  bean.setdatasource(datasource);
  bean.setmapperlocations(new pathmatchingresourcepatternresolver().getresources("classpath:mybatis-mapper/*.xml"));
  bean.setconfiglocation(new classpathresource("mybatis-config.xml"));
  return bean.getobject();
 }

6、配置mapper作用域namespace

前面提到过,二级缓存的作用域是mapper的namespace,所以这个配置需要到mapper中去写。

<mapper namespace="com.kangaroo.studio.moonlight.dao.mapper.moonlightmapper">
 <cache type="com.kangaroo.studio.moonlight.dao.cache.mybatisrediscache"/>
 <resultmap id="geofencelist" type="com.kangaroo.studio.moonlight.dao.model.geofence">
 <constructor>
  <idarg column="id" javatype="java.lang.integer" jdbctype="integer" />
  <arg column="name" javatype="java.lang.string" jdbctype="varchar" />
  <arg column="type" javatype="java.lang.integer" jdbctype="integer" />
  <arg column="group" javatype="java.lang.string" jdbctype="varchar" />
  <arg column="geo" javatype="java.lang.string" jdbctype="varchar" />
  <arg column="createtime" javatype="java.lang.string" jdbctype="varchar" />
  <arg column="updatetime" javatype="java.lang.string" jdbctype="varchar" />
 </constructor>
 </resultmap>

<select id="querygeofence" parametertype="com.kangaroo.studio.moonlight.dao.model.geofencequeryparam" resultmap="geofencelist">
 select <include refid="base_column"/> from geofence where 1=1
 <if test="type != null">
  and type = #{type}
 </if>
 <if test="name != null">
  and name like concat('%', #{name},'%')
 </if>
 <if test="group != null">
  and `group` like concat('%', #{group},'%')
 </if>
 <if test="starttime != null">
  and createtime >= #{starttime}
 </if>
 <if test="endtime != null">
  and createtime <= #{endtime}
 </if>
 </select>
</mapper>

注意:

namespace下的cache标签就是加载缓存的配置,缓存使用的正式我们刚才实现的mybatisrediscache。

<cache type="com.kangaroo.studio.moonlight.dao.cache.mybatisrediscache"/>

这里只实现了一个查询querygeofence,你可以在select标签中,开启或者关闭这个sql的缓存。使用属性值usecache=true/false。

7、mapper和model

读写缓存model需要序列化:只需要类声明的时候实现seriaziable接口就好了。

public class geofence implements serializable {
 // setter和getter省略 
}
public class geofenceparam implements serializable {
 // setter和getter省略 
}

mapper就还是以前的写法,使用mapper.xml的方式这里只需要定义出抽象函数即可。

@mapper
public interface moonlightmapper {
 list<geofence> querygeofence(geofencequeryparam geofencequeryparam);
}

到这里,所有的代码和配置都完成了,下面测试一下。

8、测试一下

controller中实现一个这样的接口post。

@requestmapping(value = "/fence/query", method = requestmethod.post)
 @responsebody
 public responseentity<response> queryfence(@requestbody geofencequeryparam geofencequeryparam) {
  try {
   integer pagenum = geofencequeryparam.getpagenum()!=null?geofencequeryparam.getpagenum():1;
   integer pagesize = geofencequeryparam.getpagesize()!=null?geofencequeryparam.getpagesize():10;
   pagehelper.startpage(pagenum, pagesize);
   list<geofence> list = moonlightmapper.querygeofence(geofencequeryparam);
   return new responseentity<>(
     new response(resultcode.success, "查询geofence成功", list),
     httpstatus.ok);
  } catch (exception e) {
   logger.error("查询geofence失败", e);
   return new responseentity<>(
     new response(resultcode.exception, "查询geofence失败", null),
     httpstatus.internal_server_error);
  }

 使用curl发送请求,注意

1)-h - content-type:application/json方式

2)-d - 后面是json格式的参数包体

curl -h "content-type:application/json" -xpost http://。。。/moonlight/fence/query -d '{
 "name" : "test",
 "group": "test",
 "type": 1,
 "starttime":"2017-12-06 00:00:00",
 "endtime":"2017-12-06 16:00:00",
 "pagenum": 1,
 "pagesize": 8

请求了三次,日志打印如下,

可以看到,只有第一次执行了sql模板查询,后面都是命中了缓存。

在我们的测试环境中由于数据量比较小,缓存对查询速度的优化并不明显。这里就不过多说明了。

springboot+mybatis+redis 二级缓存问题实例详解