实习记录1——数据库查询n+1.使用字符串在服务间传递任意文件

规避数据库会产生的1+n查询

1.直接使用mybatis配置的1+n查询

在数据库查询中很容易会一对多或者一对一的查询语句,通常遇到这种情况 ,一种很简单又很有问题的方式就是直接使用数据访问框架直接配置一对一或者一对多的查询。比如说在msbatis中的xml文件或者注解上面直接加上出一对多(一对一)的配置。

// 一个学生对应一个教室,一个教室有多个学生
@Data
public class ClassRoom {
   private Integer id;
   private String name;
}
@Data
public class Student {
   private Integer id;
   private Integer classRoom;
   private ClassRoom classRoomObj;
}
// 直接使用mybatis进行一对多映射
@Mapper
public interface StudentMapper {
    @Select("select id,class_room from student")
    @Results({
            @Result(id = true, column = "id", property = "id"),
            @Result(column = "class_room", property = "classRoomObj", one = @One(select = "com.example.demo.mysql_n_plus_one.mapper.ClassRoomMapper.getById"))
    })
    List<Student> getStudents();
}
@Mapper
public interface ClassRoomMapper {
    @Select("select * from class_room where id = #{id}")
    ClassRoom getById(@Param("id") int id);
}

当在mybatis这样配置后,mybatis执行

 List<Student> students = mapper.getStudents();

这个查询时,会将所有的student的数据从数据库一次性查询出来。然后使用这些字段依次创建结果对应的的对象Student,然后当创建对象时,发现需要classRoom字段的属性需要嵌套查询,于是mybatis执行配置的嵌套查询的getById方法查询数据库的数据。然后将查询到的数据创建一个ClassRoom对象将其塞进Student对象中。于是,于是如果第一次查询出来的Student的数量有10000个,那么查询的次数就需要1+10000次,这个次数是很大的。
另一方面,在mybatis中,它对查询执行了一些优化,在1+10000次查询的时候,每一次查询都会将查询到的结果缓存在本地。如果在10000次查询ClassRoom的过程中,如果调用查询的方法与参数已经被查询过,那么这次查询会直接走缓存而不是请求数据库。所以在使用这个mybatis的查询时,1+n的 n 的数值等于student表和class_room表记录数的最小值。

2.拆分成两次查询的 1+m次查询

上面的方式并不被推荐,在这之上有一种较好的方式。当我们需要查询一个一对多或者一对一查询的时候,可以分成两个部分,首先可以将所有需要的Student的实例全部记录查询出来,然后将需要查询的classRoom的所有id拿出来,然后我们就可以使用这些id进行批量查询。这种查询方式,通常批量查询的批量数据的阈值可以设置在1000,因为在sql的字符串的数值太多可能会出现查询错误。于是m的值大小就等于n/1000,这样就相当于减少了1000倍的请求数据库次数。接着,这m次查询本来是完全不相关的,因此,如果将这m次查询的使用线程池查询,然后把线程的尺寸设置为20,那么久相当于把请求的时间降低了20倍…

@Mapper
public interface ClassRoomMapper {
    // not impl
    List<ClassRoom> getByIds(@Param("ids") List<Integer> ids);
}
@Mapper
public interface StudentMapper {
    @Select("select id,class_room from student")
    List<Student> getAll();

}

// for test
ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args);
StudentMapper mapper = ctx.getBean(StudentMapper.class);
ClassRoomMapper classRoomMapper = ctx.getBean(ClassRoomMapper.class);
// 线程池
ExecutorService executorService = Executors.newFixedThreadPool(20);
// 查询出所有的Student和所有需要的classRoom的id
List<Student> ss = mapper.getAll();
List<Integer> ids = new ArrayList<>(ss.stream().map(Student::getClassRoom).collect(Collectors.toSet()));
// 使用线程池和批量查询
final int executeSize = 1000;
List<ClassRoom> rooms = new ArrayList<>();
List<Future<List<ClassRoom>>> futures = new ArrayList<>();
// 将集合分片
for (int s=0,e=s+executeSize; s<ss.size(); s+=executeSize,e=e+executeSize){
     int start = s , end = Math.min(e,ss.size());
     futures.add(executorService.submit(()->classRoomMapper.getByIds(ids.subList(start, end))));
}
try {
     for (Future<List<ClassRoom>> future : futures) {
         rooms.addAll(future.get());
     }
}catch (Exception ignored){}
// 设置ClassRoomObj属性
Map<Integer,ClassRoom> roomMap = rooms.stream().collect(Collectors.toMap(ClassRoom::getId,k1->k1,(k1,k2)->k1));
ss.forEach(s->s.setClassRoomObj(roomMap.get(s.getClassRoom()))); 

