springboot cache缓存之访问数据库(以mysql为例)

本文给出了springboot cache缓存在访问数据库中的应用,首先给出了缓存的概念、适用场景,然后给出了完整的java程序和代码注释,最后指出了使用springboot cache常见的问题和解决方案。写下此文为了记录下最近研究springboot cache遇到的坑和解决方案,也希望能够对其他的初学者有一些帮助。

什么是缓存cache?

外存:可以简单的理解为电脑上的CDEF盘和U盘;此类储存器一般断电后仍然能保存数据。

内存:内存(Memory)也被称为内存储器,其作用是用于暂时存放CPU中的运算数据,以及与硬盘等外部存储器交换的数据。只要计算机在运行中,CPU就会把需要运算的数据调到内存中进行运算,当运算完成后CPU再将结果传送出来,一般断电后数据就会被清空。

高速缓存cache:高速缓存是用来协调CPU与主存之间的差异而设置的。一般情况下,CPU的工作速度高,但内存的工作速度相对较低,为了解决这个问题,通常使用高速缓存,高速缓存的存取速度介于CPU和主存之间。系统将一些CPU在近几个时间段经常访问的内容存入高速缓冲,当CPU需要使用数据时,先在高速缓存中找,如果找到,就不必访问内存了,找不到时,再找内存,这样就在一定程度上缓解了由于主存速度低造成的CPU“停工待料”的情况

java程序中什么情况下需要用到缓存cache?

java程序的性能在很多情况下会受制于数据库的访问,这是因为访问数据库是一个很耗时的操作。但是高速缓存cache的访问速度是非常快的,因此为了提高java程序的执行效率,一些频繁访问且一般来说不会变化的数据会被放置在缓存中,而非访问数据库。

springboot缓存注解

缓存注解一般写在service层的方法上,在本文的例子中是使用@Cacheable注解在UserServiceImpl类中queryAllPerson方法上来声明改方法中读取数据库表person所有row缓存在内存中(即首次select * from person时访问数据库,之后使用缓存来代替访问耗时的数据库访问)。

完整的java程序和相关配置

首先在springboot配置文件application.properties添加数据库的相关配置信息。

# 日志中打印所执行的sql查询语句
spring.jpa.show-sql=true
spring.jpa.database = MYSQL
spring.datasource.url=jdbc:mysql://localhost:3306/alex?useSSL=false
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
/**
 * 定义object relationship mapping
 * 即java类和数据库table的对应关系
 * @Entity申明此类是一个实体类
 * @Table声明表的对应关系
 * 注意@Id是javax.persistence.Id
 * */

package com.example.cache;
//import org.springframework.data.annotation.Id; 
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "person")
public class Person {
    // @Id注解必须要有,否则编译时会报错找不到identifier(Caused by: org.hibernate.AnnotationException: No identifier specified for entity: com.example.dbaccess.Person)
    // 被@Id注解的成员变量对应着table的主键,且被@Id注解的成员变量的数据类型必须和UserRepository的JpaRepository<Person, String>中的ID一致,但是可以和table中对应的字段数据类型不同(比如person表中的id字段就是int类型)。其中第一个参数是bean类,第二个参数是ID
    // @GeneratedValue(strategy=GenerationType.AUTO)
    // @GeneratedValue和@Id配合使用,用于指定主键生成的方式,不显式声明的情况下默认是AUTO,即jpa自动选择。

    @Id
    private Integer id;
    private String name;
    private Integer age;
    private Boolean gender;
    /**
     * java教程中:强调写了有参数的构造方法就最好加一个无参数的构造方法.
     * 但是springboot教程中:当声明了有参的构造方法的话,必须显式地声明无参的构造方法否则报如下错误:
     * No default constructor for entity: : com.example.dbaccess.Person; nested exception is org.hibernate.InstantiationException: No default constructor for entity: :
     *
     * */

    // 有参构造方法
    public Person(Integer id, String name, Integer age, boolean gender) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.gender = gender;
    }

    // 无参构造方法
    public Person() {
    }

    public int getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

    public boolean isGender() {
        return gender;
    }

    public void setGender(boolean gender) {
        this.gender = gender;
    }

    @Override
    public String toString() {
        return String.format("{person:{id:%s, name=%s, age=%s, gender=%s}}", id, name, age, gender);
        //return "person.tostring";
    }

}






/**
 * dao/repository层
 * 对数据库进行操作的代码
 * 由于我们导入了JPA依赖,很多通常的查询,删除,更新等操作可以不用在此编写
 * @Repository注解是告诉Spring Boot这是一个仓库类,会在启动的时候帮你自动注入(查询,删除,更新等操作的代码,比如jdbc)。
 * JpaRepository里面含有一般数据库操作所需要的接口,我们会在service层中调用他们
 * */
