开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

「进击Redis」十五、奇妙的 Redis HyperLog

发表于 2020-12-14

前言

好哥哥们,接上篇Redis Bitmaps 你会了吗 。正如标题,Bitmaps 好哥哥会了吗?什么,还没看吗,那别愣着呀,赶紧看看哦。看完记得点赞加关注。讲道理应该是讲清楚了吧,Bitmaps在大数据量上的场景运用的还是挺多的(没接触过大数据量的我流下了悔恨的泪水),今天HyperLogLog 这玩意也是常用于大数据量下的基数统计,不过我又没有用过,找个机会在现在的项目用用,顺便挖点坑(手动狗头保命)。

泪水

概述

首先HyperLogLog 并不是一个数据结构,而是一种基数1统计算法。通过HyperLogLog可以利用极小的内存空间完成独立总数的统计,数据集可以是 IP、Email、ID 等。因为HyperLogLog 只会根据输入元素来计算基数,而不会存储输入元素本身,所以 HyperLogLog 不能想集合那样,返回输入的各个元素。

原理

在上面有提到说HyperLogLog 使用的是概率算法,通过存储元素的hash值的第一个 1 的位置,来计算元素数量。举个栗子:

有一天小明和小红在操场上快乐的玩耍。突然小明红着脸对小红说我们来玩一个玩抛硬币的游戏,我赢的话你就做我女朋友,输了的我话我就做你男朋友,规则是我来负责抛硬币,每次抛到国徽面为一个回合,我可以决定抛几个回合,最后我会告诉你我最长的那个回合抛了多少次,然后你就来猜我一共抛了几个回合。小红红着脸说好呀,但是这不好猜呀,你先抛吧,我要算算这个概率了,于是快速在脑海中绘制了一幅图。

回合k是每回合抛到1(1 是国徽面,0 是数字面)所用的次数,我们已知的是最大的k值,用kmax表示,由于每次抛硬币的结果只有0和1两种情况。所以,kmax 在任意回合出现的概率即为 (1/2)kmax(1/2) ^{kmax}(1/2)kmax ,因此可以推测 n = 2kmax2 ^{kmax}2kmax 。概率学把这种问题叫做伯努利实验2。

然后小明已经完成了 n 个回合,并且告诉小红最长的一次抛了 3 次。小红胸有成竹,马上说出他的答案 8,最后的结果是:小明只抛了一回合,小红输了生气的对小明说玩游戏都不让女朋友赢你个渣男,你走吧,我们不可能了(没想到吧,哈哈哈哈)。

细心的好哥哥能发现上面的的概率算法是存在问题的(导致小红都输了),Philippe Flajolet 教授针对于于上面的问题引入了桶的概念,计算m个桶的加权平均值,这样就能得到比较准确的答案了(实际上还要进行其他修正)。最终的公式如图
公式

回到 Redis 的HyperLogLog,对于一个新插入的字符串,首先得到 64 位的hash值,用前 14 位来定位桶的位置(共有 2142 ^{14}214,即 16384 个桶)。后面 50 位即为伯努利过程,每个桶有6bit,记录第一次出现 1 的位置count,如果count>oldcount,就用count替换oldcount。

命令

在 Redis 中操作HyperLogLog 只提供三个命令

1 添加

1
2
3
4
5
powershell复制代码## 格式,key:键 element: 元素
pfadd key element [element … ]
## 添加一个元素,添加成功返回1
127.0.0.1:6379> pfadd 2020-12-14:unique:ids "uuid-1" "uuid-2" "uuid-3" "uuid-4"
(integer) 1

2 计算基数

pfcount 用于计算一个或多个HyperLogLog的独立总数

1
2
3
4
5
powershell复制代码## 格式,key:键
pfcount key [key … ]
## 返回总个数
127.0.0.1:6379> pfcount 2020-12-14:unique:ids
(integer) 4

3 合并

pfmerge 可以求出多个HyperLogLog的并集并赋值给destkey

1
2
3
4
5
6
7
8
9
10
powershell复制代码## 格式,destkey :结果集key, sourcekey:需要合并的键
pfmerge destkey sourcekey [sourcekey ...]
## 添加2020-12-13号添加元素
127.0.0.1:6379> pfadd 2020-12-13:unique:ids "uuid-4" "uuid-5" "uuid-6" "uuid-7"
(integer) 1
## 计算2020-12-13和2020-12-14号基数
127.0.0.1:6379> pfmerge 2020-12_13_14:unique:ids 2020-12_13:unique:ids 2020-12-14:unique:ids
OK
127.0.0.1:6379> pfcount 2020-12_13_14:unique:ids
(integer) 7

内存使用

1 初始内存统计

1
2
3
4
powershell复制代码127.0.0.1:6379> info memory
# 内存统计
used_memory:835144
used_memory_human:815.57K

2 插入批量数据

1
2
3
4
5
6
7
8
9
10
11
lua复制代码elements=""
key="020-12-14:unique:ids"
for i in `seq 1 1000000`
do
elements="${elements} uuid-"${i}
if [[ $((i%1000)) == 0 ]];
then
redis-cli pfadd ${key} ${elements}
elements=""
fi
done

3 统计使用内存

执行完添加元素操作内存只增加了 15K 左右

1
2
3
4
powershell复制代码info memory
# 内存统计
used_memory:850616
used_memory_human:830.68K

4 准确率分析

使用pfcount的执行结果并不是 100 万

1
2
powershell复制代码127.0.0.1:6379> pfcount 2016_05_01:unique:ids
(integer) 1009838

使用场景

HyperLogLog 内存占用量非常小,但是存在错误率。所以在使用是需要符合以下两点

  1. 只为了计算独立总数,不需要获取单条数据,上面说了只会存计算基数,不会存数据本身。
  2. 可以容忍一定误差率, 上面准确率分析也说到了。

总结

理解HyperLogLog 需要一定的算法知识,我对算法这一块也说很头疼。但是这篇下来好哥哥们应该对HyperLogLog 有一定的了解了。具体算法这个就不深入了,一个头两个大,这个重任就交给好哥哥们去钻研吧。好哥哥,冲冲冲….. 弄透了记得来分享一波(手动狗头护脸)。

本期就到这啦,有不对的地方欢迎好哥哥们评论区留言,另外求关注、求点赞

上一篇: Redis Bitmaps 你会了吗

Footnotes

  1. 基数是一个正整数,代表了在一个集合内不重复元素的个数。比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素)为 5。 ↩
  2. 关于伯努利试验可以看百科解释 ↩

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

你还在用分页?试试 MyBatis 流式查询,真心强大!

发表于 2020-12-14

基本概念

流式查询指的是查询成功后不是返回一个集合而是返回一个迭代器,应用每次从迭代器取一条查询结果。流式查询的好处是能够降低内存使用。

如果没有流式查询,我们想要从数据库取 1000 万条记录而又没有足够的内存时,就不得不分页查询,而分页查询效率取决于表设计,如果设计的不好,就无法执行高效的分页查询。因此流式查询是一个数据库访问框架必须具备的功能。

流式查询的过程当中,数据库连接是保持打开状态的,因此要注意的是:执行一个流式查询后,数据库访问框架就不负责关闭数据库连接了,需要应用在取完数据后自己关闭。

MyBatis 流式查询接口

MyBatis 提供了一个叫 org.apache.ibatis.cursor.Cursor 的接口类用于流式查询,这个接口继承了 java.io.Closeable 和 java.lang.Iterable 接口,由此可知:

  1. Cursor 是可关闭的;
  2. Cursor 是可遍历的。

