2024年3月


Redis24篇集合

1 介绍

AOF(Append Only File)持久化:以独立日志的方式存储了 Redis 服务器的顺序指令序列,并只记录对内存进行修改的指令。
当Redis服务发生雪崩等故障时,可以重启服务并重新执行AOF文件中的指令达到恢复数据的目的。也就是说,通过重放(replay),来重新建立 Redis 当前实例的内存数据结构。这种模式有没有很熟悉,可以联想到MySQL主从同步时的relay log。
相对于咱们上一篇介绍的《
RDB内存快照提供持久化能力
》定点快照的做法,AOF的主要作用是解决了数据持久化的实时性,目前已经是Redis持久化的主流方式。

2 AOF实现日志记录

2.1 开启AOF日志记录

1、
开启AOF日志记录:在redis.conf文件中,找到 APPEND ONLY MODE 设置

appendonly yes  # 默认不开启, 为 no

2、
配置默认文件名:在redis.conf文件中设置

appendfilename “appendonly.aof”

2.2 执行流程

image

流程如上图所示,我们解析如下:

2.2.1 将所有的写命令(set、hset)Append 到aof_buf缓冲区中

Redis 接收到
set keyName someValue
命令的时候,会先将数据写到内存,Redis 会按照如下格式写入 AOF 文件。

  1. *3
    :表示当前指令分为三个部分,每个部分都是
    $ +
    数字开头,后面是3部分的具体内容:指令、键、值。
  2. 数字:表示这部分的命令、键、值多占用的字节大小。比如
    $3
    表示这部分包含 3 个字符,也就是 set 的长度。

我们看看一个典型的aof文件示例,为了清晰表示,下面的注释都是手动加的:

[root@localhost bin]#vim appendonly.aof
#  执行 set key value
*3
$3           # 这边代表set命令,长度为3
set
$9 
user_name      # 这边代表keyName,长度为9
$5 
brand      #  这边代表keyValue,长度为5


# 执行 mset key1 1 ,key2 2 ,key33 3
# aof日志如下:
*7  # 本批命令需要往下读7行非 $ 开始的命令
$4  #接着读取4个字节宽度,‘mset’长度为4,记为 $4
mset
$4  #接着读取4个字节宽度,‘key1’长度为4,记为 $4
key1
$1  #接着读取1个字节宽度,‘1’长度为1,记为 $1
1
$4
key2
$1
2
$5  #接着读取的字节宽度,‘$key33’长度为5,记为 $5
key33
$1
3

2.2.2 AOF缓冲区根据策略向硬盘做sync同步

AOF为什么把命令append到aof_buf中,然后再进行同步?
这是因为Redis使用单进程响应命令(参考笔者这篇《
深刻理解高性能Redis的本质
》),如果每次写AOF文件命令都直接持久化到硬盘,那么操作会是不是被间断,且性能完全取决于硬盘I/O负载。这个跟 MySQL 就没啥区别了。
先写入缓冲区aof_buf中,Redis可以提供多种缓冲区同步硬盘的策略,在性能、安全、数据可靠性方面做出平衡。

同步策略需关注以下几个配置:

1、
appendfsync 模式

appendfsync always  # 接受写命令后立即写入磁盘,强持久化但执行慢,不推荐
appendfsync everysec # 每秒写入磁盘一次, 性能和持久化方面做了折中, 推荐
appendfsync no  #  依赖操作系统自身同步的配置和策略,性能较佳,但是没法保证实时和完全持久化

2、
no-appendfsync-on-rewrite
在 AOF 重写期间是否禁用 fsync。这可以提高重写性能,但可能会增加数据丢失的风险。

# 默认值:no
# 可选值:yes 或 no
no-appendfsync-on-rewrite yes

2.2.3 AOF文件Rewrite实现压缩

随着AOF文件越来越大,需要定期对AOF文件进行重写,达到压缩减负的目的,避免AOF文件过大导致性能和数据可靠性问题。
重写后的AOF文件变小的原因主要有以下几点:
1、
进程内已超时的数据不再写入
:在重写过程中,Redis不会将已经超时的数据写入新的AOF文件,这有助于减少不必要的数据记录。
2、
删除无效命令
:旧的AOF文件中可能包含无效的命令,如
del key1

hdel key2

srem keys

set a666666
等。重写过程会识别并删除这些无效命令,只保留最终数据的写入命令,从而减小了文件大小。
3、
合并多条写命令
:为了进一步优化AOF文件的大小,重写过程会将多条写命令合并为一个。例如,
lpush list a

lpush list b

lpush list c
可以合并为
lpush list a b c
。这种合并减少了命令的数量,进而减小了AOF文件的大小。
4、
防止单条命令过大
:对于某些操作类型(如list、set、hash、zset),为了防止单条命令过大造成客户端缓冲区溢出,重写过程会以64个元素为界拆分多条命令。虽然这在一定程度上可能增加了命令的数量,但它确保了每条命令的大小都在可控范围内,有助于维持整体文件大小的合理性。
总之AOF重写降低了文件占用空间,同时提升加载性能,因为更小的AOF 文件可以更快地被Redis加载。

AOF重写关注以下配置:
1、
auto-aof-rewrite-percentage
触发 AOF 重写的增长百分比。例如,如果当前 AOF 文件大小是 100MB,并且这个值设置为 100,那么当 AOF 文件增长到 200MB 时,说明增长了100%,Redis 会尝试重写 AOF。

# 默认值:`100`
`auto-aof-rewrite-percentage 100`

2、
auto-aof-rewrite-min-size

AOF 文件的最小大小,以便触发重写。即使 AOF 文件的增长百分比超过了
auto-aof-rewrite-percentage
设置的值,但如果文件大小小于这个值,Redis 也不会触发重写。

# 默认值:`64mb`
auto-aof-rewrite-min-size 64mb

image

2.2.4 故障重启时的数据恢复

当Redis服务器重启时,可以加载AOF文件进行数据恢复。
image

流程如下:

  1. 当AOF和RDB文件同时存在时,优先加载AOF
  2. 若关闭了AOF(apendonly no),则加载RDB文件
  3. 加载AOF/RDB成功之后,redis重启成功。如果无相关的持久化,则直接启动成功。
  4. 如果AOF/RDB 数据恢复存在错误,则启动失败,并打印输出错误信息

2.3 RDB和AOF的比较和混合持久化

咱们上一篇介绍了《
RDB内存快照提供持久化能力
》定点快照的用户,那RDB跟AOF究竟孰优孰虑?
现实情况下,无论使用RDB或者AOF都差点意思。使用 rdb 来恢复内存状态,势必会丢失一部分数据。使用 AOF 日志重放,重放对性能有一定的影响,而且在 Redis 实例很大的情况下,需要花费很长的时间。
Redis 4.0 解决了这个问题,才用了一个新的持久化模式——混合持久化,该 混合模式 默认是关闭状态的。
将 RDB 文件的内容和 rdb快照时间点之后的增量的 AOF 日志文件存在一起。这时候 AOF 日志不需要再是全量的日志,而是最近一次快照时间点之后到当下发生的增量 AOF 日志,通常这部分 AOF 日志很小。
所以执行有如下顺序:

  • 查找rdb内容,如果存在先加载 rdb内容再 重放剩余的 aof。
  • 没有rdb内容,直接以aof格式重放整个文件。
    这样快照就不用频繁的执行,同时由于 AOF 只需要记录最近一次快照之后的数据,不需要记录所有的操作,避免了出现单次重放文件过大的问题。

开启混合持久化模式:

aof-use-rdb-preamble yes

这个设置告诉Redis在AOF重写时使用混合持久化模式。当这个选项设置为yes时,重写后的AOF文件将包含RDB格式的数据前缀和AOF格式的增量修改操作。

总结

  • RDB提供了快照模式,记录某个时间的Redis内存状态。RDB设计了 bgsave 和写时复制,尽可能避免执行快照期间对读写指令的影响,但是频繁快照会给磁盘带来压力以及 fork 阻塞主线程。需把握频率。
  • AOF 日志存储了 Redis 服务的顺序指令序列,通过重放(replay)指令来写入日志文件,并通过写回策略来避免高频读写给Redis带来压力。
  • RDB快照的照片时间间隔,必然会带来数据缺失,如果允许分钟级别的数据丢失,可以只使用 RDB。
  • 如果只用 AOF,写回策略优先使用 everysec 的配置选项,因为它在可靠性和性能之间取了一个平衡。
  • 数据不能丢失时,内存快照和 AOF 的混合使用是一个很好的选择。

写在前面

在Java日常开发过程中,实现Excel文件的导入导出功能是一项常见的需求。

通过使用相关的Java库,如Apache POI、EasyPoi或EasyExcel,可以轻松地实现Excel文件的读写操作。

而这篇文章将介绍如何在Java中使用Apache POI、EasyPoi 和EasyExcel库来进行Excel文件的导入和导出操作,帮助您快速掌握这一实用的技能。

一、使用场景

下面是excel导入导出的几个最常用的使用场景。


企业管理系统
:企业管理系统通常需要导入员工信息、客户信息、销售数据等大量数据到系统中,以及导出报表、数据分析结果等信息。


学校教务系统
:学校教务系统可能需要导入学生信息、课程表、成绩等数据,以及导出学生成绩单、教师工资表等信息。


电子商务平台
:电子商务平台需要导入商品信息、订单数据等内容,以及导出销售报表、库存清单等信息,方便管理和分析。


人力资源管理系统
:人力资源管理系统需要导入员工档案、薪资信息等数据,以及导出薪资条、考勤报表等信息,方便人力资源管理和工资结算。


医院信息系统
:医院信息系统可能需要导入患者信息、医疗记录等数据,以及导出医疗报告、统计分析等信息,方便医护人员进行医疗服务和管理。

以上仅是一些常见的使用场景,实际上各种系统中的Excel导入导出功能可以根据具体需求进行定制和扩展

二、三个库简介

1、Apache POI

Apache POI是一个流行的Java库,用于处理Microsoft Office格式文件,包括Excel、Word和PowerPoint。它提供了丰富的API,可以创建、读取和修改各种类型的Office文档。

官网

https://poi.apache.org/

优点:

  1. 功能强大:支持处理复杂的Excel文件,包括单元格、样式、图表等内容。

  2. 稳定性高:作为一个成熟的开源项目,得到广泛支持和持续维护。

  3. 灵活性:可以满足各种定制化需求,可以实现复杂的Excel处理功能。

缺点:

  1. 学习曲线较陡:对于初学者来说,学习成本可能较高。
  2. 性能相对较低:在处理大量数据时,性能可能受到一定影响。

2、EasyPoi

easypoi功能如同名字easy,主打的功能就是容易,让一个没见接触过poi的人员 就可以方便的写出Excel导出,Excel模板导出,Excel导入,Word模板导出,通过简单的注解和模板 语言(熟悉的表达式语法),完成以前复杂的写法

官网:
https://gitee.com/lemur/easypoi

优点:

  1. 简单易用:EasyPoi 提供了简洁的 API 接口,使得 Excel 操作更加便捷。
  2. 功能丰富:支持 Excel 文件的导入导出、模板导出、校验等多种功能。
  3. 易于扩展:EasyPoi 基于 Apache POI 和 JexcelApi,可以方便地扩展和定制功能。
  4. 文档齐全:EasyPoi 提供了详细的文档和示例,便于开发人员学习和使用。

