项目中,如果表与表之间没有任何关联关系,那这样使用缓存是没有什么问题的。那么如果表与表之间存在关联关系的情况,缓存问题该如何解决?
这里演示一下来说明问题,现在有student和school模块。
1、创建表
1.1 school
CREATE TABLE `school`
(
`id` varchar(255) NOT NULL COMMENT '主键',
`name` varchar(255) DEFAULT NULL COMMENT '学校名称',
PRIMARY KEY (`id`)
)
1.2 student
CREATE TABLE `student` (
`id` varchar(255) NOT NULL COMMENT '主键',
`name` varchar(255) DEFAULT NULL COMMENT '学生姓名',
`school_id` varchar(255) DEFAULT NULL COMMENT '学校id',
PRIMARY KEY (`id`)
)
2.创建vo
2.1 SchoolVo
@Data
public class SchoolVo {
/** 主键 */
private String id;
/** 学校名称 */
private String name;
/** 学生列表 */
private List<StudentVo> studentList;
}
2.2 StudentVo
@Data
public class StudentVo {
/** 主键 */
private String id;
/** 学校名称 */
private String name;
/** 学校vo */
private SchoolVo schoolVo;
}
3.接口
3.1 SchoolService
@Service("schoolService")
public class SchoolServiceImpl implements ISchoolService {
@Override
@Cacheable(cacheNames = "school", key = "#schoolId")
public SchoolVo getSchoolVoById(String schoolId){
// 查询数据
School school = schoolDao.getById(schoolId);
List<Student> studentList = studentService.findStudentBySchoolId(schoolId);
// 构造vo
SchoolVo schoolVo = _BeanUtil.copyProperties(school, SchoolVo.class);
schoolVo.setStudentList(_BeanUtil.copyList(studentList, StudentVo.class));
return schoolVo;
}
}
3.2 StudentService
@Service("studentService")
public class StudentServiceImpl implements IStudentService {
@Override
@Cacheable(cacheNames = "student", key = "#studentId")
public StudentVo getStudentVoById(String studentId){
// 查询数据
Student student = studentDao.getById(studentId);
School school = schoolService.getSchoolById(student.getSchoolId());
// 构造vo
StudentVo studentVo = _BeanUtil.copyProperties(student, StudentVo.class);
SchoolVo schoolVo = _BeanUtil.copyProperties(school, SchoolVo.class);
studentVo.setSchoolVo(schoolVo);
return studentVo;
}
}
4.表关联的缓存更新问题
到这里,大家就可以发现一个问题了:虽然getSchoolVoById和getStudentVoById的却是可以缓存,但是getSchoolVoById缓存school对象的同时,也缓存了关联的student。
那么试想以下,如果通过schoolService.update/delete接口修改/删除了school;或者通过studentService.add/update/delete接口新增/修改/删除了student,然后在调用getSchoolVoById接口,获取的缓存数据都是有问题的。
最简单的解决方案当然是在schoolService.update/delete或studentService.add/update/delete的时候,删除关联的缓存school::id。但是这么做显然效率不高,因为即使我进行了一次很小的修改,比如修改了school中某一个学生的name,也要把整个school::id删掉。而且更难的是,随着表之间关联表的增多,要删除关联的缓存会越来越多,要维护实时删除这种关联缓存会越来越来,并且系统的效率会越来越慢。
5.解决方案与思路
笔者借鉴mysql的思想,在mysql中,表跟表的关联,即使某个表被修改或者删除了,通过left join等关联查询,也可以立刻获取当最新的数据。因为mysql是实时通过主键和外键动态关联起来的,而不会把整个关联查询结果缓存起来;而且某个表的记录被修改或者删除,只会单单修改或删除被修改表的记录,并不会影响关联表的记录。
根据上面的思路,要解决关联表缓存更新问题,要点有二
一是mysql是单表存储的,那redis也要一组key对应一张表
二是mysql在select的时候是实时通过主键和外键动态关联,那么我们在获取vo的时候也要实时根据主键和外键动态关联起来。但是我们怎么实现动态关联呢?显然单纯用redis是无法实现的,应该是要通过java来实现。并且可以想下数据库多对多关系会新建一张关联表保存多对多关系,但是上面的student和school是一对多的关系,那么怎么实现?!其实一对多的关系也可以新建一张关系表来存放关联关系!那么我们只要在redis新开一组key来保存student和school的关联关系,不就可以实现了吗?
6.redis缓存结构设计
通过上面的思路,作出下面的redis key-value的设计
redis的key | redis的value |
---|---|
school::{id} | {id: “”, name: “”} school对象 |
student::{id} | {id: “”, name: “”, schoolId:“”} student对象 |
school::{id}::student | [studentId1, studentId2, studentId3…] school中的student |
{id}为数据真正的id。
本文举的例子是一对多的关联关系。当多对多的时候,可以拆分为双向一对多来解决。
7.根据缓存动态返回vo
实现代码如下,跟sql一样,查询关联查询结果时,并不会把关联查询结果缓存起来,而是动态的链接关联关系,最后拼装出关联结果
@Service("schoolService")
public class SchoolServiceImpl implements ISchoolService {
@Override
public SchoolVo getSchoolVoByIdInCache(String id, boolean isNeedStudent) throws Exception {
//1.根据入参id,匹配"school:id",找到school(走缓存)
ISchoolService _this = _SpringContextUtil.getBean(ISchoolService.class);
School school = _this.getSchoolById(id);
if(null == school){
return null;
}
SchoolVo schoolVo = _BeanUtil.copyProperties(school, SchoolVo.class);
if(isNeedStudent){
// 2.根据school:id:student,找到关联的studentId(走缓存)
String schoolStudentKey = _CacheKeyUtil.getSchoolStudentKey(school.getId());
List<String> studentIds = ObjectUtil.defaultIfNull(
stringRedisTemplate.opsForList().range(schoolStudentKey, 0, -1),
new ArrayList<>(0));
// 3.根据studentId,找到student(走缓存)
List<StudentVo> studentVoList = new ArrayList<>(studentIds.size());
for (String studentId : studentIds) {
StudentVo studentVo = studentService.getStudentVoByIdInCache(studentId, false);
studentVoList.add(studentVo);
}
// 4.构造Vo返回值(vo不缓存起来)
schoolVo.setStudentList(studentVoList);
}
return schoolVo;
}
@Cacheable(cacheNames = School.cacheName, key = "#id")
@Override
public School getSchoolById(String id) {
return schoolDao.getById(id);
}
}
@Service("studentService")
public class StudentServiceImpl implements IStudentService {
@Override
public StudentVo getStudentVoByIdInCache(String id, boolean isNeedSchool) throws Exception {
// 1.根据入参id,匹配"student:id",找到student(走缓存)
IStudentService _this = _SpringContextUtil.getBean(IStudentService.class);
Student student = _this.getStudentById(id);
if(null == student){
return null;
}
StudentVo studentVo = _BeanUtil.copyProperties(student, StudentVo.class);
// 2.根据schoolId, 匹配"school:schoolId",找到school(走缓存)
if (isNeedSchool) {
String schoolId = student.getSchoolId();
if(StrUtil.isNotBlank(schoolId)){
SchoolVo schoolVo = schoolService.getSchoolVoByIdInCache(schoolId, false);
studentVo.setSchoolVo(schoolVo);
}
}
// 3.构造Vo返回值(vo不缓存起来)
return studentVo;
}
@Cacheable(cacheNames = Student.cacheName, key = "#id", unless = "#result == null")
@Override
public Student getStudentById(String id){
return studentDao.getById(id);
}
}
8.拼装缓存返回Vo的好处
在新增、修改、删除的时候不用在删除关联的缓存,加快效率!而且特别对于修改操作,只需要修改自己所在的表和自己的缓存即可,不需要操作其他的缓存。
student增删改操作
- 当student新增的时候,只需要新增student记录、缓存student::id。在往school::{id}::student最后push当前studentId
- 当student被修改的时候,只需要修改student记录和刷新缓存school::id即可,并不需要删除关联缓存
- 当student被删除的时候,只需要删除student记录、删除缓存school::id、并且在school:🆔:student删除掉当前的studentId
school增删改操作
- 当school新增的时候,只需要新增school记录、缓存school::id
- 当school被修改的时候,只需要修改school记录和刷新缓存school::id即可,并不需要删除关联缓存
- 当school被删除的时候,只需要删除student记录、删除缓存school::id,并且删除school:🆔:student整个缓存
9.感谢
希望本文对您有帮助和启发,也欢迎在下方留言给出更多不同的解决方案思路,如果本文对您有帮助,请给笔者点赞+关注~