Redis+mybatis分布式缓存





完整代码地址:https://gitee.com/Gsomeone/my-batis-redis-cache

环境搭建有点全,可以跳过。



环境搭建

建表
CREATE TABLE teacher(
		id VARCHAR(255) PRIMARY KEY,
		name VARCHAR(255)
)
CREATE TABLE student (
		id VARCHAR(255) PRIMARY KEY,
		name VARCHAR(255),
		tid VARCHAR(255) REFERENCES teacher(id)
)


创建springboot项目

我们首先搭建一个spring+mybatis环境

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--junit-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>
        <!--mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.3</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>
    </dependencies>


实体类
package com.gu.entity;

import lombok.Data;
import lombok.experimental.Accessors;

import java.io.Serializable;
import java.util.List;

@Data
@Accessors(chain = true)
// 必须实现Serializable,不然无法序列化
public class Teacher implements Serializable {
    private String id;
    private String name;
    private List<Student> students;
}

package com.gu.entity;

import lombok.Data;
import lombok.experimental.Accessors;

import java.io.Serializable;

@Data
@Accessors(chain = true)
public class Student implements Serializable {
    private String id;
    private String name;
    private String tid;
}


Dao接口
package com.gu.dao;

import com.gu.entity.Student;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
public interface StudentDao {
    List<Student> findAll();

    Student findOne(String id);

    void delete(String id);

    void save(Student student);

    void update(Student student);
}




注解扫描dao包
package com.gu;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.gu.dao")
public class RedisStudyApplication {

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

}

package com.gu.dao;

import com.gu.entity.Teacher;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
public interface TeacherDao {
    List<Teacher> findAll();

    Teacher findOne(String id);

    void delete(String id);

    void save(Teacher teacher);

    void update(Teacher teacher);
}