缺点:

  1. 功能有限:相比于 Apache POI,EasyPoi 可能在一些高级功能上有所限制。
  2. 可能存在性能问题:由于封装层的存在,EasyPoi 在处理大量数据时可能存在一定的性能损耗。

3、EasyExcel

EasyExcel是一个阿里巴巴基于Apache POI封装的开源框架,专注于Excel文件的读写操作。它提供了简洁易用的API,简化了Excel处理的流程。

官网

https://easyexcel.opensource.alibaba.com/

优点:

  1. 简单易用:提供了简洁的API,使用起来非常方便。
  2. 高性能:在处理大量数据时具有较高的性能,能够快速导入导出Excel文件。
  3. 支持注解:支持使用注解配置Excel文件的导入导出规则,简化了开发过程。

缺点:

  1. 功能相对有限:相比Apache POI,功能相对简单,可能无法满足某些复杂的Excel处理需求。
  2. 定制化能力较弱:定制化能力不如Apache POI灵活。

POI和EasyExcel区别

三 、各版本Excel 区别

不同版本的Excel在功能和格式上可能会有一些差异。所以后续在处理不同版本的excel时,会有少许不同

以下是一些常见的Excel版本之间的区别

1、Excel 97-2003(.xls)

  • 最大行数为65536行,最大列数为256列。
  • 支持的最大单元格格式有限。
  • 不支持新的Excel特性,如条件格式、表格样式等。
  • 文件大小限制为2GB。

2、Excel 2007及以上版本(.xlsx)

  • 最大行数和列数均有较大提升,支持数百万行数和16384列。
  • 支持更多的单元格格式和样式。
  • 引入了新的功能,如条件格式、表格样式、数据透视表等。
  • 支持更多的图表类型和图表样式。
  • 文件大小限制较大,最多可达16,384 x 1,048,576个单元格。

四 、Excel 基本结构介绍

Java是面向对象的操作语言,万物皆对象。了解了Excel基本结构有助于我们将Excel与Java中对象关联起来

  • 工作簿(Workbook):
    Excel文件以工作簿的形式存在,一个工作簿可以包含多个工作表(Sheet)
  • 工作表(Sheet):
    每个工作表由行(Row)和列(Column)组成,交叉形成单元格(Cell),用于存储数据、文本、公式等内容
  • 单元格(Cell):
    Excel中的最小单位,用于存储数据或公式。每个单元格有一个唯一的地址,例如A1、B2等
  • 行(Row)和列(Column):
    行是水平方向的一组单元格,列是垂直方向的一组单元格。行用数字标识,列用字母标识
  • 公式(Formula):
    Excel支持使用公式进行计算和数据处理。公式以等号(=)开头,可以引用其他单元格的数值或内容进行运算
  • 函数(Function):
    Excel提供了大量的内置函数,用于进行各种复杂的计算和数据处理,如SUM(求和)、AVERAGE(平均值)、VLOOKUP(垂直查找)

Excel基本结构

五、Apache POI基本操作

由于Excel分为03版本和07版本,所以我们在使用的时候需要

注:

  • 处理03版本excel时,主要使用
    HSSFWorkbook

    HSSFSheet

    HSSFRow

    HSSFCell
    等对象来操作Excel文件;

  • 处理07版本及之后版本excel时,主要使用
    XSSFWorkbook

    XSSFSheet

    XSSFRow

    XSSFCell
    等对象来操作Excel文件

其他操作基本是一样的,了解这个之后,后续操作就很简单了~

5.1 基本写操作

5.1.1 03版本写excel

① 引入依赖

<!--poi 03版本依赖-->
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
    <version>4.1.2</version>
</dependency>
<!--单元测试-->
<dependency>
     <groupId>junit</groupId>
     <artifactId>junit</artifactId>
     <version>4.13.2</version>
     <scope>test</scope>
</dependency>
<!--日期-->
<dependency>
    <groupId>joda-time</groupId>
    <artifactId>joda-time</artifactId>
    <version>2.10.10</version>
</dependency>

②实例代码

public class AppTest
{
    String filepath="E:\\xiezhrspace\\excel-demo\\fileoutput\\";
    @Test
    public void poiexcel03Test() throws  Exception{
        //1、创建一个工作簿
        Workbook workbook = new HSSFWorkbook();
        //2、创建一个工作表
        Sheet sheet = workbook.createSheet("第一个工作表");

        //3、创建一行

        //3.1 创建第一行
        Row row1 = sheet.createRow(0);
        //3.2 创建第二行
        Row row2 = sheet.createRow(1);
        //3.3 创建第三行
        Row row3 = sheet.createRow(2);

        //4 创建一个单元格
        //4.1 创建第一行第一个单元格
        Cell cell11 = row1.createCell(0);
        //4.2 创建第一行第二个单元格
        Cell cell12 = row1.createCell(1);
        //4.3 创建第二行第一个单元格
        Cell cell21 = row2.createCell(0);
        //4.4 创建第二行第二个单元格
        Cell cell22 = row2.createCell(1);
        //4.5 创建第三行第一个单元格
        Cell cell31 = row3.createCell(0);
        //4.6 创建第三行第二个单元格
        Cell cell32 = row3.createCell(1);

        // 5 设置单元格的值
        //5.1 设置第一行第一个单元格
        cell11.setCellValue("个人公众号");
        //5.2 设置第一行第二个单元格
        cell12.setCellValue("XiezhrSpace");

        //5.3 设置第二行第一个单元格
        cell21.setCellValue("个人博客");
        //5.4 设置第二行第二个单元格
        cell22.setCellValue("www.xiezhr.cn");

        //5.5 设置第三行第一个单元格
        cell31.setCellValue("当前时间");
        //5.6 设置第三行第二个单元格
        String curdate = new DateTime().toString("yyyy-MM-dd HH:mm:ss");
        cell32.setCellValue(curdate);


        FileOutputStream fileOutputStream = new FileOutputStream(filepath + "poiexcel03.xls");
        workbook.write(fileOutputStream);
        fileOutputStream.close();
        workbook.close();

    }

}

③ excel写出效果

image-20240303162520227

5.1.2 07版本写excel

07 版本依赖与处理03版本的有点不一样,代码基本上不变

① 所需依赖

<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>4.1.2</version>
</dependency>

② 代码修改

07版本操作与03版本操作基本没什么变化,我们只需将03版本代码中
new HSSFWorkbook()
修改成
new XSSFWorkbook()

new FileOutputStream(filepath + "poiexcel03.xls")
修改成
new FileOutputStream(filepath + "poiexcel07.xlsx")
即可

 Workbook workbook = new XSSFWorkbook();
 ...省略
 FileOutputStream fileOutputStream = new FileOutputStream(filepath + "poiexcel07.xlsx");

③ 最终效果

07版本操作

5.1.3 03版本批量写excel

①代码

@Test
    public void testBigDateExcelTest() throws Exception {
        Workbook workbook = new HSSFWorkbook();
        Sheet sheet = workbook.createSheet("大文件导出测试");

        long begin = System.currentTimeMillis();

        for (int rowNum = 0; rowNum <65536 ; rowNum++) {
            Row row = sheet.createRow(rowNum);
            for (int cellNum = 0; cellNum < 10 ; cellNum++) {
                Cell cell = row.createCell(cellNum);
                cell.setCellValue("("+(rowNum+1) + "," + (cellNum+1)+")");
            }

        }
        FileOutputStream fileOutputStream = new FileOutputStream(filepath + "03版本批量写入.xls");
        workbook.write(fileOutputStream);
        fileOutputStream.close();
        workbook.close();

        long end = System.currentTimeMillis();
        System.out.println("耗时:"+(double)(end-begin)/1000+"秒");
    }

②最终效果

根据记录时间,耗时:1.663秒

03版本批量写入

5.1.4 07版本批量写excel

① 代码修改

我们只需将上面
new HSSFWorkbook()
修改成
new XSSFWorkbook()

new FileOutputStream(filepath + "03版本批量写入.xls")
修改成
new FileOutputStream(filepath + "07版本批量写入.xlsx")
即可

Workbook workbook = new XSSFWorkbook();
 ...省略
FileOutputStream fileOutputStream = new FileOutputStream(filepath + "07版本批量写入.xlsx");

② 最终效果

由于07及以上版本,没有限制行数,所以在写入数据时耗时相比较长。共耗时:10.959秒

07版本数据批量写入


注意:
如果03版本写入数据行数超过65536行会报如下错误,而07版本的不会报错

65536超出报错

5.1.5 07版本批量写入优化

通过上面的列子,我们可以看出来在07版本中批量写入大数据的时候耗时比较长,这小节,我们就使用apach提供新的类来优化代码

① 代码

代码基本不用变,我们只需要将
new XSSFWorkbook()
修改为
new SXSSFWorkbook()

并且最终将数据写入过程中产生的缓存文件删除
((SXSSFWorkbook) workbook).dispose();

 @Test
    public void batchWriteExcel07optTest() throws Exception {
        Workbook workbook = new SXSSFWorkbook();
        Sheet sheet = workbook.createSheet("大文件导出优化测试");

        long begin = System.currentTimeMillis();

        for (int rowNum = 0; rowNum <65536 ; rowNum++) {
            Row row = sheet.createRow(rowNum);
            for (int cellNum = 0; cellNum < 10 ; cellNum++) {
                Cell cell = row.createCell(cellNum);
                cell.setCellValue("("+(rowNum+1) + "," + (cellNum+1)+")");
            }

        }
        FileOutputStream fileOutputStream = new FileOutputStream(filepath + "07版本批量写入优化.xlsx");
        workbook.write(fileOutputStream);
        // 清理临时文件
        ((SXSSFWorkbook) workbook).dispose();
        fileOutputStream.close();
        workbook.close();

        long end = System.currentTimeMillis();
        System.out.println("耗时:"+(double)(end-begin)/1000+"秒");
    }

② 最终效果

同样的数据大小,耗时明显减少了

07批量写入优化

5.2 基本读操作

5.2.1 基本读excel

① 03版本读取

String filePath = "E:\\xiezhrspace\\excel-demo\\fileinput\\";

@Test
public void readExcel03Test() throws  Exception{

    FileInputStream fileInputStream = new FileInputStream(filePath + "poiexcel03.xls");

    Workbook workbook = new HSSFWorkbook(fileInputStream);
    Sheet sheet = workbook.getSheetAt(0);
    //获取第一行
    Row row1 = sheet.getRow(0);
    //获取第一行的第一个单元格
    Cell cell11 = row1.getCell(0);
    //获取第一行第二个单元格
    Cell cell12 = row1.getCell(1);
    System.out.println("第一行第一个单元格的内容是:"+cell11.getStringCellValue());
    System.out.println("第一行第二个单元格的内容是:"+cell12.getStringCellValue());

    //获取第一行
    Row row2 = sheet.getRow(1);
    //获取第一行的第一个单元格
    Cell cell21 = row2.getCell(0);
    //获取第一行第二个单元格
    Cell cell22 = row2.getCell(1);
    System.out.println("第一行第一个单元格的内容是:"+cell21.getStringCellValue());
    System.out.println("第一行第二个单元格的内容是:"+cell22.getStringCellValue());

}
//结果
第一行第一个单元格的内容是:个人公众号
第一行第二个单元格的内容是:XiezhrSpace
第一行第一个单元格的内容是:个人博客
第一行第二个单元格的内容是:www.xiezhrspace.cn

②07版本读取