3.最好的方式,使用sql的子查询,1次查询

以上的查询并不是最好的方式,无论是性能还是简洁性上来说。果然到最后,无论什么方式都会回归致简的方式。为什么会有上面两种方式的思考呢?其第一个原因没有写出来,就是我们查询出来的数据肯定是会被分页的,如果不使用分页,只是把所有的数据查询出来然后用where过滤,那么直接使用join语句可以很完美地将两张表的数据一次性查询出来,但是当需要分页的时候且如果两张表的关系的是一对多,那么直接使用join我们的数据分页就很难实施,因为这无法计算分页的起点和终点。
例如,我们有两张表,左边和右表,我们按照左表的数据进行分页,且左表的一条记录对应右表的多条记录,那么直接使用join无法计算记录的开始与结束位置。于是就慢慢想出了以上的两种查询方式。
但是果然还是我太菜了,本来可以直接使用子查询查出来的。

@Data
public class StudentAndClassRoom {
    private Integer id;
    private Integer classRoom;
    private String name;
}
// 直接上xml的sql
<select id="getStudentsLimit10" resultType="com.example.demo.mysql_n_plus_one.beans.StudentAndClassRoom">
        select
         s.id as id,
         s.class_room as classRoom,
         r.name
        from (
         select id,class_room
         from student  
         limit 0,100
         ) as s
         left join class_room as r
         on s.class_room = r.id
  </select>

直接在子查询里面进行分页与条件筛选,然后使用筛选结果和右表进行join,无论一对一还是一对多都可以进行分页。如果是一对多的关系,那么就将查询到的结果集在java程序中按照StudentAndClassRoom 的id进行分组,分组的结果每个list都可以对应到一个结果的Student。这种查询方式,不需要额外的线程,且只需要使用1次数据库请求

使用Base64字符串在服务之间传递任意的文件

1.服务方将文件上传到文件服务器

当页面请求一个文件时,前端将这个生成文件的请求发送到服务。服务方即刻响应前端的请求,提示前端任务已经提交。而页面接受到这个响应,立马调用js开始向前端轮询,判定文件是否已经生成,如果生成就开始下载。而另一方面,服务方在提交这个任务之后,立马在后台开始生成文件,成功后将文件上传到文件服务器,或者失败后也上传错误的结果。这种方式在各个层次的响应都很宽,但是每一个方面的耦合性也很大!

2.服务方将文件需要数据传输到前端,然后在前端生成文件通过io流将文件传输到页面

这样的做法就是服务方需要做的事情很少,但是增加了前端的逻辑,其实前端不需要有这么些复杂的逻辑。

3.服务方通过将文件的byte流通过base64编码成一个巨型字符串,然后将这个巨型字符串发送到前端,前端把字符串解码,再输出到达页面

这种方式,在前端需要做的事情就很少了,只需要将字符串解码,然后使用byte数组生成一个文件,这种方式的话前端的逻辑就会很轻,而且效率比较也可观了(直接使用字符串api的new String和String.getByte不能将原来的byte数组还原)

// 服务方读取一个文件,也可以读取一个流
FileInputStream fis = new FileInputStream("123.txt");
BufferedInputStream bi = new BufferedInputStream(fis);
byte[]bys = new byte[bi.available()];
bi.read(bys);
// 将流转换为base64编码
return Base64.getEncoder().encodeToString(bys);
// 调用方
byte []exportBytes = Base64.getDecoder().decode(base64String);
// 输出byte流
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值