Mapper配置文件
<?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.gu.dao.StudentDao">
    <select id="findAll" resultType="Student">
        select id, name, tid from student
    </select>

    <select id="findOne" parameterType="String" resultType="Student">
        select id, name, tid from student where id = #{id}
    </select>

    <delete id="delete" parameterType="String">
        delete from student where id = #{id}
    </delete>

    <insert id="save" parameterType="Student">
        insert into student (id, name, tid) values (#{id}, #{name}, #{tid});
    </insert>

    <update id="update" parameterType="Student">
        update student
        <set>
            <if test="name != null">
                name = #{name},
            </if>
            <if test="tid != null">
                tid = #{tid},
            </if>
        </set>
        where id = #{id};
    </update>
</mapper>

<?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.gu.dao.TeacherDao">
    <select id="findAll" resultMap="TeacherStudent">
        select t.id, t.name, s.id, s.name
        from teacher t, student s
        where t.id = s.tid
    </select>

    <select id="findOne" parameterType="String" resultType="Teacher">
        select id, name from teacher where id = #{id};
    </select>

    <delete id="delete" parameterType="String">
        delete from teacher where id = #{id}
    </delete>

    <insert id="save" parameterType="Teacher">
        insert into teacher (id, name) values (#{id}, #{name});
    </insert>

    <update id="update" parameterType="Teacher">
        update teacher
        <set>
            <if test="name != null">
                name = #{name},
            </if>
        </set>
        where id = #{id};
    </update>
    
    <!--结果集-->
    <resultMap id="TeacherStudent" type="Teacher">
        <result property="id" column="id"/>
        <result property="name" column="name"/>
        <collection property="students" ofType="Student">
            <result column="id" property="id"/>
            <result column="name" property="name"/>
        </collection>
    </resultMap>
</mapper>


Service接口
package com.gu.service;

import com.gu.entity.Student;

import java.util.List;

public interface StudentService {
    List<Student> findAll();

    Student findOne(String id);

    void delete(String id);

    void save(Student student);

    void update(Student student);
}

package com.gu.service;

import com.gu.entity.Teacher;

import java.util.List;

public interface TeacherService {
    List<Teacher> findAll();

    Teacher findOne(String id);

    void delete(String id);

    void save(Teacher teacher);

    void update(Teacher teacher);
}



Service实现类
package com.gu.service;

import com.gu.dao.StudentDao;
import com.gu.entity.Student;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.UUID;

@Service
@Transactional
public class StudentServiceImpl implements StudentService {
    @Autowired
    private StudentDao studentDao;

    @Override
    @Transactional(propagation = Propagation.SUPPORTS)
    public List<Student> findAll() {
        return studentDao.findAll();
    }

    @Override
    @Transactional(propagation = Propagation.SUPPORTS)
    public Student findOne(String id) {
        return studentDao.findOne(id);
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public void delete(String id) {
        studentDao.delete(id);
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public void save(Student student) {
        student.setId(UUID.randomUUID().toString());
        studentDao.save(student);
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public void update(Student student) {
        studentDao.update(student);
    }
}

package com.gu.service;

import com.gu.dao.TeacherDao;
import com.gu.entity.Teacher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.UUID;

@Service
@Transactional
public class TeacherServiceImpl implements TeacherService {
    @Autowired
    private TeacherDao teacherDao;

    @Override
    @Transactional(propagation = Propagation.SUPPORTS)
    public List<Teacher> findAll() {
        return teacherDao.findAll();
    }

    @Override
    @Transactional(propagation = Propagation.SUPPORTS)
    public Teacher findOne(String id) {
        return teacherDao.findOne(id);
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public void delete(String id) {
        teacherDao.delete(id);
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public void save(Teacher teacher) {
        teacher.setId(UUID.randomUUID().toString());
        teacherDao.save(teacher);
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public void update(Teacher teacher) {
        teacherDao.update(teacher);
    }
}


application.properties
# datasource
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/study?characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=root
# mybatis
mybatis.type-aliases-package=com.gu.entity
mybatis.mapper-locations=classpath:com/gu/mapper/*.xml
# redis
spring.redis.host=192.168.70.130
spring.redis.port=6379
spring.redis.database=0
#log
logging.level.com.gu.dao=debug




MyBatis开启缓存

​ mybatis的一级缓存在SqlSession,默认开启。二级缓存在对应namespace的mapper文件中,需要手动开启。

如何开启maybatis二级缓存?

​ 答:只需要在mapper文件下添加cache标签即可。例如:

<?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.gu.dao.StudentDao">
    <cache/>

    <select id="findAll" resultType="Student">
        select id, name, tid from student
    </select>

    ...
</mapper>



查询测试

@Test
public void testFindAll() {
	studentService.findAll().forEach(student -> System.out.println(student));
	System.out.println("==============================================");
	studentService.findAll().forEach(student -> System.out.println(student));
}

在这里插入图片描述

总结:可以看到第一次查询没有在缓存中找到,是通过sql查询数据库得到的。而第二次查询,在缓存中找到了,所以没有查询数据库。



深入探究

cache标签的type属性可以选择不同的缓存,比如默认的type是PerpetualCache,即

/**
 *    Copyright 2009-2019 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.apache.ibatis.cache.impl;

import java.util.HashMap;
import java.util.Map;

import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.CacheException;

/**
 * @author Clinton Begin
 */
public class PerpetualCache implements Cache {

  private final String id;

  private final Map<Object, Object> cache = new HashMap<>();

  public PerpetualCache(String id) {
    this.id = id;
  }

  @Override
  public String getId() {
    return id;
  }

  @Override
  public int getSize() {
    return cache.size();
  }

  @Override
  public void putObject(Object key, Object value) {
    cache.put(key, value);
  }

  @Override
  public Object getObject(Object key) {
    return cache.get(key);
  }

  @Override
  public Object removeObject(Object key) {
    return cache.remove(key);
  }

  @Override
  public void clear() {
    cache.clear();
  }

  @Override
  public boolean equals(Object o) {
    if (getId() == null) {
      throw new CacheException("Cache instances require an ID.");
    }
    if (this == o) {
      return true;
    }
    if (!(o instanceof Cache)) {
      return false;
    }

    Cache otherCache = (Cache) o;
    return getId().equals(otherCache.getId());
  }

  @Override
  public int hashCode() {
    if (getId() == null) {
      throw new CacheException("Cache instances require an ID.");
    }
    return getId().hashCode();
  }
}

​ 从31行可以看出,PerpetualCache是基于HashMap的,它的putObject,getObject方法就是对HashMap的put,get。 而HashMap在多线程下是不安全的,因此PerpetualCache也是线程不安全的,但是Redis是单线程是安全的。我们只需要实现Cache接口就能写RedisCache





MyBatis+Redis缓存

package com.gu.utils;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class ApplicationContextUtils implements ApplicationContextAware {
    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    public static Object getBean(String beanName) {
        return applicationContext.getBean(beanName);
    }
}



package com.gu.cache;

import com.gu.utils.ApplicationContextUtils;
import org.apache.ibatis.cache.Cache;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.util.DigestUtils;

public class RedisCache implements Cache {
    private final String id;

    public RedisCache(String id) {
        this.id = id;
    }

    @Override
    public String getId() {
        return id;
    }

    @Override
    public void putObject(Object key, Object value) {
        getRedisTemplate().opsForHash().put(id, getKeyToMD5(key.toString()), value);
    }

    @Override
    public Object getObject(Object key) {
        return getRedisTemplate().opsForHash().get(id, getKeyToMD5(key.toString()));
    }

    @Override
    public Object removeObject(Object key) {
        return null;
    }

    @Override
    public void clear() {
        System.out.println("清空缓存-----");
        getRedisTemplate().delete(id);
    }

    @Override
    public int getSize() {
        return getRedisTemplate().opsForHash().size(id).intValue();
    }

    public RedisTemplate getRedisTemplate() {
        RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtils.getBean("redisTemplate");
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        return redisTemplate;
    }

    public String getKeyToMD5(String key) {
        return DigestUtils.md5DigestAsHex(key.getBytes());
    }
}



​ 增删改的时候,为了保证Redis中的数据和数据库中具有一致性,应该同时删除Redis中对于的缓存。通过打断点我们可以知道,调用的是clear()方法,而不是removeObject()方法。执行增删改时,会执行clear方法,清除对应缓存。

​ 由于student和teacher存在多对一,所以学生类的缓存应该被老师类包含。我们可以直接使用cache-ref将学生类缓存到老师类下。

代码如下:

<?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.gu.dao.TeacherDao">
    <cache type="com.gu.cache.RedisCache"/>

    <select id="findAll" resultMap="TeacherStudent">
        select t.id, t.name, s.id, s.name
        from teacher t, student s
        where t.id = s.tid
    </select>

    ......
</mapper>

<?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.gu.dao.StudentDao">
    <cache-ref namespace="com.gu.dao.TeacherDao"/>

    <select id="findAll" resultType="Student">
        select id, name, tid from student
    </select>

    ......
</mapper>



总结:

  1. ​ 使用散列表来缓存查询的结果。断点调试可以发现,这里的id就是对应的mapper文件中的namespacecom.gu.dao.StudentDao,而key和value对应就是的查询语句和查询结果。

findAll" resultMap=“TeacherStudent”>
select t.id, t.name, s.id, s.name
from teacher t, student s
where t.id = s.tid

......
```
<?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.gu.dao.StudentDao">
    <cache-ref namespace="com.gu.dao.TeacherDao"/>

    <select id="findAll" resultType="Student">
        select id, name, tid from student
    </select>

    ......
</mapper>



总结:

  1. ​ 使用散列表来缓存查询的结果。断点调试可以发现,这里的id就是对应的mapper文件中的namespacecom.gu.dao.StudentDao,而key和value对应就是的查询语句和查询结果。

  2. 如果是设置具有时效性的数据,可以在putObject()方法中执行expire()设置生存时间。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值