只需将
new FileInputStream(filePath + "poiexcel03.xls")
修改为
new FileInputStream(filePath + "poiexcel07.xlsx")

new HSSFWorkbook(fileInputStream)
修改为
new XSSFWorkbook(fileInputStream)

String filePath = "E:\\xiezhrspace\\excel-demo\\fileinput\\";
@Test
    public void readExcel07Test() throws  Exception{

        FileInputStream fileInputStream = new FileInputStream(filePath + "poiexcel07.xlsx");

        Workbook workbook = new XSSFWorkbook(fileInputStream);
        Sheet sheet = workbook.getSheetAt(0);
        //获取第一行
        Row row1 = sheet.getRow(0);
        //获取第一行的第一个单元格
        Cell cell11 = row1.getCell(0);
        //获取第一行第二个单元格
        Cell cell12 = row1.getCell(1);
        System.out.println("第一行第一个单元格的内容是:"+cell11.getStringCellValue());
        System.out.println("第一行第二个单元格的内容是:"+cell12.getStringCellValue());

        //获取第一行
        Row row2 = sheet.getRow(1);
        //获取第一行的第一个单元格
        Cell cell21 = row2.getCell(0);
        //获取第一行第二个单元格
        Cell cell22 = row2.getCell(1);
        System.out.println("第一行第一个单元格的内容是:"+cell21.getStringCellValue());
        System.out.println("第一行第二个单元格的内容是:"+cell22.getStringCellValue());

    }
//结果
第一行第一个单元格的内容是:个人公众号
第一行第二个单元格的内容是:XiezhrSpace
第一行第一个单元格的内容是:个人博客
第一行第二个单元格的内容是:www.xiezhr.cn

注:

如果上面获取单元格数据的时候,取值的类型不对,即
String
类型的数据通过
cell22.getNumericCellValue()
获取,则会报错

其他类型不匹配情况类似

类型不匹配报错

5.2.2 读取不同类型数据

① 准备数据

现在有一张用户订单信息表,表内容如下,表中包含了各种类型的数据

用户订单信息表

②书写代码,将表格中的内容读取出来输出到控制台

  @Test
    public void readExcel07ByTypeTest() throws Exception{
        FileInputStream fileInputStream = new FileInputStream(filePath + "用户订单信息表.xlsx");

        //根据文件输入流获取excel工作簿(Workbook)
        Workbook workbook = new XSSFWorkbook(fileInputStream);

        //根据工作簿获取第一个sheet
        Sheet sheet = workbook.getSheetAt(0);
        //获取表头信息
        Row rowtitle = sheet.getRow(0);
        //获取表头有多少列
        int cells = rowtitle.getPhysicalNumberOfCells();

        for (int cellNum = 0; cellNum < cells; cellNum++) {
            Cell cell = rowtitle.getCell(cellNum);
            String cellValue = cell.getStringCellValue();
            System.out.print(cellValue + "\t");
        }
        //循环读取数据
        //获取行数
        int rows = sheet.getPhysicalNumberOfRows();
        //从第二行开始读取数据
        for (int rowNum = 1; rowNum < rows; rowNum++) {
            //获取行对象
            Row row = sheet.getRow(rowNum);
            int ofCells = row.getPhysicalNumberOfCells();
            //获取列数
            for (int cellNum = 0; cellNum < ofCells; cellNum++) {

                System.out.print("第" + (rowNum + 1) + "行第" + (cellNum + 1) + "列");
                //获取单元格
                Cell cell = row.getCell(cellNum);
                //获取单元格数据类型
                if (cell!=null) {
                    CellType cellType = cell.getCellType();
                    //根据单元格数据类型获取单元格数据
                    String cellvalue ="";
                    switch (cellType) {

                        case STRING : //判断是否是字符串类型
                            System.out.print("【String】");
                            cellvalue = cell.getStringCellValue();
                            break;
                        case NUMERIC:  //判断是否是数字类型
                            if (DateUtil.isCellDateFormatted(cell)) {
                                System.out.print("【date】");
                                Date date = cell.getDateCellValue();
                                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
                                cellvalue = sdf.format(date);

                            } else {
                                System.out.print("【double】");
                                double numericCellValue = cell.getNumericCellValue();
                                cell.setCellType(CellType.STRING);
                                cellvalue = String.valueOf(numericCellValue);

                                }
                            break;
                        case BLANK:
                            System.out.print("【BLANK】");
                            break;
                        case BOOLEAN:
                            System.out.print("【BOOLEAN】");
                            boolean bool = cell.getBooleanCellValue();
                            cellvalue = String.valueOf(bool);
                            break;
                        case _NONE:
                            System.out.print("【_NONE】");
                            break;
                        case  ERROR:
                            System.out.print("【ERROR】");
                            byte errorCode = cell.getErrorCellValue();
                            cellvalue = String.valueOf(errorCode);
                            break;
                        case  FORMULA:
                            System.out.print("【FORMULA】");
                            XSSFFormulaEvaluator formulaEvaluator = new XSSFFormulaEvaluator((XSSFWorkbook) workbook);

                            String formula = cell.getCellFormula();
                            System.out.println("formula:"+formula);
                            CellValue evaluate = formulaEvaluator.evaluate(cell);
                            cellvalue= evaluate.formatAsString();
                            break;
                    }
                    System.out.println(cellvalue);
                }

            }

        }

    }

读取不同类型数据

六、EasyPoi 使用

Easypoi的目标不是替代poi,而是让一个不懂导入导出的快速使用poi完成Excel和word的各种操作,而不是看很多api才可以完成这样工作

Easypoi 为谁而开发?

  • 不太熟悉poi的
  • 不想写太多重复太多的
  • 只是简单的导入导出的
  • 喜欢使用模板的

6.1 所需依赖

<dependency>
    <groupId>cn.afterturn</groupId>
    <artifactId>easypoi-base</artifactId>
    <version>4.1.0</version>
</dependency>
<dependency>
    <groupId>cn.afterturn</groupId>
    <artifactId>easypoi-web</artifactId>
    <version>4.1.0</version>
</dependency>
<dependency>
    <groupId>cn.afterturn</groupId>
    <artifactId>easypoi-annotation</artifactId>
    <version>4.1.0</version>
</dependency>
  • 1.easypoi
    :父包–作用大家都懂得
  • 2.easypoi-annotation
    :基础注解包,作用与实体对象上,拆分后方便maven多工程的依赖管理
  • 3.easypoi-base
    :导入导出的工具包,可以完成Excel导出,导入,Word的导出,Excel的导出功能
  • 4.easypoi-web
    :耦合了spring-mvc 基于AbstractView,极大的简化spring-mvc下的导出功能
  • 5.sax
    :导入使用xercesImpl这个包(这个包可能造成奇怪的问题哈),word导出使用poi-scratchpad,都作为可选包了

如果不使用spring mvc的便捷福利,直接引入easypoi-base 就可以了,easypoi-annotation

6.2 常用注解

EasyPoi 为了方便我们操作,实现了实体和Excel的对应,model--row,filed--col,这样利用注解,我们就可以轻轻松松实现excel的导入导出

  • @Excel
    作用到filed上面,是对Excel一列的一个描述
  • **@ExcelCollection ** 表示一个集合,主要针对一对多的导出,比如一个老师对应多个科目,科目就可以用集合表示
  • @ExcelEntity
    表示一个继续深入导出的实体,但他没有太多的实际意义,只是告诉系统这个对象里面同样有导出的字段
  • @ExcelIgnore
    和名字一样表示这个字段被忽略跳过这个导导出
  • @ExcelTarget
    这个是作用于最外层的对象,描述这个对象的id,以便支持一个对象可以针对不同导出做出不同处理
6.2.1 @ExcelTarget

限定一个到处实体的注解,以及一些通用设置,作用于最外面的实体

①常用属性

属性 类型 默认值 功能
value String null 定义id唯一标识,不能重复
height double 10 定义单元格高度
fontSize short 11 设置文字大小

②使用

@ExcelTarget("users")
public class User implements Serializable {
 	//..... 省略属性 相关GET,SET方法
}
6.2.2 @Excel

@Excel这个是必须使用的注解,用在filed(属性)上面,是对Excel一列的一个描述

① 常用属性

属性 类型 默认值 功能
name String null 生成Excel表格中列名
needMerge boolean fasle 是否需要纵向合并单元格(用于含有list中,单个的单元格,合并list创建的多个row)
orderNum String "0" 指定生成Excel中列的顺序,按照数字自然顺序排序
replace String[] {} 值得替换 导出是{a_id,b_id} 导入反过来
savePath String “upload” 指定导入Excel中图片的保存路径
type int 1 导出类型 1 是文本 2 是图片,3 是函数,10 是数字 默认是文本
width double 10 指定导出Excel时列的宽度
isImportField boolean true 校验字段,看看这个字段是不是导入的Excel中有,如果没有说明是错误的Excel,读取失败,
exportFormat String "" 导出Excel的时间格式
importFormat String "" 导入Excel的时间格式
format String "" 时间格式,相当于同时设置了exportFormat 和 importFormat
imageType int 1 导出类型 1 从file读取 2 是从数据库中读取 默认是文件 同样导入也是一样的
suffix String "" 文字后缀,如% 90 变成90%
isWrap boolean true 是否换行 即支持\n
mergeVertical boolean fasle 纵向合并内容相同的单元格

②使用

@ExcelTarget("user")
public class User {
    @Excel(name = "姓名",orderNum = "1",width = 20 )
    private String name;
    @Excel(name = "年龄",orderNum = "2",width = 20 )
    private  Integer age;
    @Excel(name = "性别",orderNum = "3",width = 20,replace = {"男_1","女_2"})
    private String sex;
    @Excel(name = "生日",orderNum = "4",width = 20,format = "yyyy-MM-dd")
    private Date birthday;
    //...省略GET、SET方法
}
7.2.3 @ExcelEntity

来标记实体类中包含另一个实体类的字段的注解。通过在父实体类的字段上添加
@ExcelEntity
注解,可以将子实体类中的字段映射到 Excel 表格中

① 常用属性

属性 类型 默认值 功能
id String null 定义ID

② 使用

@ExcelTarget("user")
public class User {
    //省略GET、SET方法和其他属性
    @ExcelEntity(name = "用户基本信息")
    private UserBaseInfo userBaseInfo;

}

@ExcelTarget("userBaseInfo")
class UserBaseInfo{
    @Excel(name = "手机号",orderNum = "5",width = 20)
    private String phone;
    @Excel(name = "邮箱",orderNum = "6",width = 20)
	//省略其他字段和方法
}

在上面的示例中,
User
类中包含一个
UserBaseInfo
类型的字段
userBaseInfo
,通过在
userBaseInfo
字段上添加
@ExcelEntity
注解,可以将
UserBaseInfo
类中的字段映射到 Excel 表格中,并且在 Excel 表格中会显示为一个包含
手机号

邮箱
列的子表格。

6.2.4 @ExcelCollection

一对多的集合注解,来标记实体类中包含集合类型字段的注解。

通过在父实体类的集合字段上添加
@ExcelCollection
注解,可以将集合中的元素映射到 Excel 表格中

①常用属性

属性 类型 默认值 功能
id String null 定义ID
name String null 定义集合列名,支持nanm_id
orderNum int 0 排序,支持name_id
type Class<?> ArrayList.class 导入时创建对象使用

② 使用

