本文给出了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的请求
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.
如有错误敬请大神指正!