Redis:redis关联表的缓存问题

本文探讨了在数据库表存在关联关系时,如何有效地使用缓存,特别是Redis。提出了借鉴MySQL的外键关联思想,通过单表存储和动态关联查询来解决缓存更新问题。设计了Redis的缓存结构,包括独立的表对象缓存和关联关系缓存,并展示了如何根据缓存动态构造VO对象,以避免在增删改操作时频繁删除关联缓存,提高系统效率。
摘要由CSDN通过智能技术生成

项目中,如果表与表之间没有任何关联关系,那这样使用缓存是没有什么问题的。那么如果表与表之间存在关联关系的情况,缓存问题该如何解决?

这里演示一下来说明问题,现在有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的keyredis的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增删改操作

  1. 当student新增的时候,只需要新增student记录、缓存student::id。在往school::{id}::student最后push当前studentId
  2. 当student被修改的时候,只需要修改student记录和刷新缓存school::id即可,并不需要删除关联缓存
  3. 当student被删除的时候,只需要删除student记录、删除缓存school::id、并且在school:🆔:student删除掉当前的studentId

school增删改操作

  1. 当school新增的时候,只需要新增school记录、缓存school::id
  2. 当school被修改的时候,只需要修改school记录和刷新缓存school::id即可,并不需要删除关联缓存
  3. 当school被删除的时候,只需要删除student记录、删除缓存school::id,并且删除school:🆔:student整个缓存

9.感谢

 希望本文对您有帮助和启发,也欢迎在下方留言给出更多不同的解决方案思路,如果本文对您有帮助,请给笔者点赞+关注~

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我叫985

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值