@ExcelTarget("user")
public class User {
   //省略GET、SET 方法和其他属性
    @ExcelCollection(name = "商品列表",orderNum = "10")
    private List<Product> products;

}
@ExcelTarget("product")
public class Product {
    @Excel(name = "商品名称" )
    private String productName;
    @Excel(name = "商品价格")
    private double productPrice;
}

在上面的示例中,
User
类中包含一个
List<Product>
类型的字段
products
,通过在
products
字段上添加
@ExcelCollection
注解,可以将
Product
类型的元素映射到 Excel 表格中,并且在 Excel 表格中会显示为一个包含多个商品信息的列表。

6.2.5 @ExcelIgnore

来标记实体类中不需要导出到 Excel 表格的字段的注解

① 使用

public class Product {
    @Excel(name = "商品名称")
    private String name;
    
    @Excel(name = "价格")
    private double price;
    
    @ExcelIgnore
    private String description; // 不需要导出到 Excel 表格的字段
    
    // 其他字段和方法
}

在上面的示例中,
Product
类中包含了
name

price

description
三个字段。通过在
description
字段上添加
@ExcelIgnore
注解,告诉 EasyPoi 在导出 Excel 表格时不导出该字段的内容。

6.3 注解使用导出Excel案例

6.3.1 导出基本数据

① 创建基本对象对象

@Data
@ExcelTarget("user")
public class User {
    @Excel(name = "姓名",orderNum = "1",width = 20,needMerge = true)
    private String name;
    @Excel(name = "年龄",orderNum = "2",width = 20,needMerge = true)
    private  Integer age;
    @Excel(name = "性别",orderNum = "3",width = 20,replace = {"男_1","女_2"},needMerge = true)
    private String sex;
    @Excel(name = "生日",orderNum = "4",width = 20,format = "yyyy-MM-dd",needMerge = true)
    private Date birthday;

}

②模拟测试数据

public List<User> testGetTestData(){
        List<User> users = new ArrayList<User>();
        for (int i = 0; i < 5; i++) {
            User user = new User();
            user.setName("小凡"+i);
            user.setAge(i+18);
            user.setSex("1");
            user.setBirthday(new Date());
            users.add(user);
        }
        return users;
    }

③导出数据

@Test
public void testExportExcel() throws Exception {
    ExportParams exportParams = new ExportParams("用户列表", "测试sheet页");
    //创建ExcelExportUtil对象,传入参数为ExportParams对象,User类,testGetTestData()方法
    Workbook workbook = ExcelExportUtil.exportExcel(exportParams, User.class, testGetTestData());
    //创建FileOutputStream对象,传入参数为文件路径
    FileOutputStream fileOutputStream = new FileOutputStream("D:/test.xls");
    //将workbook写入到文件输出流中
    workbook.write(fileOutputStream);
    //关闭文件输出流
    fileOutputStream.close();
    //关闭workbook
    workbook.close();
    System.out.println("导出成功");
}

④ 查看excel

导出基本excel

6.3.2 忽略某个字段

① 添加注解

@ExcelIgnore
private  Integer age;

②查看excel

忽略某个字段

6.3.3 导出对象中含有对象的数据

① 添加注解

@ExcelTarget("user")
public class User {
    //省略GET、SET方法和其他属性
    @ExcelEntity(name = "用户基本信息")
    private UserBaseInfo userBaseInfo;
}

② 新建UserBaseInfo类

@ExcelTarget("userBaseInfo")
class UserBaseInfo{
    @Excel(name = "手机号",orderNum = "5",width = 20)
    private String phone;
    @Excel(name = "邮箱",orderNum = "6",width = 20)
	//省略其他字段和方法
}

③ 测试数据中添加
UserBaseInfo
对象

user.setUserBaseInfo(new UserBaseInfo("15288345678","324355@qq.com","云南省昆明市","651219","532334125689558"));

④ 导出excel数据

对象中包含对象

6.3.4 导出含list集合数据

①我们新建
Product
对象

@ExcelTarget("product")
public class Product {
    //省略GET、SET方法
    @Excel(name = "商品名称" )
    private String productName;
    @Excel(name = "商品价格")
    private double productPrice;
}


User
类中添加如下list集合

@ExcelCollection(name = "商品列表",orderNum = "10")
private List<Product> products;

③添加测试数据

public List<User> testGetTestData(){
    List<User> users = new ArrayList<User>();
    for (int i = 0; i < 5; i++) {
        User user = new User();
        user.setName("小凡"+i);
        user.setAge(i+18);
        user.setSex("1");
        user.setBirthday(new Date());
        user.setUserBaseInfo(new UserBaseInfo("15288345678","324355@qq.com","云南省昆明市","651219","532334125689558"));
        Product pro1 = new Product("冰箱"+i, 5000);
        Product pro2 = new Product("洗衣机"+i, 3000);
        Product pro3 = new Product("空调"+i, 4000);
        List<Product> products = Arrays.asList(pro1, pro2, pro3);
        user.setProducts(products);
        users.add(user);
    }
    return users;
}

④ 由于是一对多关系,所以需要添加合并单元格属性
needMerge = true

@Excel(name = "姓名",orderNum = "1",width = 20,needMerge = true)
private String name;
@Excel(name = "年龄",orderNum = "2",width = 20,needMerge = true)
//其他属性类似省略。。。

⑤导出excel

导出list

6.3.5 导出图片


User
类中定义头像
avter
字段

@Excel(name = "头像",orderNum = "0",width = 20,type = 2,height = 20,needMerge = true) //type的值一定要指定为2
private String avatar; //定义头像 直接写指定图片路径

② 构造测试数据

    public List<User> testGetTestData(){
        List<User> users = new ArrayList<User>();
        for (int i = 0; i < 5; i++) {
            User user = new User();
            user.setName("小凡"+i);
            user.setAge(i+18);
            user.setSex("1");
            user.setBirthday(new Date());
            user.setUserBaseInfo(new UserBaseInfo("15288345678","324355@qq.com","云南省昆明市","651219","532334125689558"));
            Product pro1 = new Product("冰箱"+i, 5000);
            Product pro2 = new Product("洗衣机"+i, 3000);
            Product pro3 = new Product("空调"+i, 4000);
            List<Product> products = Arrays.asList(pro1, pro2, pro3);
            user.setAvatar("https://xiezhrspace.cn/medias/logo.png");
            user.setProducts(products);
            users.add(user);
        }
        return users;
    }

③ 导出excel

导出图片

6.4 大数据导出

当我们导出得数据量为几十万到几百万时,一次将所有数据加载到内存中,会对cpu、内存都产生巨大压力。

当然了,EasyPoi作者也为我们考虑了,专门提供
exportBigExcel
方法来导出大数据

Workbook workbook1 = ExcelExportUtil.exportBigExcel(new ExportParams("用户列表", "测试"), User.class, getUsers());
workbook1.write(outputStream);
ExcelExportUtil.closeExportBigExcel();

6.5 导入excel

6.5.1 导入基本数据

① 准备目标excel

目标excel

② 定义基本数据对象

@ExcelTarget("student")
public class Student implements Serializable {
    @Excel(name = "编号")
    private String id;
    @Excel(name = "姓名")
    private String name;
    @Excel(name = "性别", replace = {"男_1", "女_2"})
    private String gender;
    @Excel(name = "年龄")
    private Integer age;
    @Excel(name = "出生日期", importFormat = "yyyy-MM-dd")
    private Date birth;
    //省略GET、SET方法
}

③ 导入代码

@Test
public void excelImportTest() throws Exception{
    // 创建导入参数配置
    ImportParams importParams = new ImportParams();
    // 设置标题行数
    importParams.setTitleRows(1);
    // 设置表头行数
    importParams.setHeadRows(1);

    // 创建文件输入流
    FileInputStream fileInputStream = new FileInputStream("E:\\xiezhrspace\\excel-demo\\fileinput\\学生信息表.xlsx");
    // 调用ExcelImportUtil的importExcel方法,传入文件输入流,实体类,参数配置,返回实体类列表
    List<Student> students = ExcelImportUtil.importExcel(fileInputStream, Student.class, importParams);
    // 遍历实体类列表,输出实体类信息
    students.forEach(System.out::println);
    // 关闭文件输入流
    fileInputStream.close();

}

② 打印导入结果

Student(id=10001, name=张三, gender=1, age=25, birth=1992-02-23)
Student(id=10002, name=李四, gender=1, age=18, birth=2006-01-01)
Student(id=10003, name=王五, gender=1, age=26, birth=1998-01-01)
Student(id=10004, name=赵六, gender=2, age=26, birth=1998-03-01)
6.5.2 导入带图片数据

①excel数据

excel图片数据


Student
类设置图片保存路径

@ExcelTarget("student")
public class Student implements Serializable {
    @Excel(name = "编号")
    private String id;
    @Excel(name = "姓名")
    private String name;
    @Excel(name = "性别", replace = {"男_1", "女_2"})
    private String gender;
    @Excel(name = "年龄")
    private Integer age;
    @Excel(name = "出生日期", importFormat = "yyyy-MM-dd")
    private Date birth;
    @Excel(name = "头像", type = 2,  savePath = "src/main/imgs")  //设置导入图片的保存路径
    private String avatar;
    //省略GET、SET 方法
}

③ 导入excel

关键部分

importParams.setNeedSave(false);

importParams.setSaveUrl("src/main/imgs");

@Test
public void excelImportTest() throws Exception{
    // 创建导入参数配置
    ImportParams importParams = new ImportParams();
    // 设置标题行数
    importParams.setTitleRows(1);
    // 设置表头行数
    importParams.setHeadRows(1);

    // 设置是否需要保存
    importParams.setNeedSave(false);
    // 设置保存路径
    importParams.setSaveUrl("src/main/imgs");

    // 创建文件输入流
    FileInputStream fileInputStream = new FileInputStream("E:\\xiezhrspace\\excel-demo\\fileinput\\学生信息表.xlsx");
    // 调用ExcelImportUtil的importExcel方法,传入文件输入流,实体类,参数配置,返回实体类列表
    List<Student> students = ExcelImportUtil.importExcel(fileInputStream, Student.class, importParams);
    // 遍历实体类列表,输出实体类信息
    students.forEach(System.out::println);
    // 关闭文件输入流
    fileInputStream.close();

}

④ 导入结果

Student(id=10001, name=张三, gender=1, age=25, birth=1992-02-23,avatar=src/main/imgs\pic33945659303.PNG)
Student(id=10002, name=李四, gender=1, age=18, birth=2006-01-01,avatar=src/main/imgs\pic31457305277.JPG)
Student(id=10003, name=王五, gender=1, age=26, birth=1998-01-01,avatar=src/main/imgs\pic71983821334.PNG)
Student(id=10004, name=赵六, gender=2, age=26, birth=1998-03-01,avatar=src/main/imgs\pic41577097054.PNG)

图片保存路径

6.6 小结

以上小节为easypoi的最基本操作,如果大家还想了解更多,可以关注官方文档:
http://doc.wupaas.com/docs/easypoi/

作者也给出了大量的实例:
https://gitee.com/lemur/easypoi-test

八、EasyExcel使用

8.1 快速入门之写excel

8.1.1 引入依赖