package com.example.cache;

import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface UserRepository extends JpaRepository<Person, Integer> {
    // 其中Person是与数据库表person对应的entity类,Integer是@Id注解的主键列

    // Spring Data Jpa 默认实现时hibernate,我们都知道hibernate使用HQL查询(Hibernate时JPA的实现之一),
    // 而不推荐使用sql查询,因为这样子就跟具体数据库耦合了。
    // 但是如果需要使用原生的sql查询语句就需要将nativeQuery设置为true(默认是false)。
    // @Query--执行sql语句并将结果作为被@Query注解的方法return的内容
    @Query(value = "select name from person", nativeQuery = true)
    List<String> queryAllName();
    @Query(value = "select * from person", nativeQuery = true)
    List<Person> queryAllPerson();
}






/**
 * service层:主要负责业务逻辑处理
 * 在此处我们编写了一个接口和一个实现类。
 * 接口的设计是为了满足松耦合的需求。
 *
 * */
package com.example.cache;

import org.springframework.stereotype.Service;
import java.util.List;

@Service
public interface UserService {
    List<Person> getAll();
    List<String> queryAllName();
    List<Person> queryAllPerson(String key);
}





/**
 * service层:主要负责业务逻辑处理
 * */
package com.example.cache;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    UserRepository userRepository;

    @Override
    public List<Person> getAll() {
        return userRepository.findAll();
    }

    @Override
    public List<String> queryAllName() {
        return userRepository.queryAllName();
    }

    // 缓存cache注解
    @Cacheable(cacheNames = "queryPerson", key = "'user_'.concat(#a0)")
    @Override
    public List<Person> queryAllPerson(String key) {
        System.out.println(">>>> queryPerson executed.");
        return userRepository.queryAllPerson();
    }

    public void delById(int id){
        userRepository.deleteById(id);

    }

    public void addRecord(Person person){
        userRepository.save(person);
    }

}







/**
 * 编写Control层
 * 此层主要进行对页面的处理(和页面有mapping),包括跳转或者传参等等。
 * */
package com.example.cache;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.*;

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

@Configuration
@RestController
@RequestMapping("controller")
public class UserController implements Serializable {
    private static Logger logger = LoggerFactory.getLogger(UserController.class);
    @Autowired
    private UserServiceImpl userServiceImpl;
    @RequestMapping("/name")
    @ResponseBody
    private List<Person> getNames(){
        List<String> nameArr = new ArrayList<>();
        List<Person> personArr = new ArrayList<>();
        for (Person person: userServiceImpl.getAll()){
            String name =  person.getName();
            //logger.info(">>>>>> name is: " + name);
            nameArr.add(name);
            personArr.add(person);
        }
        System.out.println(">>>>>> execute method.");
        return personArr;
    }
    /**
     * 注意:class和method上面都需要@RequestMapping标注,这样访问的网址是:
     * localhost:port/controller/name
     * 其中host默认是8080,如果在application.properties中修改了server.port,则以配置文件为准
     * */

    @RequestMapping("/del")
    private String delRecord(@RequestParam("id") int id){
        userServiceImpl.delById(id);
        return String.format(">>>>>> record (id=%s) deleted.", id);

    }

    @RequestMapping(value = "/add", method = RequestMethod.GET)
    private String addRecord(@RequestParam(name="id", required = true) Integer id, @RequestParam(name="name",required=false) String name, @RequestParam(name="age",required=false) Integer age, @RequestParam(name="gender",required=false) Boolean gender) {
        userServiceImpl.addRecord(new Person(id, name, age, gender));
        System.out.println(">>>>>> execute method.");
        return String.format(">>>>>> record (id=%s, name=%s, age=%s, gender=%s) added.", id, name, age, gender);
        //return "add succeed.";
    }

    @ResponseBody
    @RequestMapping(value = "/query", method = RequestMethod.GET)
    private List<String> queryAllNames(){
        return userServiceImpl.queryAllName();
    }

    @ResponseBody
    @RequestMapping(value = "/queryPerson", method = RequestMethod.GET)
    private List<Person> queryAllPersons(){
        return userServiceImpl.queryAllPerson("queryP");
    }
}






/**
 * 主类
 * */
package com.example.cache;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching  // 开启缓存
public class UserMain {
    @Autowired
    private UserController controller;

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

    }
}

程序执行结果

step 1. 在web端发起读取mysql中person表所有row的请求
query-test.png
console中打印显示执行了一次读取数据库的操作

>>>> queryPerson executed.
Hibernate: select * from person

step 2. 刷新页面以再次发起同样的请求
此时console中并没有打印新的sql,如下图所示:

>>>> queryPerson executed.
Hibernate: select * from person

