1、为什么要实现二级缓存呢?
不为什么,单纯是为了将热点数据和非热点数据分一下类,根本上来讲就是为了提高系统性能,扩大吞吐量。要是在说原因的话,可能是为了避免一级缓存的热点数据失效时,不至于将大量的请求涌向数据库,这也是解决缓存雪崩的方案之一。
2、二级缓存的设计思路?
有了前面学习的ehcache和redis,及他们与springBoot整合的案例后,在springBoot下用ehcache结合redis实现二级缓存系统就比较简单了。首先说一下整体思路,我打算用ehcache结合spring的缓存注解来实现一级缓存,用redis来充当二级缓存。理由如下:参照CPU的L1 cache、L2 cache、L3 cache的设计功能与原则,L1 cache是最快的同时也是容量最小的,这和它使用的存储介质SRAM有关,L2 cache容量比L1大,但速度却比L1慢得多,L3和L2的关系类似。我们横向类比一下,假设我们的程序就是CPU,那么本地缓存ehcache、分布式缓存redis和本地磁盘就相当于CPU的L1、L2、L3缓存了。首先ehcache数据存储在JVM堆内存中,这对于java程序(CPU)来说是天然的优势,因为java程序就是跑在JVM里面的,访问速度不受网络影响,自然要快于redis,而且既然数据处于JVM堆内存中,也注定了数据存储量不会太大,正好符合CPU中L1 cache的特点。用redis来充当二级缓存,因为redis是通过网络socket进行数据传输的,redis服务器可能不和java程序部署在同一台机器上,分离的越远“redis的性能瓶颈”就越明显,所以访问速度上要鳗鱼ehcache的,但由于redis属于分布式缓存,服务本身可以独立部署运行,而且可以做集群,所以容量上可以远大于ehcache,也正好符合的CPU中L2 cache的特点。最底层的当然是我们的数据库了,数据存储在磁盘中,访问速度最慢,但数据不易丢失且容量最大。
我们这么设计,除了根据数据传输速度来划分一级、二级、三级缓存之外,还可以在缓存不同的数据内容上面做一些文章。CPU的L1 cache快于L2 cache根本原因是硬件SRAM的速度快于RAM,我们软件层面无法做硬件层面的区分,但可以通过一些设计使之更像L1和L2 cache,比如,我们在一级缓存中存储最热的那些数据,次热的数据放到二级缓存里面;一级缓存存储由多个二级缓存经过复杂计算后的计算结果等方式。
更多CPU缓存请看百度百科:https://baike.baidu.com/item/%E7%BC%93%E5%AD%98/100710?fr=aladdin
3、一个基本的二级缓存小案例
首先,我们将@Cacheable注解打在了controller的方法上,用来缓存该controller方法的返回值,下次在请求该controller方法时,会直接走ehcache的缓存。若一级缓存中有数据就直接返回;若没有再调用service层的方法,service层首先调用二级缓存redis,若有数据直接返回并将结果回写到一级缓存ehcache的中;若redis中也没有则查询数据库,将结果回写到redis中,再将service层的返回值回写到ehcache中。对于@Cacheable注解标注的方法,若该方法里面只是进行了一些简单的查询计算,而且被调用的次数还不是很频繁,那么该一级缓存的收益就很小,可以不将这样的返回值放到一级缓存中,直接调用service层的方法走二级缓存即可;但如果该方法会被频繁调用,或者方法里面进行了复杂的查询和计算,那么将这样的方法的返回值放到一级缓存里面,收益就很高了。
controller层
@RestController
public class EmployeeController {
@Autowired
private EmployeeService employeeService;
@Cacheable(cacheNames = "employee",key = "#id")
@GetMapping("/emp/{id}")
public Employee getEmpById(@PathVariable("id") Integer id){
Employee employee = employeeService.getEmployeeById(id);
return employee;
}
@CachePut(cacheNames = "employee",key = "#employee.id")
@PostMapping("/addEmp")
public Employee addEmp(Employee employee){
employeeService.addEmployee(employee);
return employee;
}
//@CachePut进行的是更新缓存而不是删除更新,不符合cache-aside pattern,建议手动删除ehcache中的缓存,而不是使用@CachePut注解
@CachePut(cacheNames = "employee",key = "#employee.id")
@PostMapping("/updateEmp")
public Employee updateEmployee(Employee employee){
employeeService.updateEmployee(employee);
return employee;
}
@CacheEvict(cacheNames = "employee",key = "#id")
//@CacheEvict(cacheNames = "employee",allEntries = true)
@GetMapping("/deleteEmp")
public String deleteEmployee(Integer id){
System.out.println("删除id为"+id+"的员工。");
return "删除成功!";
}
}
serviceImpl层
@Service
public class EmployeeServiceImpl implements EmployeeService {
@Autowired
private EmployeeMapper employeeMapper;
//@Cacheable(cacheNames = "employee",key = "#id")
@Override
public Employee getEmployeeById(Integer id) {
String redisValue = RedisUtils.get("employee:" + id);
if (!StringUtils.isEmpty(redisValue)) {
return JsonUtils.stringToObject(redisValue, Employee.class);
}else {
//这里可以加个锁,防止大并发下的缓存击穿
Employee employee = employeeMapper.getEmployeeById(id);
//回写数据到redis中
String jsonEmp = JsonUtils.objectToString(employee);
RedisUtils.set("employee:"+id,jsonEmp);
return employee;
}
}
@Override
public void addEmployee(Employee employee) {
employeeMapper.addEmployee(employee);
}
@Override
public void updateEmployee(Employee employee) {
int b = employeeMapper.updateEmployee(employee);
//更新完数据库后,删除缓存,使用的是cache-aside pattern 模式
if (b == 1){
RedisUtils.del("employee:" + employee.getId());
}
}
}
可以看到serviceImpl层的第一个方法,我们光操作缓存就写了很多代码,而且多个这种方法的缓存处理逻辑几乎全一样,所以我们可以参照阿里的java开发规范中的工程规范中讲的那样,在service层和dao层之间引入manager层,用来处理这些通用的缓存处理代码。如果项目中使用的缓存模式确定下来后,还可以定义一个专用的缓存注解,利用aop来处理这些二级缓存和数据库之间的缓存操作。
行了,本文只是提供了一种二级缓存的设计思路,并配上了一个简单的小例子。实际在项目中去应用肯定也会遇到其他问题,不要怕,去探索吧!
下一篇:缓存设计和使用中的经典问题