目前最新的依赖是3.3.3,我们这里引入使用最多也最稳定的3.3.2版本

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <version>3.3.2</version>
</dependency>
8.1.2 构建数据对象
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Employee {
    @ExcelProperty("员工编号")
    private  Integer id;
    @ExcelProperty("员工姓名")
    private  String name;
    @ExcelProperty("员工年龄")
    private  Integer age;
    @ExcelProperty("员工薪资")
    private  Double salary;
    @ExcelProperty("入职日期")
    private Date inDate;
}
8.1.3 构建测试数据
private List<Employee> data(long count) {
    List<Employee> list = ListUtils.newArrayList();
    for (int i = 0; i < count; i++) {
        Employee employee = new Employee();
        employee.setId(i);
        employee.setName("小陈"+i);
        employee.setAge(18+1);
        employee.setSalary(6.66+i);
        employee.setInDate(new Date());
        list.add(employee);
    }
    return list;
}

上面代码根据传入count,构建通用数据,后面不会重复写

8.1.4 写入数据
@Test
public void testbaseWrite() {
    String fileName = "E:\\xiezhrspace\\excel-demo\\fileoutput\\" + "simpleWrite" + System.currentTimeMillis() + ".xlsx";
    // 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
    // 如果这里想使用03 则 传入excelType参数即可
    EasyExcel.write(fileName, Employee.class).sheet("测试模板").doWrite(data(10));

}

写入的数据

8.2 快速入门之读excel

8.2.1 准备目标excel

员工信息表

8.2.2 构建数据对象

这里我们沿用8.1.2 小节中创建的employee对象

8.2.3 读取数据

注:
PageReadListener 为默认easyexcel为我们写好的监听类,当然了,我们也可以自己定义,在后面的进阶操作中

我们会说到

@Test
public void testbaseReade(){
    String fileName = "E:\\xiezhrspace\\excel-demo\\fileinput\\" + "员工信息表.xlsx";
    EasyExcel.read(fileName, Employee.class, new PageReadListener<Employee>(dataList -> {
        for (Employee employee : dataList) {
            System.out.println(employee);
        }
    })).sheet().doRead();
}

读取数据

8.3 进阶操作

8.3.1 复杂表头写入

① 准备对象

@Data
public class Student {
    @ExcelProperty({"学生信息表","学生ID"})
    private  Integer id;
    @ExcelProperty({"学生信息表","学生姓名"})
    private String name;
    @ExcelProperty({ "学生信息表", "学生年龄" })
    private Integer age;
    @ExcelProperty({ "学生信息表", "生日" })
    private Date birth;
}

②代码

public class UpEasyExcelTest {
    private List<Student> data(long count) {
        List<Student> list = ListUtils.newArrayList();
        for (int i = 0; i < count; i++) {
            Student student = new Student();
            student.setId(i);
            student.setName("小李" + i);
            student.setAge(22+i);
            student.setBirth(new Date());
            list.add(student);
        }
        return list;
    }

    @Test
    public void testUpWrite() {
        String fileName = "E:\\xiezhrspace\\excel-demo\\fileoutput\\" + "upWrite" + System.currentTimeMillis() + ".xlsx";
        // 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
        // 如果这里想使用03 则 传入excelType参数即可
        EasyExcel.write(fileName, Student.class).sheet("测试模板").doWrite(data(10));

    }
}

③excel写入示例

复杂表头

8.3.2 重复多次写入(写到单个或者多个Sheet)

① 准备对象

public class Employee {
    @ExcelProperty("员工编号")
    private  Integer id;
    @ExcelProperty("员工姓名")
    private  String name;
    @ExcelProperty("员工年龄")
    private  Integer age;
    @ExcelProperty("员工薪资")
    private  Double salary;
    @ExcelProperty(value = "入职日期")
    private Date inDate;
    //省略GET、SET 方法
}

②代码

 @Test
public void testmanyDateWriter(){
    // 方法1: 如果写到同一个sheet
    String fileName = "E:\\xiezhrspace\\excel-demo\\fileoutput\\" + "manydataWrite" + System.currentTimeMillis() + ".xlsx";
    // 这里 需要指定写用哪个class去写
    try (ExcelWriter excelWriter = EasyExcel.write(fileName, Employee.class).build()) {
        // 这里注意 如果同一个sheet只要创建一次
        WriteSheet writeSheet = EasyExcel.writerSheet("模板").build();
        // 去调用写入,这里我调用了五次,实际使用时根据数据库分页的总的页数来
        for (int i = 0; i < 5; i++) {
            // 分页去数据库查询数据 这里可以去数据库查询每一页的数据
            List<Employee> data = data(200000);
            excelWriter.write(data, writeSheet);
        }
    }

    // 方法2: 如果写到不同的sheet 同一个对象
    fileName =  "E:\\xiezhrspace\\excel-demo\\fileoutput\\" + "manydataWrite" + System.currentTimeMillis() + ".xlsx";
    // 这里 指定文件
    try (ExcelWriter excelWriter = EasyExcel.write(fileName, Employee.class).build()) {
        // 去调用写入,这里我调用了五次,实际使用时根据数据库分页的总的页数来。这里最终会写到5个sheet里面
        for (int i = 0; i < 5; i++) {
            // 每次都要创建writeSheet 这里注意必须指定sheetNo 而且sheetName必须不一样
            WriteSheet writeSheet = EasyExcel.writerSheet(i, "模板" + i).build();
            // 分页去数据库查询数据 这里可以去数据库查询每一页的数据
            List<Employee> data = data(200000);
            excelWriter.write(data, writeSheet);
        }
    }

    // 方法3 如果写到不同的sheet 不同的对象
    fileName =  "E:\\xiezhrspace\\excel-demo\\fileoutput\\" + "manydataWrite" + System.currentTimeMillis() + ".xlsx";
    // 这里 指定文件
    try (ExcelWriter excelWriter = EasyExcel.write(fileName).build()) {
        // 去调用写入,这里我调用了五次,实际使用时根据数据库分页的总的页数来。这里最终会写到5个sheet里面
        for (int i = 0; i < 5; i++) {
            // 每次都要创建writeSheet 这里注意必须指定sheetNo 而且sheetName必须不一样。这里注意DemoData.class 可以每次都变,我这里为了方便 所以用的同一个class
            // 实际上可以一直变
            WriteSheet writeSheet = EasyExcel.writerSheet(i, "模板" + i).head(Employee.class).build();
            // 分页去数据库查询数据 这里可以去数据库查询每一页的数据
            List<Employee> data = data(200000);
            excelWriter.write(data, writeSheet);
        }
    }
}

③Excel示例

100万数据写入

除了上面的常用例子外,还有更多玩法,大家可以参照官方文档,里面写的很详细,这里就不一一例举了~

8.4 excel填充

EasyExcel提供了数据填充的功能,可以将指定的数据填充到事先设计好带有样式和格式的Excel模板文,这样我们就可以制作出更加优美的excel

8.4.1 简单的填充

① 准备模板

image-20240310172107654

②准备对象

@Data
public class Student {
    @ExcelProperty({"学生信息表","学生ID"})
    private  Integer id;
    @ExcelProperty({"学生信息表","学生姓名"})
    private String name;
    @ExcelProperty({ "学生信息表", "学生年龄" })
    private Integer age;
    @ExcelProperty({ "学生信息表", "生日" })
    private Date birth;
}

③填充数据

根据对象填充

@Test
public void filebyObjFillTest() {
    // 模板注意 用{} 来表示你要用的变量 如果本来就有"{","}" 特殊字符 用"\{","\}"代替
    String templateFileName ="E:\\xiezhrspace\\excel-demo\\fileinput\\" +  "template.xlsx";

    // 方案1 根据对象填充
    String fileName = "E:\\xiezhrspace\\excel-demo\\fileoutput\\" + "fillWrite" + System.currentTimeMillis() + ".xlsx";

    Student student = new Student();
    student.setId(1);
    student.setName("张三");
    student.setAge(20);
    student.setBirth(new Date());
    // 这里 会填充到第一个sheet, 然后文件流会自动关闭

    EasyExcel.write(fileName).withTemplate(templateFileName).sheet().doFill(student);

}

根据map填充

@Test
public void filebyMapFillTest() {
    // 模板注意 用{} 来表示你要用的变量 如果本来就有"{","}" 特殊字符 用"\{","\}"代替
    String templateFileName ="E:\\xiezhrspace\\excel-demo\\fileinput\\" +  "template.xlsx";
    // 方案2 根据Map填充
    String fileName = "E:\\xiezhrspace\\excel-demo\\fileoutput\\" + "fillWrite" + System.currentTimeMillis() + ".xlsx";

    // 这里 会填充到第一个sheet, 然后文件流会自动关闭
    Map<String, Object> map = MapUtils.newHashMap();
    map.put("id", "001");
    map.put("name", "张三");
    map.put("age", 20);
    map.put("birthday", new Date());
    EasyExcel.write(fileName).withTemplate(templateFileName).sheet().doFill(map);
}

④最终效果

最终效果

8.4.2 填充列表数据

①准备模板

相比上一小节的模板,列表数据的模板
{id}----->{.id}
{name}--->{.name}
依此类推

列表模板

②填充对象

参照上一小节

③ 准备数据

private List<Student> data(long count) {
        List<Student> list = ListUtils.newArrayList();
        for (int i = 0; i < count; i++) {
            Student student = new Student();
            student.setId(i);
            student.setName("小李" + i);
            student.setAge(22+i);
            student.setBirth(new Date());
            list.add(student);
        }
        return list;
    }

④ 填充数据

@Test
public void listFill() {
    // 模板注意 用{} 来表示你要用的变量 如果本来就有"{","}" 特殊字符 用"\{","\}"代替
    // 填充list 的时候还要注意 模板中{.} 多了个点 表示list
    // 如果填充list的对象是map,必须包涵所有list的key,哪怕数据为null,必须使用map.put(key,null)
    String templateFileName ="E:\\xiezhrspace\\excel-demo\\fileinput\\" +  "template-list.xlsx";

    // 方案1 一下子全部放到内存里面 并填充
    String fileName = "E:\\xiezhrspace\\excel-demo\\fileoutput\\" + "fillWrite-list" + System.currentTimeMillis() + ".xlsx";
    // 这里 会填充到第一个sheet, 然后文件流会自动关闭
    EasyExcel.write(fileName).withTemplate(templateFileName).sheet().doFill(data(10));

}

⑤ 最终效果

最终效果

8.4.3 复杂填充

当excel模板相对复杂时,也可以填充

① 准备复杂模板

复杂模板

② 准备填充对象

参照8.4.1小节

③准备数据

参照8.4.1小节

④填充数据

@Test
public void complexFill() {
    // 模板注意 用{} 来表示你要用的变量 如果本来就有"{","}" 特殊字符 用"\{","\}"代替
    // {} 代表普通变量 {.} 代表是list的变量
    String templateFileName ="E:\\xiezhrspace\\excel-demo\\fileinput\\" +  "template-hard.xlsx";

    String fileName = "E:\\xiezhrspace\\excel-demo\\fileoutput\\" + "fillWrite-hard" + System.currentTimeMillis() + ".xlsx";
    // 方案1
    try (ExcelWriter excelWriter = EasyExcel.write(fileName).withTemplate(templateFileName).build()) {
        WriteSheet writeSheet = EasyExcel.writerSheet().build();
        // 这里注意 入参用了forceNewRow 代表在写入list的时候不管list下面有没有空行 都会创建一行,然后下面的数据往后移动。默认 是false,会直接使用下一行,如果没有则创建。
        // forceNewRow 如果设置了true,有个缺点 就是他会把所有的数据都放到内存了,所以慎用
        // 简单的说 如果你的模板有list,且list不是最后一行,下面还有数据需要填充 就必须设置 forceNewRow=true 但是这个就会把所有数据放到内存 会很耗内存
        // 如果数据量大 list不是最后一行 参照下一个
        FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build();
        excelWriter.fill(data(5), fillConfig, writeSheet);
        excelWriter.fill(data(5), fillConfig, writeSheet);
        Map<String, Object> map = MapUtils.newHashMap();
        map.put("date", "2024年03月10日17:28:28");
        map.put("total", 1000);
        excelWriter.fill(map, writeSheet);
    }
}