因此可以看出当web端第2、3…次发起请求后,web server端并没有再次去访问数据库而是从cache中读取了之前缓存的结果并返回到web端。

使用springboot cache常见问题和解决方案:
// 会出错的写法,引用 参数 name作为Cache的key。
@Cacheable(cachnames="user", key="#name")
public User findByName(String name);
报错信息:java.lang.IllegalArgumentException: Null key returned for cache operation (maybe you are using named params on classes without debug info......

原因分析:上述代自定义了Key,因此Key generator不起效,它会用SpEL来把 name 这个参数作为Key。这是官网教程的例子,为什么会行不通呢?再反复斟酌官网教程,终于明白它说什么了,就是如果编译没选debug模式,编译出来的class文件是没有参数名的信息的,那么反射机制来获取这个参数的值,就找不到参数名字!只能用 #a0或者#p0来指代第一个参数,依此类推。(If for some reason the names are not available (e.g. no debug information), the argument names are also available under the #a<#arg> where #arg stands for the argument index (starting from 0).)

正确的万能写法,这里把 user_ 作为 key的前缀,传参 name作为后缀。
@Cacheable(cachnames=“user”, key="‘user_’.concat(#a0)")
public User findByName(String name);
关于原因分析:参考了大神的博客,https://www.jianshu.com/p/6196dd5870c7.

如有错误敬请大神指正!

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,以下是一个简单的示例程序,包含了Controller层、Service层、Service实现类、Dao层以及Mapper.xml文件。该程序会统计本周第一天、本月第一天、本年第一天起到今天为止的中奖趋势,并对接口进行缓存处理,过期时间为10分钟。 Controller层代码: ``` @RestController @RequestMapping("/lottery") public class LotteryController { @Autowired private LotteryService lotteryService; @GetMapping("/trend") public Result getTrend() { return Result.success(lotteryService.getTrend()); } } ``` Service层代码: ``` public interface LotteryService { List<Trend> getTrend(); } ``` Service实现类代码: ``` @Service public class LotteryServiceImpl implements LotteryService { @Autowired private LotteryDao lotteryDao; @Override @Cacheable(value = "trend", keyGenerator = "customKeyGenerator") public List<Trend> getTrend() { LocalDate now = LocalDate.now(); LocalDate weekStart = now.with(DayOfWeek.MONDAY); LocalDate monthStart = now.withDayOfMonth(1); LocalDate yearStart = now.withDayOfYear(1); return lotteryDao.getTrend(weekStart, monthStart, yearStart, now); } } ``` Dao层代码: ``` public interface LotteryDao { List<Trend> getTrend(@Param("weekStart") LocalDate weekStart, @Param("monthStart") LocalDate monthStart, @Param("yearStart") LocalDate yearStart, @Param("now") LocalDate now); } ``` Mapper.xml文件代码: ``` <select id="getTrend" parameterType="map" resultType="Trend"> SELECT date, COUNT(*) count FROM lottery WHERE date BETWEEN #{weekStart} AND #{now} GROUP BY date UNION SELECT date, COUNT(*) count FROM lottery WHERE date BETWEEN #{monthStart} AND #{now} GROUP BY date UNION SELECT date, COUNT(*) count FROM lottery WHERE date BETWEEN #{yearStart} AND #{now} GROUP BY date </select> ``` 其中,Trend是一个简单的数据类,用于表示中奖趋势: ``` public class Trend { private LocalDate date; private int count; // getter/setter } ``` 在以上代码中,我们使用了Spring框架提供的缓存注解@Cacheable,将getTrend方法的返回值缓存起来,缓存的key使用了自定义的keyGenerator,用于保证不同的请求使用不同的缓存。 为了让@Cacheable注解生效,我们还需要在启动类上添加@EnableCaching注解。 缓存过期时间为10分钟,可以在application.properties配置文件中添加以下配置: ``` spring.cache.redis.time-to-live=600000 ``` 其中,600000表示缓存过期时间,单位为毫秒,即10分钟。 最后,我们需要自定义一个缓存key生成器,用于生成不同的缓存key。代码如下: ``` @Component public class CustomKeyGenerator implements KeyGenerator { @Override public Object generate(Object target, Method method, Object... params) { StringBuilder sb = new StringBuilder(); sb.append(target.getClass().getName()); sb.append(method.getName()); for (Object obj : params) { sb.append(obj.toString()); } return sb.toString(); } } ``` 以上就是一个简单的使用Java、SpringBoot和MyBatis框架连接MySQL数据库的示例程序,包含了Controller层、Service层、Service实现类、Dao层以及Mapper.xml文件,统计本周第一天、本月第一天、本年第一天起到今天为止的中奖趋势接口,并对接口进行缓存处理,过期时间为10分钟。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值