除此之外,Cursor 还提供了三个方法:

  1. isOpen():用于在取数据之前判断 Cursor 对象是否是打开状态。只有当打开时 Cursor 才能取数据;
  2. isConsumed():用于判断查询结果是否全部取完。
  3. getCurrentIndex():返回已经获取了多少条数据

因为 Cursor 实现了迭代器接口,因此在实际使用当中,从 Cursor 取数据非常简单:

1
scss复制代码cursor.forEach(rowObject -> {...});

但构建 Cursor 的过程不简单

我们举个实际例子。下面是一个 Mapper 类:

1
less复制代码@Mapperpublic interface FooMapper {    @Select("select * from foo limit #{limit}")    Cursor<Foo> scan(@Param("limit") int limit);}

方法 scan() 是一个非常简单的查询。通过指定 Mapper 方法的返回值为 Cursor 类型,MyBatis 就知道这个查询方法一个流式查询。

然后我们再写一个 SpringMVC Controller 方法来调用 Mapper(无关的代码已经省略):

1
less复制代码@GetMapping("foo/scan/0/{limit}")public void scanFoo0(@PathVariable("limit") int limit) throws Exception {    try (Cursor<Foo> cursor = fooMapper.scan(limit)) {  // 1        cursor.forEach(foo -> {});                      // 2    }}

上面的代码中,fooMapper 是 @Autowired 进来的。注释 1 处调用 scan 方法,得到 Cursor 对象并保证它能最后关闭;2 处则是从 cursor 中取数据。

上面的代码看上去没什么问题,但是执行 scanFoo0() 时会报错:

1
csharp复制代码java.lang.IllegalStateException: A Cursor is already closed.

这是因为我们前面说了在取数据的过程中需要保持数据库连接,而 Mapper 方法通常在执行完后连接就关闭了,因此 Cusor 也一并关闭了。

所以,解决这个问题的思路不复杂,保持数据库连接打开即可。我们至少有三种方案可选。关注公众号Java技术栈获取 Mybatis 及更多面试题带答案。

方案一:SqlSessionFactory

我们可以用 SqlSessionFactory 来手工打开数据库连接,将 Controller 方法修改如下:

1
less复制代码@GetMapping("foo/scan/1/{limit}")public void scanFoo1(@PathVariable("limit") int limit) throws Exception {    try (        SqlSession sqlSession = sqlSessionFactory.openSession();  // 1        Cursor<Foo> cursor =               sqlSession.getMapper(FooMapper.class).scan(limit)   // 2    ) {        cursor.forEach(foo -> { });    }}

上面的代码中,1 处我们开启了一个 SqlSession (实际上也代表了一个数据库连接),并保证它最后能关闭;2 处我们使用 SqlSession 来获得 Mapper 对象。这样才能保证得到的 Cursor 对象是打开状态的。

方案二:TransactionTemplate

在 Spring 中,我们可以用 TransactionTemplate 来执行一个数据库事务,这个过程中数据库连接同样是打开的。代码如下:

1
python复制代码@GetMapping("foo/scan/2/{limit}")public void scanFoo2(@PathVariable("limit") int limit) throws Exception {    TransactionTemplate transactionTemplate =             new TransactionTemplate(transactionManager);  // 1    transactionTemplate.execute(status -> {               // 2        try (Cursor<Foo> cursor = fooMapper.scan(limit)) {            cursor.forEach(foo -> { });        } catch (IOException e) {            e.printStackTrace();        }        return null;    });}

上面的代码中,1 处我们创建了一个 TransactionTemplate 对象(此处 transactionManager 是怎么来的不用多解释,本文假设读者对 Spring 数据库事务的使用比较熟悉了),2 处执行数据库事务,而数据库事务的内容则是调用 Mapper 对象的流式查询。注意这里的 Mapper 对象无需通过 SqlSession 创建。

方案三:@Transactional 注解

这个本质上和方案二一样,代码如下:

1
less复制代码@GetMapping("foo/scan/3/{limit}")@Transactionalpublic void scanFoo3(@PathVariable("limit") int limit) throws Exception {    try (Cursor<Foo> cursor = fooMapper.scan(limit)) {        cursor.forEach(foo -> { });    }}

它仅仅是在原来方法上面加了个 @Transactional 注解。这个方案看上去最简洁,但请注意 Spring 框架当中注解使用的坑**:**只在外部调用时生效。在当前类中调用这个方法,依旧会报错。

以上是三种实现 MyBatis 流式查询的方法。

总结了一些2020年的面试题,这份面试题的包含的模块分为19个模块,分别是: Java 基础、容器、多线程、反射、对象拷贝、Java Web 、异常、网络、设计模式、Spring/Spring MVC、Spring Boot/Spring Cloud、Hibernate、MyBatis、RabbitMQ、Kafka、Zookeeper、MySQL、Redis、JVM 。

获取资料以上资料:关注公众号:有故事的程序员,获取学习资料。

记得点个关注+评论哦~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

依赖注入的三种方式 依赖注入的三种方式

发表于 2020-12-14

依赖注入的三种方式

  • 依赖注入:减小对象之间的耦合,通过spring来对对象内的属性赋值
  • 依赖注入的数据类型:
    1. 基本数据类型和String类型
    2. 其他bean类型(在配置文件中或者注解配置过的bean)
    3. 复杂类型和集合类型

通过构造函数注入

  • ==前提==:
    1. 构造函数注入的属性最好不经常发生改变
    2. 类中提供了有参的构造函数
  • 构造函数注入:
    • 使用的标签:
    • 标签出现的位置:标签的内部
    • 标签中的属性:
      • type:指定要注入构造函数参数的数据类型,当存在多个相同数据类型的参数时会报错——==不推荐使用==
      • index:指定要注入构造函数的参数的索引,从0开始——==不推荐使用==
      • name:指定要注入构造函数的参数的名称——==常用==
      • ——————————————————————————————————————
      • value:注入基本数据类型和String数据类型的值
      • ref:根据id注入复杂数据类型(在IOC容器中的bean)
  • 注入方式:
1
2
3
4
5
6
7
8
xml复制代码<!--被注入的bean-->
<bean id="xxx" class="xx.xx.xx">
<construtcor name="xx1" value="1"></construtcor>
<constructor name="xx2" value="2"></constructor>
<constructor name="xx3" ref="now"></constructor><!--引入容器中的日期对象-->
</bean>
<!--配置一个日期对象-->
<bean id="now" class="java.util.Date"></bean>
  • 构造函数注入的优势:在获取bean对象时,注入数据时必须的操作,否则对象无法成功创建
  • 构造函数注入的劣势:改变了bean的实例化方式,使我们在创建bean对象时,如果用不到这些数据,也必须提供

通过set方法注入

  • ==前提==:在类中提供了set方法
  • set方法注入:
    • 使用的标签:
    • 标签位置:标签内部
    • 标签属性
      • name:用于指定属性名称(set方法后的名称,首字母变小写)
      • value:注入基本数据和String
      • ref:引入复杂的数据类型和集合类型
  • 注入方式:
1
2
3
4
5
xml复制代码<bean id="xx" class="xx.xx.xx">
<property name="name" value="周杰伦"></property>
<property name="age" value="18"></property>
<property name="birthday" ref="now"></property><!--引入复杂数据类型-->
</bean>
  • set方法注入的优势:创建对象时没有明确的限制,可以使用默认的构造函数
  • set方法注入的劣势:如果有某个成员必须有值,则获取对象时,有可能set方法没有执行

注入集合类型

  • 使用的标签:结构相同的标签可以互换
    • ,,:用于给list结构集合注入的标签
  • 标签位置:标签内部
  • 注入方式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
xml复制代码<bean id="xx" class="xx.xx.xx">
<!--注入list结构的集合数据-->
<property name="array">
<array>
<value>aaa</value>
<value>bbb</value>
</array>
</property>
<property name="list">
<list>
<value>aaa</value>
<value>bbb</value>
</list>
</property>
<property name="set">
<set>
<value>aaa</value>
<value>bbb</value>
</set>
</property>

<!--注入Map结构的集合数据-->
<property id="map">
<map>
<entry key="xxA" value="aaa"></entry>
<entry key="xxB">bbb</entry><!--value写哪都可以(属性或标签体内)-->
</map>
</property>
<property name="props">
<props>
<prop key="xxA">aaa</prop>
<prop key="xxb">bbb</prop>
</props>
</property>
</bean>

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

MySQL表锁,总以为自己懂了

发表于 2020-12-14

关于 MySQL 的如下问题,你能准确的答出来么?

(1)和行锁相比,表锁有什么优势?

(2)频繁 group by 的业务,用 MyISAM 更好,还是 InnoDB 更好,为什么?

(3)某个 session 占有了表写锁,有另外 N 个 session 又要对表进行写操作,MySQL 是如何处理的?

(4)某个 session 释放了表写锁,有另外 N 个 session 要对表进行写操作,同时还有 M 个 session 要对表进行读操作,谁先抢到锁,为什么?

(5)如何判断表锁是不是主要冲突点?

(6)如何高效的实现并发插入与查询,如何互斥?

(7)MyISAM 什么情况下,数据文件会出现空洞?

(8)MyISAM,假如数据文件有空洞,新插入的数据是先补上空洞,还是插入在文件尾部?

…

但如果你花 1 分钟认真阅读了《频繁插入 (insert) 的业务,用什么存储引擎更合适?》,上述问题都是小 case。

_画外音:_可以跳回原文去找答案。

从评论留言来看,不少同学的反馈是:

“… 文章太容易了…”

“…MyISAM 过时了…”

…

听了大家的反馈,我起初是抱歉,以为聊了一个大家都非常清楚的话题,浪费了大家的时间。

可是,从作业题的评论情况来看:

竟然没有一个同学答对!!!

画外音:可以跳回原文去看评论,很遗憾一个 offer 都发不出去。

当然,可能会有朋友说,你乱出题,你的答案靠谱么。这样,这次我直接贴 MySQL 官网的截图吧:

通常情况下,下列四种情况,表锁会优于行锁:

…

这些情况,用更粗粒度的锁,更易于应用程序调优,因为锁开销会比行锁更小。

画外音:英文不好,翻译不对的大家提出。

很多时候,我们以为自己懂了,其实懂的不透彻。

很多时候,思路比结论重要,MyISAM 确实现在不是主流,但其中的技术思路,也值得我们学习。

**架构师之路 - 分享技术思路**

大家看下开篇的 8 个问题,以及作业题,重温下《频繁插入 (insert) 的业务,用什么存储引擎更合适?》,相信你会有新的收获。

调研:

开篇的 8 个题,全有把握答对的同学,扣个 1。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

记一次 MySQL 启动导致的事故

发表于 2020-12-13

MySQL` 启动存在端口被监听, 但服务没完全启动的情况.

背景

MySQL 启动时会进行 第一次 初始化数据库, 等待 MySQL 完全启动后, 再进行 第二次 初始化数据库.

第一次 初始化数据库使用 --skip-networking (这个选项表示禁止 MySQL 的 TCP/IP 连接方式) 启动 MySQL 进行初始化, 初始化完成后会关闭 --skip-networking 选项重新启动 MySQL.

第二次 初始化数据库会设置 root 密码.

判断 MySQL 已完全启动的方式

尝试主动连接 MySQL, 连接成功则表明服务已完全启动

1
bash复制代码mysql -hlocalhost -P3306 -uroot

事故

由于上面的判断方式使用的是 socket 进行连接数据库, 但第一次只是禁止 MySQL 的 TCP/IP 连接方式,
所以没等 第一次 初始化数据库完成可能就已经进行了 第二次 初始化数据库,
而 第二次 初始化数据库提前于 第一次 初始化数据库设置 root 密码, 导致 第一次 初始化连不上数据库而失败,
最后没有开启 TCP/IP 连接方式, 应用无法连接数据库.

第一次修改

使用 -h127.0.0.1 进行连接数据库

1
bash复制代码mysql -h127.0.01 -P3306 -uroot

但由于 root 用户的 host 设置的是 localhost, 不允许 127.0.0.1 进行连接, 第一次修改失败.

第二次修改

1
2
3
bash复制代码wait-for-it.sh 127.0.0.1:3306 --timeout=300

mysql -hlocalhost -P3306 -uroot

wait-for-it.sh 用于检测 127.0.0.1:3306 是否处于监听状态, 如果是, 则表明 第一次 初始化数据库完成了,
然后再使用 localhost 去尝试连接数据库.

原文链接: k8scat.com/posts/wait-…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

WebApi JWT 身份验证 Net 使用JWT

发表于 2020-12-13

.Net 使用JWT

JWT

  • 什么是JSON Web Token?

SON Web Token(以下简称 JWT)是一套开放的标准(RFC 7519),它定义了一套简洁(compact)且 URL 安全(URL-safe)的方案,以安全地在客户端和服务器之间传输 JSON 格式的信息。

  • 它有什么优点?
  • 体积小(一串字符串)。因而传输速度快
  • 传输方式多样。可以通过 HTTP 头部(推荐)/URL/POST 参数等方式传输
  • 严谨的结构化。它自身(在 payload 中)就包含了所有与用户相关的验证消息,如用户可访问路由、访问有效期等信息,服务器无需再去连接数据库验证信息的有效性,并且 payload 支持应用定制
  • 支持跨域验证,多应用于单点登录
  • 为什么使用JWT?

充分依赖无状态 API ,契合 RESTful 设计原则(无状态的 HTTP)

JWT的Token组成部分

  • header:对token的类型以及加密方法进行base64加密得到;
  • payload:对有效信息进行base64加密得到;
  • signature:对base64加密后的header和base64加密后的payload使用’.’组合为字符串,再通过header中指定的加密方式加secret组合加密;

他是怎么鉴别客户端传来的tojen是否被篡改的?

在我们收到客户端的token后,将token的第一和第二部分,再次进行组合加密第三部分的过程,得到一个signature。如果这个singature和token里的singnature对比不同,则token被篡改。
因此我们服务器端必须保留secret且不能泄露,否则客户端可以自行签发。

如何使用JWT

  • 加密
    • 引入JWT程序包
  • 生成token
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
c#复制代码    private static string Secret = "serect";
static void Main(string[] args)
{
//如果设定过期时间,一定的是秒数
var payload = new Dictionary<string, object>() {
{"name","huangwei" },
};
IJwtAlgorithm algorithm = new HMACSHA256Algorithm();
IJsonSerializer serializer = new JsonNetSerializer();
IBase64UrlEncoder urlencoder = new JwtBase64UrlEncoder();
IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlencoder);
var token = encoder.Encode(payload, Secret);
Console.WriteLine(token);
Console.ReadKey();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
c#复制代码    private static string Secret = "serect";
static void Main(string[] args)
{
IDateTimeProvider provider = new UtcDateTimeProvider();
var timeSpan=provider.GetNow().AddHours(1).ToUnixTimeSeconds();
var payload = new Dictionary<string, object>() {
{"name","huangwei" },
{"exp",timeSpan},
};
IJwtAlgorithm algorithm = new HMACSHA256Algorithm();
IJsonSerializer serializer = new JsonNetSerializer();
IBase64UrlEncoder urlencoder = new JwtBase64UrlEncoder();
IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlencoder);
var token = encoder.Encode(payload, Secret);
Console.WriteLine(token);
Console.ReadKey();
}
}

WebApi中使用JWT进行身份验证

  • 发起登录请求,并传递参数
  • 接收参数,验证登录逻辑,登陆成功则返回token
  • 客户端接受,保存token,请求权限api,并将token附加在header头
  • 服务器验证身份,如果token不存在或token被篡改,验证失败,没有权限获得数据
  • 未使用过滤器的身份验证,略显代码冗余
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
c#复制代码    public class JwtTools
{
//密钥,
public static string key = "love this world and love you too";
public static string Encode(Dictionary<string,object> payload,long expTime,string key)
{
key = key == null ? key : JwtTools.key;
payload.Add("exp", expTime);
IJsonSerializer serializer = new JsonNetSerializer();
IJwtAlgorithm algorithm = new HMACSHA256Algorithm();
IBase64UrlEncoder base64UrlEncoder = new JwtBase64UrlEncoder();
IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, base64UrlEncoder);
//通过header.payload 用 key加密 生成第三段 signature 并返回三段完整token
var token = encoder.Encode(payload, key);
return token;
}
private static string DeEncode(string token,string key)
{
try
{
key = key == null ? key : JwtTools.key;
IJsonSerializer jsonSerializer = new JsonNetSerializer();
var provider = new UtcDateTimeProvider();
IJwtValidator jwtValidator = new JwtValidator(jsonSerializer, provider);
IBase64UrlEncoder base64UrlEncoder = new JwtBase64UrlEncoder();
IJwtAlgorithm algorithm = new HMACSHA256Algorithm();
IJwtDecoder decoder = new JwtDecoder(jsonSerializer,jwtValidator,base64UrlEncoder,algorithm);
var json = decoder.Decode(token, key,verify:true);
return json;
}
catch (TokenExpiredException)
{
throw;
}
catch (SignatureVerificationException)
{
throw;
}
}
public static string ValidateLogin(HttpRequestHeaders headers, string key)
{
if (headers.GetValues("token") == null || !headers.GetValues("token").Any())
{
throw new Exception("请登录");
}
return DeEncode(headers.GetValues("token").First(), key);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
c#复制代码        [HttpPost]
[Route("Login")]
public string Login(dynamic model)
{
if(model.userName.ToString().Length>2 && model.userPwd.ToString() == "123456")
{
return JwtTools.Encode(new Dictionary<string, object>(){
{"name",model.userName.ToString() }
},DateTimeOffset.UtcNow.AddMinutes(30).ToUnixTimeSeconds(),JwtTools.key);
}
throw new Exception("登录失败");
}
[HttpGet]
[Route("getMs")]
public string GetAccoutMS()
{
var json = JwtTools.ValidateLogin(Request.Headers, JwtTools.key);
//首先我们要知道webApi中 http无状态
return json;
}
}

使用这种方法可以实现身份验证,但是如果每个Action都需要通过身份验证,我们岂不是需要在每一个Action中都写一次调用。显然很麻烦。

因此我们有第二种方法,过滤器,当每一次请求到达时,都将先执行过滤器的方法,如果通过则将执行Action。符合asp.net管道事件。

  • 过滤器
    • 1.使用过滤器对JWT进行验证
    • 2.将验证过后得到的结果赋给User.Identity……
    • 3.因为User和Identity是接口类型,因此我们可以实现这些接口,并在过滤器中赋值给User和Identity

      过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
c#复制代码public class MyAuth : Attribute, IAuthorizationFilter
{
public bool AllowMultiple { get; }

public async Task<HttpResponseMessage> ExecuteAuthorizationFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation)
{
IEnumerable<string> headers;
if(actionContext.Request.Headers.GetValues("token")==null|| actionContext.Request.Headers.TryGetValues("token",out headers) == false)
{
return new HttpResponseMessage(HttpStatusCode.Unauthorized);
}
var logName = JwtTools.DeEncode<Dictionary<string,object>>(headers.First())["user"];
var userID = JwtTools.DeEncode<Dictionary<string,object>>(headers.First())["userID"];
//过期时间
var expTime = (long)JwtTools.DeEncode<Dictionary<string, object>>(headers.First())["exp"];
if(DateTimeOffset.UtcNow.ToUnixTimeSeconds() > expTime)
{
return new HttpResponseMessage(HttpStatusCode.Unauthorized);
}
(actionContext.ControllerContext.Controller as ApiController).User = new AppUser(logName.ToString(), int.Parse(userID.ToString()));
return await continuation();
}
}

实现User和Identity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
c#复制代码public class AppUser : IPrincipal
{
public AppUser(string name,int id)
{
Identity = new AppIdentity(name, id);
}
public IIdentity Identity { get; }

public bool IsInRole(string role)
{
throw new NotImplementedException();
}
}
public class AppIdentity : IIdentity
{
public AppIdentity(string name,int id)
{
Name= name;
Id = id;
}
public string Name { get; }
public int Id { get;}

public string AuthenticationType { get; }

public bool IsAuthenticated { get; }
}

JWTTools

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
c#复制代码public class JwtTools
{
public static string key = "huangwei@";
public static string Encode(Dictionary<string,object> payload,string key=null)
{
key = string.IsNullOrEmpty(key) ? JwtTools.key : key;
IJwtAlgorithm algorithm = new HMACSHA256Algorithm();
IJsonSerializer serializer = new JsonNetSerializer();
IBase64UrlEncoder base64UrlEncoder = new JwtBase64UrlEncoder();
IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, base64UrlEncoder);
payload.Add("exp",DateTimeOffset.UtcNow.AddHours(2).ToUnixTimeSeconds());
var json=encoder.Encode(payload, key);
return json;
}
public static T DeEncode<T>(string token, string key = null)
{
key = string.IsNullOrEmpty(key) ? JwtTools.key : key;
try
{
var provider = new UtcDateTimeProvider();
IJsonSerializer jsonSerializer = new JsonNetSerializer();
IBase64UrlEncoder base64UrlEncoder = new JwtBase64UrlEncoder();
IJwtValidator validator = new JwtValidator(jsonSerializer, provider);
IJwtAlgorithm jwtAlgorithm = new HMACSHA256Algorithm();
IJwtDecoder decoder = new JwtDecoder(jsonSerializer, validator, base64UrlEncoder, jwtAlgorithm);
var json = decoder.Decode(token, key, verify: true);
return JsonConvert.DeserializeObject<T>(json);
}
catch (TokenExpiredException)
{
throw;
}
catch (SignatureVerificationException)
{
throw;
}
}
}
  • WebApi支持数据注解

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

图形学 蓄势待发!说一说图片相关的那些概念 前言 目录

发表于 2020-12-13

点赞关注,不再迷路,你的支持对我意义重大!

🔥 Hi,我是丑丑。本文 GitHub · Android-NoteBook 已收录,这里有 Android 进阶成长路线笔记 & 博客,欢迎跟着彭丑丑一起成长。(联系方式在 GitHub)

前言

  • 在深入理解图片底层原理的过程中,会接触到各种概念,今天我们简单梳理一下。
  • 如果能帮上忙,请务必点赞加关注,这真的对我非常重要。

系列文章

  • 《图形学 | 蓄势待发!说一说图片相关的那些概念》
  • 《图形学 | 格物致知!PNG 除了无损压缩你还知道什么?》
  • 《Android | 老生常谈!屏幕适配原理 & 方案总结笔记》
  • 《Android | 毫分缕析!说说图片加载的整个过程》

目录


  1. 存储形式

图像的主要存储形式分为:File、Stream、Bitmap。


  1. 图片分类

2.1 有损压缩 & 无损压缩

所谓损失指的是图片的质量,根据压缩过程是否损失图片质量分为两种:

  • 有损压缩(Lossy compression)

压缩过程中损失图片的一部分信息 / 质量,而这种损失是不可逆的。最常见的有损压缩手段是矢量量化,即按照一定的算法将颜色相似的一块区域合并为同一种颜色。

举例:JPEG。

  • 无损压缩(Lossless Compression)

压缩过程中优化冗余数据,而这种优化不会造成图片质量损失。

举例:PNG。

2.2 位图 & 矢量图

  • 位图(Bitmap,又称点阵图、栅格图)
    • 通过像素阵列的排列来来显示图像;
    • 像素是位图的最小单位,每个像素都有自己的颜色信息;
    • 位图缩放会是增大 / 缩小单个像素,会出现失真(锯齿)。

举例:JEPG、PNG、GIF、WebP。

  • 矢量图(Vector)
    • 通过记录图像元素的形状和颜色的算法,根据算法运算来显示图像;
    • 无限放大缩小不失真。

举例:SVG。

2.3 直接色 & 索引色

  • 直接色(Index Colour)

像素数据数值本身就代表一种颜色,例如一个 ARGB 颜色类型的数值0xFFFF0000就代表正红色。

  • 索引色(Direct Colour)

像素数据本身不代表颜色,而是一个指向颜色索引表的索引号。索引色格式适用于图像颜色类型较少的情况,相对于直接色能够大大缩小文件大小。

引用自 developer.android.com/topic/perfo… —— Android Developers


  1. 色彩

3.1 色彩深度(Bit Depth)

表示像素能呈现的颜色数量,一般用数据位数表示。例如 8 bit 色彩深度,就是把每种颜色通道分为 256 种不同强度等级,所以 RGB 三种颜色通道加起来一共可以表示2242^{24}224种颜色。

3.2 色彩空间(ColorSpace)

又称色域(Color Gamut),表示像素能涵盖的色彩范围,主要有以下几种:

  • CIE XYZ

1931 年,国际照明委员会(CIE)在测得三原色的准确波长后,定义了一个 XYZ 色彩空间,第一次表示出了人类肉眼可见的色彩范围。

经过数学运算之后可以转换为这张更直观的 CIE XY 色度图:

  • sRGB

目前互联网上最常见的色域标准,大概覆盖了 35% 的CIE XYZ 色彩空间。

  • YUV

主要用于表示彩色视频的色彩空间(节省带宽,每个像素位深最大不超过 12 位,最小为 6位)

  • CMYK

主要用于彩色印刷的色彩空间。

引用自 baike.baidu.com/item/%E8%89… —— 百度百科


  1. 文件格式

Editting…

GIF位深为8位,所以文件通常较小,而且支持alpha通道以及动画
Webp在等质量大小上和等大小清晰度上都占极大优势
SVG由xml描述,可以适配任何分辨率而保证图像不失真
所以图片压缩主要是针对PNG和JPEG这两种格式

png压缩采用libpng进行压缩,主要场景为编译阶段以及api层调用
jpeg压缩采用libjpeg(Android7后是)进行压缩,主要场景为api层调用


推荐阅读

  • 算法 | 链表问题总结
  • 算法 | 回溯算法解题框架
  • 密码学 | Base64是加密算法吗?
  • 操作系统 | 中断 & 系统调用浅析
  • 数据结构 | 微博 Top 10 热搜是怎么计算出来的?(二叉堆)
  • 计算机网络 | 图解 DNS & HTTPDNS 原理
  • Android | 毫分缕析!说说图片加载的整个过程
  • Android | 食之无味!App Startup 可能比你想象中要简单
  • Android | 适可而止!看Glide如何把生命周期安排得明明白白

感谢喜欢!你的点赞是对我最大的鼓励!欢迎关注彭旭锐的GitHub!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Keepalived详解

发表于 2020-12-13
  1. Keepalived 简介

Keepalived 是一种高性能的服务器高可用或热备解决方案,Keepalived 可以用来防止服务器单点故障的发生,通过配合 Nginx 可以实现web服务的高可用。

Keepalived 是以 VRRP 协议为实现基础的,VRRP全称Virtual Router Redundancy Protocol,即虚拟路由冗余协议。

虚拟路由冗余协议,可以认为是实现路由器高可用的协议,即将N台提供相同功能的路由器(可以理解为单个Nginx节点)组成一个路由器组,这个组里面有一个master 和多个 backup,master 上面有一个对外提供服务的 VIP(Virtual IP Address)(该路由器所在局域网内其他机器的默认路由为该 vip),master 会发组播,当 backup 收不到 vrrp 包时就认为 master 宕掉了,这时就需要根据 VRRP 的优先级来选举一个 backup 当 master。这样的话就可以保证路由器的高可用了。

keepalived 主要有三个模块,分别是core、check 和 vrrp。core 模块为keepalived的核心,负责主进程的启动、维护以及全局配置文件的加载和解析。check 负责健康检查,包括常见的各种检查方式。vrrp 模块是来实现 VRRP 协议的。

  1. Keepalived 安装部署

注意:以下都是在本地Mac上通过虚拟机的方式进行搭建运行的。在阿里云及腾讯云无虚拟IP功能,比较麻烦。

  • 1、下载安装包
    下载地址:
    www.keepalived.org/download.ht…


将下载后的keepalived上传到linux上,这里上传到/home/software上

  • 2、解压
1
复制代码tar -zxvf keepalived-2.0.18.tar.gz
  • 3、配置
    解压后,进入相应的文件夹下,用configure进行安装配置。
1
bash复制代码./configure --prefix=/usr/local/keepalived --sysconf=/etc

prefix:keepalived安装的位置

sysconf:keepalived核心配置文件所在位置,固定位置,改成其他位置则keepalived启动不了,/var/log/messages中会报错。

配置过程中可能会出现警告信息,如下所示:

解决方法:安装 libnl/libnl-3 依赖 yum -y install libnl libnl-devel,重新configure一下就好了。

  • 4、安装keepalived
1
go复制代码make && make install

-5、通过 whereis keepalived 查看安装后的未知

  1. Keepalived 配置与启动

1、进入 /etc/keepalived/,对keepalived.conf进行相关配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
perl复制代码global_defs {
# 路由id:当前安装keepalived的节点主机标识符,保证全局唯一
router_id keep_104
}
# 计算机节点
vrrp_instance VI_1 {
# 表示状态是MASTER主机还是备用机BACKUP
state MASTER
# 该实例绑定的网卡名称
interface ens33
# 保证主备节点一致即可
virtual_router_id 51
# 权重,master权重一般高于backup,如果有多个,那就是选举,谁的权重高,谁就当选
priority 100
# 主备之间同步检查时间间隔,单位秒,默认为1s
advert_int 1
# 认证权限密码,防止非法节点进入
authentication {
auth_type PASS
auth_pass 1111
}
# 虚拟出来的ip,可以有多个(vip)
virtual_ipaddress {
192.168.1.108 # 注意阿里云及腾讯云目前不支持自己定义VIP,
腾讯云支持自己申请,但是目前也暂时关闭了。
}
}

附:查看网卡名称

2、启动 Keepalived

在sbin目录中进行启动,如下图:

查看进程

1
perl复制代码ps -ef|grep keepalived

查看虚拟 IP(VIP)

在网卡 ens33 下,多了一个 192.168.1.108 ,这个就是虚拟ip

  1. 把 Keepalived 注册为系统服务

进入解压缩安装包的 etc 文件夹


将配置文件拷贝至系统 etc文件

1
2
bash复制代码cp init.d/keepalived /etc/init.d/
cp sysconfig/keepalived /etc/sysconfig/

刷新系统服务,加载新添加的 Keepalived 服务

1
复制代码systemctl daemon-reload

之后就可以通过如下命令启动/关闭keepalived

1
2
3
4
5
6
bash复制代码# 启动keepalived
systemctl start keepalived
# 停止keepalived
systemctl stop keepalived
# 重启keepalived
systemctl restart keepalived

5.Keepalived 实现双机主备高可用

在配置完 Keepalived 主服务器节点后,接下来就可以配置备用服务器节点了,备用服务器配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
perl复制代码global_defs {
router_id keep_105
}

vrrp_instance VI_1 {
# 备用机设置为BACKUP
state BACKUP
interface ens33
virtual_router_id 51
# 权重低于MASTER
priority 80
advert_int 2
authentication {
auth_type PASS
auth_pass 1111
}
virtual_ipaddress {
# 注意:主备两台的vip都是一样的,绑定到同一个vip
192.168.1.108
}
}

启动 Keepalived

1
sql复制代码systemctl start keepalived

查看备用服务器 Keepalived 进程

现在主服务器节点 192.168.1.104 以及备用服务器节点 192.168.1.105 以及 Keepalived 虚拟 IP 192.168.1.105已配置完毕

这里我在本地将 Keepalived 虚拟IP 192.168.1.108已映射至 www.keep.com

所以直接访问 www.keep.com 即可访问主服务器节点

当主服务器节点的 Keepalived 服务不可用时(这里我直接将主服务器的 Keepalived 服务直接停止systemctl stop keepalived.service,便于测试),虚拟IP 自动绑定至备用服务器节点地址

  1. Keepalived 配置 Nginx 自动重启

1、增加Nginx重启检测脚本

1
bash复制代码vim /etc/keepalived/check_nginx_alive_or_not.sh
1
2
3
4
5
6
7
8
9
10
11
12
bash复制代码#!/bin/bash

A=`ps -C nginx --no-header |wc -l`
# 判断nginx是否宕机,如果宕机了,尝试重启
if [ $A -eq 0 ];then
/usr/local/nginx/sbin/nginx
# 等待一小会再次检查nginx,如果没有启动成功,则停止keepalived,使其启动备用机
sleep 3
if [ `ps -C nginx --no-header |wc -l` -eq 0 ];then
killall keepalived
fi
fi

增加运行权限

1
bash复制代码chmod +x /etc/keepalived/check_nginx_alive_or_not.sh

2、在 keepalived.conf 配置定时监听 nginx 状态脚本

1
2
3
4
5
6
bash复制代码vrrp_script check_nginx_alive {
script "/etc/keepalived/check_nginx_alive_or_not.sh"
interval 2 # 每隔两秒运行上一行脚本
weight 10 # 如果脚本运行成功,则升级权重+10
# weight -10 # 如果脚本运行失败,则升级权重-10
}

3、在vrrp_instance中新增监控的脚本

1
2
3
bash复制代码track_script {
check_nginx_alive # 追踪 nginx 脚本
}

4、重启Keepalived使得配置文件生效

1
复制代码systemctl restart keepalived
  1. Keepalived 实现双主热备高可用

实现双机主备,可能由于一台资源一直空闲的情况,这是采用双主热备可以解决这个问题。

Keepalived 双主热备详细配置:

规则:以一个虚拟ip分组归为同一个路由

主节点配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
kotlin复制代码global_defs {
router_id keep_104
}

vrrp_instance VI_1 {
state MASTER
interface ens33
virtual_router_id 51
priority 100
advert_int 1
authentication {
auth_type PASS
auth_pass 1111
}
virtual_ipaddress {
192.168.1.108
}
}

vrrp_instance VI_2 {
state BACKUP
interface ens33
virtual_router_id 52
priority 80
advert_int 1
authentication {
auth_type PASS
auth_pass 1111
}
virtual_ipaddress {
192.168.1.138
}
}

备用节点配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
kotlin复制代码global_defs {
router_id keep_105
}

vrrp_instance VI_1 {
state BACKUP
interface ens33
virtual_router_id 51
priority 80
advert_int 1
authentication {
auth_type PASS
auth_pass 1111
}
virtual_ipaddress {
192.168.1.108
}
}

vrrp_instance VI_2 {
state MASTER
interface ens33
virtual_router_id 52
priority 100
advert_int 1
authentication {
auth_type PASS
auth_pass 1111
}
virtual_ipaddress {
192.168.1.138
}
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【SpringBoot DB系列】Jooq批量写入采坑记录

发表于 2020-12-13

【SpringBoot DB系列】Jooq批量写入采坑记录

前面介绍了jooq的三种批量插入方式,结果最近发现这里面居然还有一个深坑,我以为的批量插入居然不是一次插入多条数据,而是一条一条的插入…,这就有点尬了

1. 三种插入姿势

关于项目创建以及jooq的相关使用姿势,推荐查看之前的博文: 【DB系列】Jooq之新增记录使用姿势

下面是我们采用的三种批量插入方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
java复制代码/**
* 通过Record执行批量添加
*
* 通过源码查看,这种插入方式实际上是单条单条的写入数据,和下面的一次插入多条有本质区别
*
* @param list
* @return
*/
public boolean batchSave(List<PoetBO> list) {
List<PoetPO> poList = list.stream().map(this::bo2po).collect(Collectors.toList());
int[] ans = dsl.batchInsert(poList).execute();
System.out.println(JSON.toJSONString(ans));
return true;
}

/**
* 类sql写法,批量添加
*
* @param list
* @return
*/
public boolean batchSave2(List<PoetBO> list) {
InsertValuesStep2<PoetPO, Integer, String> step = dsl.insertInto(table).columns(table.ID, table.NAME);
for (PoetBO bo : list) {
step.values(bo.getId(), bo.getName());
}
return step.execute() > 0;
}

/**
* 不基于自动生成的代码,来批量添加数据
*
* @param list
* @return
*/
public boolean batchSave3(List<PoetBO> list) {
InsertQuery insertQuery = dsl.insertQuery(DSL.table("poet"));
for (PoetBO bo : list) {
insertQuery.addValue(DSL.field("id", Integer.class), bo.getId());
insertQuery.addValue(DSL.field("name", String.class), bo.getName());
insertQuery.newRecord();
}

return insertQuery.execute() > 0;
}

请注意上面的三种批量插入方式,基本上对应的就是jooq的三种常见的用法

  • 直接借助自动生成的Record类来操作
  • 类sql的拼接写法,基本上我们平时的sql怎么写,这里就怎么用
  • InsertQuery:借助jooq提供的各种Query类来执行目标操作

2. 日志验证

上面三种写法中,第一种批量插入方式,并不是我们传统理解的一次插入多条记录,相反它是一条一条的插入的,我们可以通过开启jooq的日志来查看一些执行的sql情况

配置文件 application.properties,添加下面的配置

1
2
3
properties复制代码debug=false
trace=false
logging.level.org.jooq=DEBUG

如果有自己的logback.xml配置文件,可以调整一下日志级别,将jooq的debug日志放出来

一个简单的测试case

1
2
3
4
5
java复制代码public void test() {
this.batchSave(Arrays.asList(new PoetBO(14, "yh"), new PoetBO(15, "yhh")));
this.batchSave2(Arrays.asList(new PoetBO(16, "yihui"), new PoetBO(17, "yihuihui")));
this.batchSave3(Arrays.asList(new PoetBO(18, "YiHui"), new PoetBO(19, "YiHuiBlog")));
}

从上面的sql来看,后面两个确实是一次插入多条,但是第一个,也没有将具体执行的sql打印出来,所有不看源码的话,也没有办法实锤是一条一条插入的

为了验证这个问题,一个简单的解决办法就是批量插入两条数据,第一条正常,第二条异常,如果第一条插入成功,第二条失败那就大概率是单个插入的了

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码// 表结构中,name的字段最大为20,下面插入的第二条数据长度超限
try {
this.batchSave(Arrays.asList(new PoetBO(14, "yh"), new PoetBO(15, "1234567890098765432112345")));
} catch (Exception e) {
e.printStackTrace();
}

try {
this.batchSave2(Arrays.asList(new PoetBO(16, "yihui"), new PoetBO(17, "1234567890098765432112345")));
} catch (Exception e) {
e.printStackTrace();
}
this.batchSave3(Arrays.asList(new PoetBO(18, "YiHui"), new PoetBO(19, "YiHuiBlog")));

第一种批量插入失败

第二种插入失败

插入后结果

请注意上面的报错,以及最终插入的结果,第一种插入方式一个插入成功一个失败;第二种批量插入方式,两条都插入失败;

通常情况下,一次插入多条数据时,一个插入失败,会导致整个插入都失败,如下

3. 源码分析

上面是从日志以及结果表现来推测实际的执行情况,接下来就需要从源码角度来看一下,是否真的是单个的执行了

省略掉具体的定位过程,直接找到org.jooq.impl.BatchCRUD#execute,对应的代码

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Override
public final int[] execute() throws DataAccessException {

// [#1180] Run batch queries with BatchMultiple, if no bind variables
// should be used...
if (executeStaticStatements(configuration.settings())) {
return executeStatic();
}
else {
return executePrepared();
}
}

上面有两种插入方式,对于插入的核心逻辑一样

遍历集合,获取单个 record,执行 CURD

II. 其他

0. 项目

系列博文

  • 【SpringBoot DB系列】Jooq之记录更新与删除
  • 【SpringBoot DB系列】Jooq之新增记录使用姿势
  • 【SpringBoot DB系列】Jooq代码自动生成
  • 【SpringBoot DB系列】Jooq初体验

项目源码

  • 工程:github.com/liuyueyi/sp…
  • 项目源码: github.com/liuyueyi/sp…

1. 一灰灰Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

  • 一灰灰Blog个人博客 blog.hhui.top
  • 一灰灰Blog-Spring专题博客 spring.hhui.top
  • 微信公众号:一灰灰blog

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

图形学 格物致知!PNG 除了无损压缩你还知道什么? 前

发表于 2020-12-13

点赞关注,不再迷路,你的支持对我意义重大!

🔥 Hi,我是丑丑。本文 GitHub · Android-NoteBook 已收录,这里有 Android 进阶成长路线笔记 & 博客,欢迎跟着彭丑丑一起成长。(联系方式在 GitHub)

前言

  • PNG(Protable Network Graphics,便携式网络图形) 是一种非常常见的图片格式,深入理解 PNG 文件原理,有利于加强对图片编解码过程的理解,便于开展优化工作。
  • 在这篇文章里,我将带你探究 PNG 图片的 数据结构 & 编码原理 & 优化方法等。如果能帮上忙,请务必点赞加关注,这真的对我非常重要。

系列文章

  • 《图形学 | 蓄势待发!说一说图片相关的那些概念》
  • 《图形学 | 格物致知!PNG 除了无损压缩你还知道什么?》
  • 《Android | 老生常谈!屏幕适配原理 & 方案总结笔记》
  • 《Android | 毫分缕析!说说图片加载的整个过程》

目录


  1. 概述

1.1 定义

PNG(Portable Network Graphics,便携式网络图形)是一种 无损压缩的位图图形格式。它本身的设计目的是替代 GIF 格式,所以它与 GIF 有一些相似之处。

1.1 特点

  • 无损压缩(Lossless Compression):采用了 LZ77 派生算法 + Huffman 编码,压缩率更高,体积更小且不损失数据;
  • 支持透明:支持 8 位透明通道,而 GIF 只支持 1 比特透明通道,JPEG 不支持透明通道;
  • 图像的复杂程度越低,使用 PNG 格式的压缩率更高(原因见 第 3 节 编码原理);
  • 支持五种像素 / 颜色类型
+ 索引色(Indexed-colour,8 位)
+ 灰度(Greyscale,8 位)
+ 真彩色(Truecolour,24 位)
+ 灰度透明(Greyscale with alpha,16 位)
+ 真彩色透明(Truecolour with alpha,32 位)


引用自 www.w3.org/TR/2003/REC… —— w3.org

提示: 网上所述三种 PNG 类型为:PNG 8、PNG 24、PNG 32,经查阅百科、PNG 官网和《PNG 文件协议》未能找到支撑依据,本文不作阐述。


  1. PNG 数据结构

这一节我们来讨论 PNG 图片的数据结构,一个 PNG 格式文件(或数据流)由一个 8 字节的签名和若干个数据块(chunk)组成。

2.1 PNG 签名

PNG 文件头部的 8 字节为文件签名(或称魔数),这个值将被用来识别文件是否为 PNG 文件,数据固定为8950 4E47 0D0A 1A0A。以下是我任意打开的一个 PNG 文件:

提示: 使用 010 Editor 可以查看文件二进制数据。首次打开 PNG 文件时,它会提示安装 PNG.bt,安装后将帮助解析你读懂每块数据的含义。

2.2 数据块

  • 数据块类型

PNG 定义了两种数据块:关键数据块(critical chunks)& 辅助数据块(ancillary chunks)。

其中,关键数据块是从 PNG 数据流中成功解码图像所必需的,而辅助数据块是可选的,我们将重点了解关键数据块。关键数据块分为四种:文件头数据块、调色板数据块、图像数据块和图像结束数据块。

  • 数据块的内部结构

PNG 的每一块数据块都由四个部分组成:

名称 字节数 描述
长度(length) 4 指定数据块中数据域的长度
数据块类型码(Chunk Type Code) 4 由(A-Z、a-z)组成
数据 length 数据块数据
循环冗余检测(CRC) 4 用于检错
  • 文件头数据块 IDHR(header chunk)

文件头数据块是 PNG 文件中的第一个数据块,包含了 PNG 文件的基本信息,由 13 个字节组成,数据结构如下:

字段 字节数 描述
图片宽度(Width) 4 以像素为单位
图片高度(Height) 4 以像素为单位
图像深度(Bit depth) 1 /
颜色类型(ColorType) 1 5 种颜色类型:索引色灰度灰度透明真彩色(24 位直接色)真彩色透明(32 位直接色)
压缩方法(Compression method) 1 LZ77 派生算法
滤波方法(Filter method) 1 /
隔行扫描方法(Interface method) 1 0:非隔行扫描1: Adam7(7遍隔行扫描方法)
  • 调色板数据块 PLTE(palette chunk)

调色板数据块包含有与索引彩色图像(indexed-color image)相关的彩色变换数据,它仅与索引彩色图像有关,而且要放在图像数据块 IDAT 之前。真彩色的 PNG 数据流也可以有调色板数据块,目的是便于非真彩色显示程序用它来量化图像数据,从而显示该图像。

  • 图像数据块 IDAT(image data chunk)

图像数据块是图像实际存储的数据,可包含多个连续顺序的图像数据块。

  • 图像结束数据块 IEND(image trailer chunk)

图像结束数据块标记 PNG 文件或者数据流已经结束,必须要放在尾部。


  1. PNG 编码原理

通过查阅《Protable Network Graphic (PNG) Specification (Second Edition)》,可以了解到 PNG 文件的编码过程主要分为五个阶段:

  • 1、通过提取(pass extraction)
  • 2、扫描线序列化(scanline serialization)
  • 3、过滤(filtering)
  • 4、压缩(compression)
  • 5、分块(chunking)

前两个步骤我看不懂,我直接讲后三个步骤。

3.3 滤波(filtering)

这个步骤的主要思想是 增量编码,即:一个值可以表示为与前值的差值。

例如:[2,3,4,5,6,7,8][2,3,4,5,6,7,8][2,3,4,5,6,7,8]可以编码为[2,3−2=1,4−3=1,5−4=1,6−5=1,7−6=1,8−7=1][2,3-2=1,4-3=1,5-4=1,6-5=1,7-6=1,8-7=1][2,3−2=1,4−3=1,5−4=1,6−5=1,7−6=1,8−7=1] =>[2,1,1,1,1,1,1][2,1,1,1,1,1,1][2,1,1,1,1,1,1]

可以发现,增量编码后的数据变成了大量重复、低值的数据,这样的数据是有利于压缩的,原因我在 第 3.4 节 说。

回到 PNG 的滤波步骤,这个步骤主要是选择合适的差分过滤器,分别处理每一行中的每个像素,使得差分编码的数值尽可能重复、尽可能小,要点如下:

  • 1、滤波器有以下五种


引用自 zh.wikipedia.org/wiki/PNG —— 维基百科

在后面的 分块(chunking) 步骤中,会把编码过程中使用的滤波方法被记录 IHDR 中的Filter method字段里。

  • 2、为了最优效果,每一行可使用不同滤波器


引用自 medium.com/@duhroach/h… —— Colt McAnlis 著

  • 3、滤波却是按通道而不是按像素进行的

滤波是按通道而不是按像素进行的,也就是说滤波是先扫描一行中像素的红色通道,然后再扫描一行中像素的蓝色通道,以此类推。

  • 4、一行中所有通道使用相同的滤波器

3.4 压缩(compression)

PNG 的压缩过程结合了 LZ77 编码和 Huffman 编码,要点如下:

  • 1、压缩过程是无损压缩
  • 2、颜色越单一,颜色差异越小,压缩率越大

LZ77 编码

一种基于字典的、“滑动窗”的无损压缩算法。

Huffman 编码(霍夫曼编码)

一种用于无损压缩的变长编码,主要思想为:评估码元的出现几率选择不同长度的编码,几率较高的码元使用较短的编码,而几率较低的码元使用较长的编码,这将使得编码的平均长度降低。

3.5 分块(chunking)

分块可方便地将压缩数据流分解为可管理的块,关于数据块的结构我们在 第 2 节中讨论过。


  1. 缩小 PNG 文件大小

4.1 减少颜色数量 & 差异

在 第 3 节 编码原理 的讨论,我们提到了 增量编码 的概念,通过减少颜色的数量 & 差异,滤波步骤之后数据的差异更小,有利于在压缩步骤中缩小文件。

不过,减少颜色的数量 & 差异等同于给图片进行了有损编码,应当确保在高效压缩和图片质量之间保持适当的平衡。

4.2 采用索引色格式

如果图片的单色数量不超过 256 个,则可以采用Indexed-colour(索引色)格式。原始图像的每个像素由调色板中的一个索引表示。此时,每个像素的字节数由 3 字节(无透明通道)和 4 字节(有透明通道)减少到 1 字节,大大缩小文件。

引用自 developer.android.com/topic/perfo… —— Android Developers

4.3 采用矢量量化(vector quantization)

如果图片的单色数量超过 256 个,就需要 采用矢量量化较少颜色数量,再生成索引色格式。矢量量化就是将颜色相似的一块区域合并为同一种颜色的舍入的过程,主要分为以下步骤:

  • 1、根据相近区域相似度进行分组;
  • 2、将该组中所有颜色替换为单个中心点的颜色;
  • 3、生成索引色格式。

引用自 developer.android.com/topic/perfo… —— Android Developers


  1. PNG 的有损压缩方案

在 第 3 节 编码原理 中我们提到了 PNG 压缩是无损压缩,为了进一步提高压缩率,可以采用有损压缩算法,以下是现有的 PNG 有损压缩方案。

在官网介绍中,其实可以清楚地了解到它们的原理,主要就是采用了 第 4.3 节 讨论的方案:采用矢量量化较少颜色数量,再生成索引色格式。

5.1 TinyPNG

官方地址:tinypng.com

How does it work?

…… This technique is called “quantization”. By reducing the number of colors, 24-bit PNG files can be converted to much smaller 8-bit indexed color images. All unnecessary metadata is stripped too. The result better PNG files with 100% support for transparency. Have your cake and eat it too!

局限:只提供了 HTTP 请求的方式,并且有次数限制。

5.2 pngquant

官方地址:pngquant.org

Features

High-quality palette generation using a combination of vector quantization algorithms.
……

5.3 zopflipng

5.4 pngcursh

Editting…


参考资料

  • 《PNG 词条》 —— 维基百科
  • 《PNG 格式》 —— GameRes.com
  • 《缩减图片下载大小》 —— Android Developers
  • 《How PNG Works》 —— Colt McAnlis 著
  • 《优化包大小 - PNG 部分》 —— 奋斗的 Leo 著
  • 《浅谈移动端图片压缩(iOS & Android)》 —— Nemocdz 著
  • 《Protable Network Graphic (PNG) Specification (Second Edition)》 —— w3.org

推荐阅读

  • 算法 | 链表问题总结
  • 算法 | 回溯算法解题框架
  • 密码学 | Base64是加密算法吗?
  • 操作系统 | 中断 & 系统调用浅析
  • 数据结构 | 微博 Top 10 热搜是怎么计算出来的?(二叉堆)
  • 计算机网络 | 图解 DNS & HTTPDNS 原理
  • Android | 毫分缕析!说说图片加载的整个过程
  • Android | 食之无味!App Startup 可能比你想象中要简单
  • Android | 适可而止!看Glide如何把生命周期安排得明明白白

感谢喜欢!你的点赞是对我最大的鼓励!欢迎关注彭旭锐的GitHub!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…754755756…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%