⑤ 最终效果

学生信息统计表

九、文章小节

文中只例举出excel 常用操作,如果这些还不满足你的需求,大家可以查看官方文档。

官方文档还是非常详细的,并且都给出了具体的demo。

另外文中例举出的代码已提交到https://gitee.com/xiezhr/excel-demo.git 欢迎大家访问查看

本期内容到此就结束了,希望对你有所帮助,我们下期再见 (●'◡'●)

同步和异步调用的本质区别是什么?

引言

现在异步编程真的是越来越普遍了,从前端的Promise到后端的Channel、Future、Task,异步编程正变得越来越流行。很多同学也玩得很溜了,满世界的异步调用,让程序的效率和用户体验都大大提升。不过,当谈到为什么要使用异步编程,以及它背后的工作原理时,大部分同学就哑火了。对于一个有追求的程序员来说,我们不仅要会用,更要理解其中的原理,所谓“知其所以然”。

而且异步编程并不是银弹,本质上它不会让程序运行的更快,使用它也伴随着复杂的错误处理和调试难题,比如著名的“回调地狱”。因此,了解它的工作原理,以及正确地使用它,对于编写高质量的代码来说特别重要。

本文,我们就来一起探讨下同步和异步调用的本质区别,深入解析异步编程的工作原理,以及介绍如何在实际开发中灵活运用这两种调用方式。

概念

要讨论问题,首先得明确概念,也就是我们到底在说什么。

同步调用
,简单来说,就是执行多个任务的时候,其中一个任务必须完成后,才能开始下一个任务。在这种模式下,任务按照顺序依次执行,每个任务的执行必须等待前一个任务完成,所以大家也称之为阻塞调用。

在编程中,同步调用的一个典型应用场景是数据库事务。比如,在事务中更新一系列的记录时,系统会按照顺序执行这些操作,直到全部完成,期间不会去处理其他任务。这确保了数据的一致性和完整性,但也意味着在事务处理期间,其他依赖于这些数据的操作必须等待。

异步调用
,顾名思义,是一种任务可以在后台执行,而不阻塞当前线程继续执行其他任务的调用方式,这可以使多个任务得以并行处理。

在编程中,异步调用的一个典型应用场景是网络请求。比如,前端向服务器请求数据时,我们可以不需要让整个应用停下来等待服务器的响应。通过异步调用,前端可以在等待服务器响应的同时,继续执行其他任务,比如响应用户的输入,这会提高用户体验。

简单来说,同步调用就像是在排队取餐,不能走开,而异步调用则像是扫码点餐,可以去做其他事情,等饭好了给你送过来。

异步的优势所在

更快

这里先抛出一个问题:异步会不会让程序运行的更快?

我们以经典的网络请求场景为例,当客户端使用异步的方式发起一次请求后,程序霸占的当前线程就被底层系统分配去干别的事情去了,然后请求会在网络上传递极短的一些时间,到达服务端后再进行一段时间的处理,最后再通过网络将处理结果返回给客户端底层系统,底层系统再唤起之前的任务继续处理。

在这个过程中,网络来回传输的时间、服务端处理的时间都没有受到异步调用的任何影响,反而可能会因为异步调用产生任务切换而增加网络请求的响应时间。所以单次的异步调用并没有让程序运行的更快。

但是但是,异步调用还是可能会让程序整体运行的更快。还是以网络请求场景为例,假设我们需要在页面上发起3个网络请求,每个网络请求的响应时间都是基本相同的,同步的情况下我们只能一个一个的干,总的响应时间就是单次网络请求响应时间的3倍,如果换成异步调用,理想情况下,这三个网络请求可以在服务端并行处理,而网络传输的时间是极短的,那么总的响应时间可能就是一个比单次网络请求响应时间略高一点的数字。所以异步调用相比同步调用,很有可能会让程序整体运行的更快。

谈到更快时,我们这里一直比较的就是时间,如果网络传输的时间、服务端处理的时间都很短,短到就像本地的一次函数调用,那么异步也不会让程序更快。所以根本的问题是网络传输的时间太慢、服务端处理的时间太慢,它们相比CPU的处理速度要慢上很多个数量级,所以这才让异步有了可乘之机,而异步就是在这些网络IO、磁盘IO等慢速设备的通信上发挥主要作用。

更多

我们以一个服务端网络处理程序为例,当请求到达服务端时,程序会给这个请求分配一个线程,用来运行相关的服务端处理程序,假设这个处理中还要调用别的API,同步调用和异步调用就会出现不同的行为了。

同步调用时,线程会一直等在这里,等待的时候谁也不能抢走这个线程,直到这次内部调用返回结果,然后继续处理,直到全部完成,最后返回给调用方。

异步调用时,调用发起后,线程就被底层系统分配给别的任务了,比如用来接收新的网络请求,等这次内部调用的结果返回后,底层系统再为本次任务分配线程资源,然后继续处理,直到全部完成,最后返回给调用方。

我们可以看到,在使用异步调用的情况下,线程的利用率提高了,而这会节省大量的服务器资源。比如,在Linux系统中,一个线程会占用8M的内存资源,那么同步调用时,8G的内存也就能同时接入大概1000个请求,改为异步调用后,8G的内存能同时接入多少请求呢?这里做一个不是很严谨的计算,假设1个请求的完整处理时间为100毫秒,请求接入到发起异步调用的时间为1毫秒,那么使用异步调用后,8G内存就能在这100毫秒内接收100倍的请求,也就是10万个请求。

这也是Go语言、Node.js等可以轻松驾驭高并发的核心法门。

更省

有一种说法是异步调用后,CPU就去干别的了,不用等着网络请求返回,所以节省了CPU资源。其实现代操作系统一般没有这么傻,它有一套比较科学的CPU调度算法,CPU并不会傻傻的等着网络请求返回,除非我们使用特殊的方法霸占着CPU不放。这种说法可能只在古老的操作系统或者一些特殊的嵌入式系统中存在。

异步节省内存资源是实实在在的,同样的网络请求数量下,需要的线程更少了,占用的内存也就更少了。

更好的用户体验

我们可以以一个现代Web应用的实例来说明。当用户在一个复杂的Web应用中进行操作时,比如提交一个表单,这个表单的数据需要通过网络发送到服务器。在这个过程中,我们不希望用户界面冻结或变得无响应。通过使用异步调用发送数据,用户界面可以继续响应其他用户操作,比如滚动页面、点击其他按钮等。服务器的响应会在数据处理完成后返回,这时应用会相应地更新用户界面,而用户可能都没有注意到这个后台的数据交换过程。

异步的实现原理

接下来,我们深入探讨一下异步是怎么做到上边这一切的,特别是事件循环、回调函数,以及Promises和Async/Await这些概念。以Node.js为例,可以先看看这张图,下边会有详细介绍。

事件循环

在一家餐厅里,有一个厨师(CPU)和一个服务员(事件循环)。当顾客(任务)下单(发起异步调用)后,服务员记录下订单,然后继续服务其他顾客。厨师在后厨准备好食物后,服务员再将食物递给对应的顾客。这个过程中,服务员不断的在顾客和厨师之间循环,确保每个顾客的需求都得到满足,这就是事件循环的机制。

在不同的操作系统和语言框架中,事件循环的具体实现可能有所不同,但核心思想是一致的:使得单线程环境下,可以高效地处理多个异步任务,而不会造成阻塞。

Node.js

Node.js是一个基于Chrome V8引擎的JavaScript运行环境,它使用事件驱动、非阻塞IO模型,非常适合处理大量的并发连接。Node.js的事件循环由libuv库实现,这个库专门为了提高Node.js的异步IO性能而设计。

在Node.js中,事件循环负责执行用户代码、收集和处理事件,以及执行队列中的子任务。

.NET

在.NET框架中,异步编程模型(Asynchronous Programming Model, APM)和基于任务的异步模式(Task-based Asynchronous Pattern, TAP)都是.NET中处理异步操作的方式。.NET中的事件循环不像Node.js那样明显,因为.NET应用通常运行在多线程环境下,通过线程池(Thread Pool)来处理异步任务。

在.NET中,异步操作通常通过Task来表示,搭配使用async和await关键字让异步代码的编写和阅读更加直观。.NET运行时会负责调度这些Task到线程池中的线程上执行,从而实现非阻塞的异步操作。

操作系统

语言框架的异步处理都是基于操作系统的底层支持。

在操作系统层面,Linux和Windows提供了不同的机制来实现高效的IO事件处理。

  • Linux上的epoll是一种高效的IO事件通知机制,它允许应用程序监视多个文件描述符,以了解是否有IO操作可执行。epoll相比于传统的select或poll,在处理大量并发连接时可以显著减少资源消耗和提高性能。
  • Windows上的IO完成端口(IOCP)是一个高效的线程池技术,用于处理大量的并发IO操作。IOCP能够将IO操作的完成通知直接与线程池结合起来,当IO操作完成时,相应的处理线程会被唤醒来处理结果。

语言框架为了实现异步操作,在不同的操作系统上会选择相应的异步IO处理方式。

回调函数

回调函数就像是你对服务员说:“当我的汉堡准备好了,请通知我。”服务员(事件循环)记下了这个请求,当厨师(CPU)做好汉堡后,服务员会回来通知你。这个过程就是回调机制。

然而,如果你的要求变得复杂,比如:“我的汉堡准备好后,请通知我,然后我会要求加薯条,薯条准备好后,请再通知我,我可能还会有其他要求……”这样的多层次回调会导致所谓的“回调地狱”,使得代码难以阅读和维护。

function prepareBurger(callback) {
  console.log("开始准备汉堡...");
  setTimeout(() => {
    console.log("汉堡准备好了!");
    callback("汉堡");
  }, 2000); // 假设准备汉堡需要2秒钟
}

function prepareFries(callback) {
  console.log("开始准备薯条...");
  setTimeout(() => {
    console.log("薯条准备好了!");
    callback("薯条");
  }, 1500); // 假设准备薯条需要1.5秒钟
}

// 请求汉堡,然后请求薯条
prepareBurger(function(burger) {
  console.log("你的" + burger + "已经准备好了。");

  // 汉堡准备好后,请求薯条
  prepareFries(function(fries) {
    console.log("你的" + fries + "也准备好了。");

    // 如果这里还有更多的异步请求,代码会继续嵌套下去...
  });
});

Promises和Async/Await

为了解决“回调地狱”的问题,现代编程语言引入了Promises和Async/Await,以Javascript为例:

Promises 就像是你给服务员下了一个订单,并得到了一个“承诺”。服务员说:“我保证会告诉你何时你的汉堡准备好。”这样,你就不需要在柜台前等待,而是可以去做其他事情,服务员会在承诺的时间里来通知你。

function prepareBurger() {
  // 返回一个Promise对象
  return new Promise((resolve, reject) => {
    console.log("开始准备汉堡...");
    setTimeout(() => {
      // 模拟汉堡准备过程
      console.log("汉堡准备好了!");
      resolve("汉堡"); // 成功完成时调用resolve
    }, 2000); // 假设准备汉堡需要2秒钟
  });
}

// 调用prepareBurger,并处理结果
prepareBurger().then(burger => {
  console.log("你的" + burger + "已经准备好了。");
}).catch(error => {
  console.log("出错了:" + error);
});

Promise的写法看起来还是有点怪异,Async/Await 则是在Promises的基础上,让异步代码看起来更像同步代码。使用async/await时,你可以用同步的方式写异步代码,这让代码更加直观易懂。比如,你对服务员说:“我会在这里等,你准备好汉堡后直接给我。”尽管实际上汉堡的准备是异步的,但对你来说,就像是同步等待结果一样。

async function getOrder() {
  try {
    // 等待prepareBurger完成,并获取结果
    const burger = await prepareBurger();
    console.log("你的" + burger + "已经准备好了。");
  } catch (error) {
    // 处理可能发生的错误
    console.log("出错了:" + error);
  }
}

// 调用getOrder
getOrder();

async/await 其实还利用了协程的一些处理方式,协程不是操作系统提供的,而是由编程语言框架在用户程序中实现的,在异步编程中,它就是用来在IO操作发起后,将线程分给其它的任务,在IO操作完成后再给任务分配线程。具体到JavaScript中,是通过Generator生成器实现的,它可以控制函数的暂停和恢复,async/await只是做了一个包装,实际执行时,运行引擎会转换处理。

在 .NET 平台中,同样支持使用 async/await 的方式编写异步代码,只不过 Promise 变成了 Task。

总结

最后,让我们总结一下同步调用和异步调用的区别,以及它们对软件开发的影响。

首先,同步调用就像是在餐厅里排队取餐,你得等服务员把饭端上来后才能干别的事情;而异步调用则像是扫码点餐,餐点制作的时候,你可以去做任何其他事情。简而言之,同步调用会阻塞当前操作直到任务完成,而异步调用不会,它允许程序在等待过程中继续执行其他任务。

对软件开发来说,这两种调用方式的本质区别影响深远。同步调用因为简单直接,适合那些必须顺序执行、步步为营的任务,特别是计算密集型的任务,异步了也没有可以节省的地方;但是,在处理IO操作等耗时任务时,同步调用可能会导致程序"卡住",既霸占大量的资源,又影响用户体验,此时选择异步调用则能更有效的利用计算资源,且显著提高程序的响应性和性能,尤其是在需要大量IO操作的场景下,比如网络服务器、大型数据库操作等。

以上就是本文的主要内容,如有问题欢迎留言讨论!

几乎所有业务系统,都会涉及行政区域。
国家统计局
官网上公开了所有的区域编码,一年一更新。但只能在线查看,没有提供完整数据库下载的连接。为此,我编写了一个简陋的
python
脚本,抓取了近几年的数据,供大家下载。如果这里的下载成品中没有你需要的数据,可以根据自己的要求,修改脚本,再运行起来去官网抓取即可。


在这篇全面解析CDN的技术文章中,我们深入探讨了CDN的基础概念、核心架构、多样化产品和在不同行业中的应用案例。文章揭示了CDN技术如何优化内容分发,提升用户体验,并展望了CDN面临的挑战和未来发展趋势。

关注【TechLeadCloud】,分享互联网架构、云服务技术的全维度知识。作者拥有10+年互联网服务架构、AI产品研发经验、团队管理经验,同济本复旦硕,复旦机器人智能实验室成员,阿里云认证的资深架构师,项目管理专业人士,上亿营收AI产品研发负责人。

file

一、引言

在当今这个以信息消费为主导的数字时代,网页加载的速度和内容的即时可用性已经成为衡量在线服务品质的重要标准。内容分发网络(CDN)作为一种旨在加速网站内容到用户浏览器的服务,其技术、架构以及实现方式的深度解析对于理解现代互联网生态至关重要。

CDN的定义

CDN是一种分布式网络服务,它通过在全球多个数据中心缓存内容,将用户的请求重新路由到最近的服务器节点,从而减少数据传输的距离和时间,加快内容的加载速度。CDN不仅限于缓存静态内容(如图片和CSS文件),而且也能通过各种优化技术提高动态内容(如视频流和实时交互)的传输效率。

CDN的历史演变

自从1990年代中期CDN技术首次出现以来,它已经从基础的缓存和负载均衡服务演变成一个复杂的、具有多层次优化功能的网络。在早期,CDN主要用于解决由于网络堵塞造成的延迟问题,但随着互联网用户数量的爆炸性增长以及移动设备的普及,CDN的功能也不断扩展,不仅提高了内容的分发速度,还增强了数据的安全性,降低了原始内容提供者的带宽成本。

阅读指南

本文旨在为读者提供一个关于CDN的全面技术视角,从基本概念到高级架构,再到未来的趋势。文章将按照逻辑结构展开,首先介绍CDN的基础知识,随后深入分析其内部架构和工作机制,比较不同CDN提供商的服务特点,并探讨CDN技术在未来网络发展中的作用。

为了确保技术深度,本文将引用来自顶级技术博客、GitHub资源以及最新的学术论文。我们鼓励具有不同技术背景的读者深入阅读每个部分,以充分理解CDN的复杂性和它在现代网络基础设施中的关键作用。

通过本文,读者不仅能获得关于CDN技术的知识,还能了解到如何根据具体需求选择合适的CDN解决方案,以及如何评估其性能。最终,我们期望读者能够将这些知识应用到实际场景中,促进云服务技术的普及和理解,为构建更快、更安全、更可靠的互联网环境做出贡献。


二、基础概念

file
要深入理解CDN的技术和应用,首先需要把握它的基础构建块。CDN涉及到多个关键概念,这些概念是其能够有效提供服务的基础。从数据缓存到边缘计算,每一环都对最终用户体验至关重要。

CDN的工作原理

数据缓存

数据缓存是CDN技术的核心。通过将内容存储在靠近用户的服务器上,CDN能够减少数据的传输距离,从而加快内容的加载速度。例如,假设一个用户位于巴黎,而网站的原始服务器设在纽约。没有CDN,所有的请求和数据都需要跨越大西洋进行传输,造成不小的延迟。引入CDN后,这些内容可以缓存到法国或者邻近国家的服务器上,当同一个或者相近地区的用户访问该网站时,可以直接从本地或者临近的缓存服务器获取数据,大大减少了加载时间。

内容分发网络

CDN不仅仅是一系列分散的缓存服务器,而是一个智能的内容分发网络。这个网络根据实时的互联网流量和各种路由算法,动态地决定如何最快地将内容送达用户。举个例子,如果某个地区的服务器由于硬件故障而下线,CDN会自动将请求重定向到下一个最佳节点,而不是简单地返回错误。

边缘计算

边缘计算则是CDN技术发展的最前沿。它涉及在网络边缘执行数据处理,而不是在一个中心节点或云中。这意味着计算更接近数据的来源,可以进一步减少延迟,并优化性能。例如,智能家居设备可以在本地进行数据处理和分析,而无需将数据回传至中心服务器。

关键技术指标

命中率

命中率是衡量CDN性能的一个关键指标,它指的是请求直接由缓存服务而非原始服务器满足的比例。高命中率意味着CDN的效率高,用户能够迅速获得内容。例如,对于一个大型在线零售商,高命中率意味着其产品图片和描述大部分情况下可以直接从CDN缓存中获取,而不需要每次都从原始服务器加载。

延迟

延迟是指数据从源头传到目的地所需的时间。CDN的目标之一就是降低延迟,使用户可以感觉到页面或应用几乎是即时响应的。在线游戏是一个延迟敏感的例子,其中高延迟会导致玩家体验延迟和卡顿,而CDN能通过近距离的节点分发游戏内容来解决这个问题。

带宽

带宽是网络传输能力的度量,它决定了在任何给定时间内可以传输多少数据。CDN通常可以通过分散流量来减轻原始服务器的带宽负担。例如,视频流媒体服务在高峰时段可能会遇到带宽不足的问题,导致视频质量下降或缓冲,CDN可以在多个节点分发流量,确保用户得到连贯的观看体验。

可伸缩性

随着业务

或应用流量的波动,CDN必须能够动态适应流量变化,这就是可伸缩性。例如,电商在促销期间可能会遭遇流量激增,一个好的CDN服务能够在这种突然的流量增长中提供平稳的服务。

通过这些基础概念,我们建立起了理解CDN工作方式的框架。在接下来的章节中,我们将进一步探讨每个概念,并将其与实际案例结合,以便更全面地揭示CDN背后的技术和策略。


三、CDN的架构

file
内容分发网络(CDN)的架构设计是确保其高效运行的关键。这种架构不仅需要考虑如何存储和传输内容,还要考虑如何在全球范围内快速、可靠地提供服务。我们将深入探讨CDN的多层架构模型,包括节点的分布、内容的缓存策略、负载均衡机制,以及安全防护措施。

节点分布

全球分布式节点

CDN的基础是遍布世界各地的节点网络。这些节点通常分布在不同的地理位置,例如数据中心、交换站或互联网服务提供商(ISP)的网络枢纽。通过这种分布式部署,CDN能够将内容靠近用户,从而减少延迟和提高数据传输速度。例如,Netflix在全球部署了数以千计的节点,确保了无论观众位于哪里,都能快速加载并流式传输视频内容。

边缘服务器

边缘服务器是位于CDN节点中的关键组件。它们负责响应最终用户的请求,并直接从边缘位置提供内容。举例来说,当用户尝试访问一个网站时,CDN提供的边缘服务器而非原始主机服务器响应请求,用户从就近的边缘服务器接收数据,大大加快了加载时间。

内容缓存策略

缓存更新机制

CDN必须确保内容是最新的,这就需要一个有效的缓存更新机制。当原始内容发生变化时,CDN需要能够快速更新缓存中的内容。例如,新闻网站会频繁更新报道,CDN会使用特定的算法(如时间戳或ETag验证)来确定何时需要替换缓存中的旧新闻内容。

缓存失效处理

当内容不再有效或过时时,CDN如何处理这些内容是架构设计中的另一关键方面。CDN使用的缓存失效策略决定了内容更新的及时性。采用有效的缓存失效策略,如即时清除或定时失效,可以确保用户总是接收到最新的内容,同时也减轻了原始服务器的压力。

负载均衡机制

智能DNS解析

CDN通常使用智能DNS解析来分配用户请求到最近或最佳的边缘服务器。这个过程涉及到实时地评估哪个节点可以提供最快的响应。以Google的全球负载均衡为例,它通过一个单一的任播IP地址来管理全球范围内的请求,根据用户的地理位置、服务器的健康状况以及全球网络流量状况智能路由,优化整个网络的负载和性能。

冗余与容错

在CDN的架构中,冗余设计和容错机制也至关重要。它们确保当某个节点发生故障时,其他节点能够接管流量,从而提供持续不断的服务。这种设计通过创建多个备份节点和使用自动故障转移技术来实现,保证了即便在单点故障情况下,服务也不会中断。

安全防护措施

DDoS攻击防护

CDN还负责抵御分布式拒绝服务(DDoS)

攻击,这些攻击可能会淹没网站的服务器,导致服务不可用。通过CDN的分布式资源,可以吸收和分散攻击流量,使原始服务器不会直接暴露在攻击下。例如,Cloudflare的CDN服务提供了广泛的DDoS防护,能够识别异常流量并在攻击到达源服务器之前予以阻止。

数据加密与TLS

在传输敏感数据时,加密是必不可少的。CDN通过支持TLS(传输层安全性协议)确保数据在传输过程中的安全性。此外,它还管理和自动更新SSL/TLS证书,以保障数据的加密和用户的隐私。

CDN的架构是一个复杂而精细的系统,它需要精心设计以满足全球范围内不断增长的数据需求和网络安全挑战。在后续章节中,我们将进一步解析这些组件是如何相互作用,以及它们如何支持CDN提供快速、可靠和安全的内容分发服务。


四、CDN的产品介绍

内容分发网络(CDN)的产品类型多样,每种产品都有其特定的功能和优势,以适应不同业务的需求。在这一部分,我们将探讨几种主流的CDN产品,并通过实例说明它们如何服务于现实世界的场景。

通用型CDN产品

Akamai

Akamai 是CDN行业的先行者之一,提供广泛的CDN服务,包括网站和移动内容加速、视频流媒体、云安全服务。它的智能平台通过分布在全球的服务器提供内容分发,保证性能和安全性。
例证
:Akamai 助力全球大型体育赛事,比如奥运会和世界杯的在线直播,承受巨大的并发访问量而不影响观众的观赏体验。

Cloudflare

Cloudflare 是一个性能和安全性都非常强的CDN提供商,它通过其庞大的全球网络来提供服务,特别强调安全性和DDoS攻击防护。
例证
:Cloudflare CDN 成功地缓解了一些史上最大规模的DDoS攻击,保护了许多受欢迎的网站不受损害。

视频和大文件分发

Amazon CloudFront

Amazon CloudFront 是与Amazon Web Services (AWS) 集成的CDN服务,它专门优化了大文件和视频流媒体的分发。
例证
:使用 Amazon CloudFront,Twitch 这个流行的游戏直播平台能够将直播内容高效地传输给全球范围内的用户。

私有CDN

Microsoft Azure CDN

Azure CDN 提供了私有CDN解决方案,允许企业建立和管理自己的CDN,更好地控制内容分发和性能。
例证
:大型企业如EA(Electronic Arts)利用Azure CDN来分发游戏更新和补丁,确保玩家能够迅速获得最新内容。

移动内容加速

Fastly

Fastly 强调其实时CDN能力,可以快速更新缓存内容,特别适合动态内容和移动加速。
例证
:The New York Times 使用 Fastly 来确保新闻内容能够在全球范围内快速和可靠地更新和传递给读者。

安全专注型CDN

Imperva

Imperva 提供CDN服务,特别关注应用程序和数据安全。它结合了数据保护和DDoS防护等服务。
例证
:金融服务公司使用 Imperva 的CDN产品保护其在线交易平台免受攻击,同时提供快速的用户体验。

云原生CDN

Google Cloud CDN

Google Cloud CDN 利用Google的全球基础设施提供快速、可靠的Web和视频内容分发服务,适合构建云原生应用。
例证
:YouTube 作为Google旗下的产品,自然而然地利用了 Google Cloud CDN 的技术,保证了全球数亿用户流畅的视频观看体验。

在选择CDN产品时,企业需要根据自己的业务类型、内容种类、预算限制、性能要求和安全需求来做出决策。市场上的CDN产品虽多,但选择最适合自己业务场景的才是最关键的。


五、CDN的最佳实践

在CDN的应用中,掌握一系列的最佳实践是确保内容分发有效性、提高用户体验、并优化成本的关键。这一节将深入探讨一些被广泛认可的CDN最佳实践,并通过具体的例证来进一步阐明它们的实际应用价值。

缓存策略优化

缓存是CDN效能的核心。合理设置缓存策略,能有效减少源站负载,提高响应速度。

  • 设置合理的缓存时间(TTL)
    : 根据内容更新频率来调整TTL值。静态资源如图片、CSS文件、JavaScript文件可以设置较长的TTL,而动态内容则设置较短的TTL。

  • 利用缓存标签
    : 如ETag或Last-Modified头,可用于验证缓存内容是否为最新,无需重新下载。

  • 例证
    : 一家电商网站在大型促销活动期间,通过增加静态资源的TTL和优化缓存标签,成功应对了流量高峰,减少了服务器压力。

内容压缩

通过压缩可以减少文件大小,提高传输速率,缩短加载时间。

  • 使用Gzip或Brotli压缩
    : 对文本文件如HTML、CSS和JavaScript进行压缩。

  • 例证
    : 新闻网站使用Gzip压缩其文章内容,让用户即便在网络条件不佳的情况下也能迅速加载和阅读。

安全实践

保证内容的安全分发对维护品牌信誉至关重要。

  • 使用HTTPS
    : 确保所有的内容传输通过SSL/TLS加密。

  • 定期更新TLS证书
    : 防止证书过期导致的服务中断。

  • DDoS防护
    : 使用CDN的分布式资源来分散攻击流量。

  • 例证
    : 一家金融服务公司通过CDN启用HTTPS和自动续签SSL证书,确保了客户数据的安全传输。

边缘计算

利用CDN边缘节点进行计算,减少数据回源,加速响应。

  • 边缘逻辑
    : 在CDN节点上执行自定义代码,如重定向、A/B测试、身份验证。

  • 例证
    : 一家国际化媒体集团使用边缘计算功能,实现了用户地理位置识别,动态提供区域化内容与广告。

CDN与其他技术的集成

将CDN与云服务、WAF(网络应用防火墙)、API管理等技术相结合,提升整体性能和安全性。

  • 与云存储集成
    : 直接从云存储拉取内容到CDN节点,加快响应速度。

  • 与WAF集成
    : 在CDN层面集成WAF,实现更近源的安全防护。

  • 例证
    : 一家视频流平台将其内容存储与CDN紧密集成,实现了高效的内容分发和安全防护。

性能监控与分析

持续监控CDN性能,并通过分析日志和数据优化配置。

  • 实时监控
    : 跟踪CDN性能,如命中率、加载时间、流量等。

  • 日志分析
    : 定期分析访问日志,以便于发现并解决问题。

  • 例证
    : 一家在线教育平台通过实时监控和日志分析,不断优化

其CDN配置,保证了学生无缝的在线学习体验。

将这些最佳实践应用于CDN管理中,可以帮助企业提升效率,节省成本,并最终为用户提供更佳的访问体验。


六、CDN的应用案例概述

内容分发网络(CDN)已经成为多个行业内不可或缺的技术,帮助各种类型的企业提高了用户访问速度,提升了内容的全球可达性,同时增强了数据的安全性。以下通过不同行业的应用案例,展示CDN如何在多样化的业务场景中实现价值。

媒体与娱乐行业

  • 视频流服务商
    : 如Netflix或Hulu等视频点播服务,使用CDN缓存和分发高清视频,确保低延迟、高带宽,提供流畅的观影体验。

  • 在线音乐平台
    : Spotify等音乐流服务利用CDN对音乐文件进行加速,使全球用户能够快速下载和播放音乐。

  • 新闻门户
    : CNN或BBC等全球新闻网站通过CDN分发新闻内容,保证即时新闻快速加载和全球可访问性。

电子商务

  • 大型购物平台
    : 亚马逊和eBay等使用CDN优化网页加载速度,提升用户浏览和购物体验,特别是在促销期间,能够有效应对流量激增。

  • 闪购活动
    : 短时间内集中推广的闪购活动通过CDN分担源服务器压力,防止网站因访问量激增而崩溃。

金融服务

  • 在线交易平台
    : CDN在金融行业中用于保障关键交易数据的安全传输,如股票交易平台,保证实时数据的快速分发和高安全标准。

  • 银行及金融应用
    : 银行使用CDN技术加快网银服务的访问速度,同时利用CDN提供的安全防护能力,抵御DDoS攻击等网络威胁。

教育和在线学习

  • 远程教育平台
    : 为了支持高清视频课程和资料下载,课堂直播服务商利用CDN分发内容,提供稳定且连续的学习体验。

  • 大规模开放在线课程(MOOC)
    : 例如Coursera和edX利用CDN技术,使全球学生即便在网络连接质量较差的地区也能顺畅学习。

医疗保健

  • 电子健康记录(EHR)系统
    : 通过CDN提高访问速度和安全性,保护病人的隐私数据不被泄露。

  • 远程医疗服务
    : CDN提升远程医疗视频咨询的质量,确保通信无延迟,增强患者和医生之间的互动。

游戏行业

  • 多人在线游戏
    : CDN用于加速游戏内容的下载,减少游戏加载时间,为玩家提供无缝游戏体验。

  • 游戏更新和补丁分发
    : 游戏公司通过CDN分发大型更新和补丁文件,保证玩家能够同步获取最新内容。

科技和云服务

  • 云计算平台
    : 利用CDN加速云资源的访问,为用户提供低延迟的服务体验,例如AWS、Azure和Google Cloud Platform等。

  • 软件即服务(SaaS)
    : SaaS提供商如Salesforce利用CDN提高全球用户访问速度


七、总结

通过深入探索CDN的基础概念、架构、产品类型及其在各个行业中的实际应用案例,我们可以明确看到CDN技术在当今数字化世界中的重要性。它不仅仅是提高网站和网络应用性能的工具,而且是实现全球化内容分发的基础设施,保证了信息交换的高效性和安全性。

CDN技术之所以能够广泛应用于各种不同的行业,关键在于其能够针对性地解决多样化的需求:

  • 对于媒体与娱乐行业,CDN提供了必要的速度和带宽,让内容创造者与消费者之间的距离变得更近。
  • 在电子商务领域,CDN不仅加快了页面加载,提升了用户体验,还在促销高峰期保障了网站的稳定性。
  • 在金融服务中,CDN的加速和安全特性确保了交易的及时性和保密性。
  • 教育和在线学习平台利用CDN技术打破了地域限制,使知识共享变得无界限。
  • 医疗保健行业通过CDN保护敏感数据,同时提供高效的远程医疗服务。
  • 对于快节奏的游戏行业,CDN是提供持续、快速访问体验的关键。
  • 在科技和云服务领域,CDN是提升云服务体验和全球资源整合的重要推手。

总之,CDN是构建现代网络服务不可或缺的一环,它不断进化以满足日益增长的数据和性能要求。但也要看到,随着5G、边缘计算等新技术的发展,CDN将面临新的挑战和机遇。它需要不断革新,以支撑更加分散和动态的内容分发网络,对抗日益复杂的网络安全威胁,同时还要确保数据的隐私和合规性。

在此基础上,未来的CDN服务商和使用者需要更加注重CDN与云服务的整合,探索基于人工智能的智能内容分发机制,以及提高对于IoT设备和移动端的支持。如此,CDN将继续在优化网络性能、提升用户体验以及推动数字化转型中发挥核心作用,成为推动全球网络发展的重要力量。

关注【TechLeadCloud】,分享互联网架构、云服务技术的全维度知识。作者拥有10+年互联网服务架构、AI产品研发经验、团队管理经验,同济本复旦硕,复旦机器人智能实验室成员,阿里云认证的资深架构师,项目管理专业人士,上亿营收AI产品研发负责人。

如有帮助,请多关注
TeahLead KrisChang,10+年的互联网和人工智能从业经验,10年+技术和业务团队管理经验,同济软件工程本科,复旦工程管理硕士,阿里云认证云服务资深架构师,上亿营收AI产品业务负责人。