auto commit
This commit is contained in:
parent
0a47291191
commit
b04f8e35a4
|
@ -406,7 +406,7 @@ Session 可以存储在服务器上的文件、数据库或者内存中,现在
|
|||
- 服务器返回的响应报文的 Set-Cookie 首部字段包含了这个 Session ID,客户端收到响应报文之后将该 Cookie 值存入浏览器中;
|
||||
- 客户端之后对同一个服务器进行请求时会包含该 Cookie 值,服务器收到之后提取出 Session ID,从 Redis 中取出用户信息,继续之后的业务操作。
|
||||
|
||||
应该注意 Session ID 的安全性问题,不能让它被恶意攻击者轻易获取,那么就不能产生一个容易被才到的 Session ID 值。此外,还需要经常重新生成 Session ID。在对安全性要求极高的场景下,例如转账等操作,除了使用 Session 管理用户状态之外,还需要对用户进行重新验证,比如重新输入密码,或者使用短信验证码等方式。
|
||||
应该注意 Session ID 的安全性问题,不能让它被恶意攻击者轻易获取,那么就不能产生一个容易被猜到的 Session ID 值。此外,还需要经常重新生成 Session ID。在对安全性要求极高的场景下,例如转账等操作,除了使用 Session 管理用户状态之外,还需要对用户进行重新验证,比如重新输入密码,或者使用短信验证码等方式。
|
||||
|
||||
### 8. 浏览器禁用 Cookie
|
||||
|
||||
|
|
|
@ -270,7 +270,7 @@ finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。
|
|||
|
||||
<div align="center"> <img src="../pics//a4248c4b-6c1d-4fb8-a557-86da92d3a294.jpg" width=""/> </div><br>
|
||||
|
||||
将需要回收的对象进行标记,然后清理掉被标记的对象。
|
||||
将需要存活的对象进行标记,然后清理掉未被标记的对象。
|
||||
|
||||
不足:
|
||||
|
||||
|
|
323
notes/Redis.md
323
notes/Redis.md
|
@ -1,39 +1,42 @@
|
|||
<!-- GFM-TOC -->
|
||||
* [一、Redis 是什么](#一redis-是什么)
|
||||
* [二、五种基本类型](#二五种基本类型)
|
||||
* [1. STRING](#1-string)
|
||||
* [2. LIST](#2-list)
|
||||
* [3. SET](#3-set)
|
||||
* [4. HASH](#4-hash)
|
||||
* [5. ZSET](#5-zset)
|
||||
* [三、键的过期时间](#三键的过期时间)
|
||||
* [四、发布与订阅](#四发布与订阅)
|
||||
* [五、事务](#五事务)
|
||||
* [六、持久化](#六持久化)
|
||||
* [1. 快照持久化](#1-快照持久化)
|
||||
* [2. AOF 持久化](#2-aof-持久化)
|
||||
* [七、复制](#七复制)
|
||||
* [从服务器连接主服务器的过程](#从服务器连接主服务器的过程)
|
||||
* [主从链](#主从链)
|
||||
* [八、处理故障](#八处理故障)
|
||||
* [九、分片](#九分片)
|
||||
* [1. 客户端分片](#1-客户端分片)
|
||||
* [2. 代理分片](#2-代理分片)
|
||||
* [3. 服务器分片](#3-服务器分片)
|
||||
* [十、事件](#十事件)
|
||||
* [事件类型](#事件类型)
|
||||
* [事件的调度与执行](#事件的调度与执行)
|
||||
* [十一、Redis 与 Memcached 的区别](#十一redis-与-memcached-的区别)
|
||||
* [一、概述](#一概述)
|
||||
* [二、数据类型](#二数据类型)
|
||||
* [STRING](#string)
|
||||
* [LIST](#list)
|
||||
* [SET](#set)
|
||||
* [HASH](#hash)
|
||||
* [ZSET](#zset)
|
||||
* [三、使用场景](#三使用场景)
|
||||
* [缓存](#缓存)
|
||||
* [计数器](#计数器)
|
||||
* [应用限流](#应用限流)
|
||||
* [消息队列](#消息队列)
|
||||
* [查找表](#查找表)
|
||||
* [交集运算](#交集运算)
|
||||
* [排行榜](#排行榜)
|
||||
* [分布式 Session](#分布式-session)
|
||||
* [分布式锁](#分布式锁)
|
||||
* [四、Redis 与 Memcached](#四redis-与-memcached)
|
||||
* [数据类型](#数据类型)
|
||||
* [数据持久化](#数据持久化)
|
||||
* [分布式](#分布式)
|
||||
* [内存管理机制](#内存管理机制)
|
||||
* [十二、Redis 适用场景](#十二redis-适用场景)
|
||||
* [缓存](#缓存)
|
||||
* [消息队列](#消息队列)
|
||||
* [计数器](#计数器)
|
||||
* [好友关系](#好友关系)
|
||||
* [十三、数据淘汰策略](#十三数据淘汰策略)
|
||||
* [五、键的过期时间](#五键的过期时间)
|
||||
* [六、数据淘汰策略](#六数据淘汰策略)
|
||||
* [七、持久化](#七持久化)
|
||||
* [快照持久化](#快照持久化)
|
||||
* [AOF 持久化](#aof-持久化)
|
||||
* [八、发布与订阅](#八发布与订阅)
|
||||
* [九、事务](#九事务)
|
||||
* [十、事件](#十事件)
|
||||
* [文件事件](#文件事件)
|
||||
* [时间事件](#时间事件)
|
||||
* [事件的调度与执行](#事件的调度与执行)
|
||||
* [十一、复制](#十一复制)
|
||||
* [连接过程](#连接过程)
|
||||
* [主从链](#主从链)
|
||||
* [十二、Sentinel](#十二sentinel)
|
||||
* [十三、分片](#十三分片)
|
||||
* [十四、一个简单的论坛系统分析](#十四一个简单的论坛系统分析)
|
||||
* [文章信息](#文章信息)
|
||||
* [点赞功能](#点赞功能)
|
||||
|
@ -42,15 +45,15 @@
|
|||
<!-- GFM-TOC -->
|
||||
|
||||
|
||||
# 一、Redis 是什么
|
||||
# 一、概述
|
||||
|
||||
Redis 是速度非常快的非关系型(NoSQL)内存键值数据库,可以存储键和五种不同类型的值之间的映射。
|
||||
|
||||
五种类型数据类型为:字符串、列表、集合、有序集合、散列表。
|
||||
键的类型只能为字符串,值支持的五种类型数据类型为:字符串、列表、集合、有序集合、散列表。
|
||||
|
||||
Redis 支持很多特性,例如将内存中的数据持久化到硬盘中,使用复制来扩展读性能,使用分片来扩展写性能。
|
||||
|
||||
# 二、五种基本类型
|
||||
# 二、数据类型
|
||||
|
||||
| 数据类型 | 可以存储的值 | 操作 |
|
||||
| :--: | :--: | :--: |
|
||||
|
@ -62,7 +65,7 @@ Redis 支持很多特性,例如将内存中的数据持久化到硬盘中,
|
|||
|
||||
> [What Redis data structures look like](https://redislabs.com/ebook/part-1-getting-started/chapter-1-getting-to-know-redis/1-2-what-redis-data-structures-look-like/)
|
||||
|
||||
## 1. STRING
|
||||
## STRING
|
||||
|
||||
<div align="center"> <img src="../pics//6019b2db-bc3e-4408-b6d8-96025f4481d6.png" width="400"/> </div><br>
|
||||
|
||||
|
@ -77,7 +80,7 @@ OK
|
|||
(nil)
|
||||
```
|
||||
|
||||
## 2. LIST
|
||||
## LIST
|
||||
|
||||
<div align="center"> <img src="../pics//fb327611-7e2b-4f2f-9f5b-38592d408f07.png" width="400"/> </div><br>
|
||||
|
||||
|
@ -105,7 +108,7 @@ OK
|
|||
2) "item"
|
||||
```
|
||||
|
||||
## 3. SET
|
||||
## SET
|
||||
|
||||
<div align="center"> <img src="../pics//cd5fbcff-3f35-43a6-8ffa-082a93ce0f0e.png" width="400"/> </div><br>
|
||||
|
||||
|
@ -139,7 +142,7 @@ OK
|
|||
2) "item3"
|
||||
```
|
||||
|
||||
## 4. HASH
|
||||
## HASH
|
||||
|
||||
<div align="center"> <img src="../pics//7bd202a7-93d4-4f3a-a878-af68ae25539a.png" width="400"/> </div><br>
|
||||
|
||||
|
@ -170,7 +173,7 @@ OK
|
|||
2) "value1"
|
||||
```
|
||||
|
||||
## 5. ZSET
|
||||
## ZSET
|
||||
|
||||
<div align="center"> <img src="../pics//1202b2d6-9469-4251-bd47-ca6034fb6116.png" width="400"/> </div><br>
|
||||
|
||||
|
@ -202,41 +205,98 @@ OK
|
|||
2) "982"
|
||||
```
|
||||
|
||||
# 三、键的过期时间
|
||||
# 三、使用场景
|
||||
|
||||
## 缓存
|
||||
|
||||
将热点数据放到内存中,设置内存的最大使用量以及过期淘汰策略来保证缓存的命中率。
|
||||
|
||||
## 计数器
|
||||
|
||||
Redis 这种内存数据库能支持计数器频繁的读写操作。
|
||||
|
||||
## 应用限流
|
||||
|
||||
限制一个网站访问流量。
|
||||
|
||||
## 消息队列
|
||||
|
||||
使用 List 数据类型,它是双向链表。
|
||||
|
||||
## 查找表
|
||||
|
||||
使用 HASH 数据类型。
|
||||
|
||||
## 交集运算
|
||||
|
||||
使用 SET 类型,例如求两个用户的共同好友。
|
||||
|
||||
## 排行榜
|
||||
|
||||
使用 ZSET 数据类型。
|
||||
|
||||
## 分布式 Session
|
||||
|
||||
多个应用服务器的 Session 都存储到 Redis 中来保证 Session 的一致性。
|
||||
|
||||
## 分布式锁
|
||||
|
||||
除了可以使用 SETNX 实现分布式锁之外,还可以使用官方提供的 RedLock 分布式锁实现。
|
||||
|
||||
|
||||
# 四、Redis 与 Memcached
|
||||
|
||||
两者都是非关系型内存键值数据库。有以下主要不同:
|
||||
|
||||
## 数据类型
|
||||
|
||||
Memcached 仅支持字符串类型,而 Redis 支持五种不同种类的数据类型,使得它可以更灵活地解决问题。
|
||||
|
||||
## 数据持久化
|
||||
|
||||
Redis 支持两种持久化策略:RDB 快照和 AOF 日志,而 Memcached 不支持持久化。
|
||||
|
||||
## 分布式
|
||||
|
||||
Memcached 不支持分布式,只能通过在客户端使用像一致性哈希这样的分布式算法来实现分布式存储,这种方式在存储和查询时都需要先在客户端计算一次数据所在的节点。
|
||||
|
||||
Redis Cluster 实现了分布式的支持。
|
||||
|
||||
## 内存管理机制
|
||||
|
||||
在 Redis 中,并不是所有数据都一直存储在内存中,可以将一些很久没用的 value 交换到磁盘。而 Memcached 的数据则会一直在内存中。
|
||||
|
||||
Memcached 将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题,但是这种方式会使得内存的利用率不高,例如块的大小为 128 bytes,只存储 100 bytes 的数据,那么剩下的 28 bytes 就浪费掉了。
|
||||
|
||||
|
||||
# 五、键的过期时间
|
||||
|
||||
Redis 可以为每个键设置过期时间,当键过期时,会自动删除该键。
|
||||
|
||||
对于散列表这种容器,只能为整个键设置过期时间(整个散列表),而不能为键里面的单个元素设置过期时间。
|
||||
|
||||
过期时间对于清理缓存数据非常有用。
|
||||
# 六、数据淘汰策略
|
||||
|
||||
# 四、发布与订阅
|
||||
可以设置内存最大使用量,当内存使用量超过时施行淘汰策略,具体有 6 种淘汰策略。
|
||||
|
||||
订阅者订阅了频道之后,发布者向频道发送字符串消息会被所有订阅者接收到。
|
||||
| 策略 | 描述 |
|
||||
| :--: | :--: |
|
||||
| volatile-lru | 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰 |
|
||||
| volatile-ttl | 从已设置过期时间的数据集中挑选将要过期的数据淘汰 |
|
||||
|volatile-random | 从已设置过期时间的数据集中任意选择数据淘汰 |
|
||||
| allkeys-lru | 从所有数据集中挑选最近最少使用的数据淘汰 |
|
||||
| allkeys-random | 从所有数据集中任意选择数据进行淘汰 |
|
||||
| noeviction | 禁止驱逐数据 |
|
||||
|
||||
发布与订阅模式和观察者模式有以下不同:
|
||||
如果使用 Redis 来缓存数据时,要保证所有数据都是热点数据,可以将内存最大使用量设置为热点数据占用的内存量,然后启用 allkeys-lru 淘汰策略,将最近最少使用的数据淘汰。
|
||||
|
||||
- 观察者模式中,观察者和主题都知道对方的存在;而在发布与订阅模式中,发布者与订阅者不知道对方的存在,它们之间通过频道进行通信。
|
||||
- 观察者模式是同步的,当事件触发时,主题会去调度观察者的方法;而发布与订阅模式是异步的;
|
||||
作为内存数据库,出于对性能和内存消耗的考虑,Redis 的淘汰算法(LRU、TTL)实际实现上并非针对所有 key,而是抽样一小部分 key 从中选出被淘汰 key,抽样数量可通过 maxmemory-samples 配置。
|
||||
|
||||
<div align="center"> <img src="../pics//bee1ff1d-c80f-4b3c-b58c-7073a8896ab2.jpg" width="400"/> </div><br>
|
||||
|
||||
发布与订阅有一些问题,很少使用它,而是使用替代的解决方案。问题如下:
|
||||
|
||||
- 如果订阅者读取消息的速度很慢,会使得消息不断积压在发布者的输出缓存区中,造成内存占用过多;
|
||||
- 如果订阅者在执行订阅的过程中网络出现问题,那么就会丢失断线期间发送的所有消息。
|
||||
|
||||
# 五、事务
|
||||
|
||||
Redis 最简单的事务实现方式是使用 MULTI 和 EXEC 命令将事务操作包围起来。
|
||||
|
||||
MULTI 和 EXEC 中的操作将会一次性发送给服务器,而不是一条一条发送,这种方式称为流水线,它可以减少客户端与服务器之间的网络通信次数从而提升性能。
|
||||
|
||||
# 六、持久化
|
||||
# 七、持久化
|
||||
|
||||
Redis 是内存型数据库,为了保证数据在断电后不会丢失,需要将内存中的数据持久化到硬盘上。
|
||||
|
||||
## 1. 快照持久化
|
||||
## 快照持久化
|
||||
|
||||
将某个时间点的所有数据都存放到硬盘上。
|
||||
|
||||
|
@ -246,13 +306,13 @@ Redis 是内存型数据库,为了保证数据在断电后不会丢失,需
|
|||
|
||||
如果数据量很大,保存快照的时间会很长。
|
||||
|
||||
## 2. AOF 持久化
|
||||
## AOF 持久化
|
||||
|
||||
将写命令添加到 AOF 文件(Append Only File)的末尾。
|
||||
|
||||
对硬盘的文件进行写入时,写入的内容首先会被存储到缓冲区,然后由操作系统决定什么时候将该内容同步到硬盘,用户可以调用 file.flush() 方法请求操作系统尽快将缓冲区存储的数据同步到硬盘。
|
||||
对硬盘的文件进行写入时,写入的内容首先会被存储到缓冲区,然后由操作系统决定什么时候将该内容同步到硬盘,用户可以调用 file.flush() 方法请求操作系统尽快将缓冲区存储的数据同步到硬盘。可以看出写入文件的数据不会立即同步到硬盘上,在将写命令添加到 AOF 文件时,要根据需求来保证何时同步到硬盘上。
|
||||
|
||||
将写命令添加到 AOF 文件时,要根据需求来保证何时将添加的数据同步到硬盘上,有以下同步选项:
|
||||
有以下同步选项:
|
||||
|
||||
| 选项 | 同步频率 |
|
||||
| :--: | :--: |
|
||||
|
@ -260,67 +320,55 @@ Redis 是内存型数据库,为了保证数据在断电后不会丢失,需
|
|||
| everysec | 每秒同步一次 |
|
||||
| no | 让操作系统来决定何时同步 |
|
||||
|
||||
always 选项会严重减低服务器的性能;everysec 选项比较合适,可以保证系统奔溃时只会丢失一秒左右的数据,并且 Redis 每秒执行一次同步对服务器性能几乎没有任何影响;no 选项并不能给服务器性能带来多大的提升,而且也会增加系统奔溃时数据丢失的数量。
|
||||
- always 选项会严重减低服务器的性能;
|
||||
- everysec 选项比较合适,可以保证系统奔溃时只会丢失一秒左右的数据,并且 Redis 每秒执行一次同步对服务器性能几乎没有任何影响;
|
||||
- no 选项并不能给服务器性能带来多大的提升,而且也会增加系统奔溃时数据丢失的数量。
|
||||
|
||||
随着服务器写请求的增多,AOF 文件会越来越大;Redis 提供了一种将 AOF 重写的特性,能够去除 AOF 文件中的冗余写命令。
|
||||
随着服务器写请求的增多,AOF 文件会越来越大。Redis 提供了一种将 AOF 重写的特性,能够去除 AOF 文件中的冗余写命令。
|
||||
|
||||
# 七、复制
|
||||
# 八、发布与订阅
|
||||
|
||||
通过使用 slaveof host port 命令来让一个服务器成为另一个服务器的从服务器。
|
||||
订阅者订阅了频道之后,发布者向频道发送字符串消息会被所有订阅者接收到。
|
||||
|
||||
一个从服务器只能有一个主服务器,并且不支持主主复制。
|
||||
某个客户端使用 SUBSCRIBE 订阅一个频道,其它客户端可以使用 PUBLISH 向这个频道发送消息。
|
||||
|
||||
## 从服务器连接主服务器的过程
|
||||
发布与订阅模式和观察者模式有以下不同:
|
||||
|
||||
- 主服务器创建快照文件,发送给从服务器,并在发送期间使用缓冲区记录执行的写命令。快照文件发送完毕之后,开始向从服务器发送存储在缓冲区中的写命令;
|
||||
- 观察者模式中,观察者和主题都知道对方的存在;而在发布与订阅模式中,发布者与订阅者不知道对方的存在,它们之间通过频道进行通信。
|
||||
- 观察者模式是同步的,当事件触发时,主题会去调用观察者的方法;而发布与订阅模式是异步的;
|
||||
|
||||
- 从服务器丢弃所有旧数据,载入主服务器发来的快照文件,之后从服务器开始接受主服务器发来的写命令;
|
||||
<div align="center"> <img src="../pics//bee1ff1d-c80f-4b3c-b58c-7073a8896ab2.jpg" width="400"/> </div><br>
|
||||
|
||||
- 主服务器每执行一次写命令,就向从服务器发送相同的写命令。
|
||||
# 九、事务
|
||||
|
||||
## 主从链
|
||||
一个事务包含了多个命令,服务器在执行事务期间,不会改去执行其它客户端的命令请求。
|
||||
|
||||
随着负载不断上升,主服务器可能无法很快地更新所有从服务器,或者重新连接和重新同步从服务器将导致系统超载。为了解决这个问题,可以创建一个中间层来分担主服务器的复制工作。中间层的服务器是最上层服务器的从服务器,又是最下层服务器的主服务器。
|
||||
事务中的多个命令被一次性发送给服务器,而不是一条一条发送,这种方式被称为流水线,它可以减少客户端与服务器之间的网络通信次数从而提升性能。
|
||||
|
||||
<div align="center"> <img src="../pics//395a9e83-b1a1-4a1d-b170-d081e7bb5bab.png" width="600"/> </div><br>
|
||||
|
||||
# 八、处理故障
|
||||
|
||||
要用到持久化文件来恢复服务器的数据。
|
||||
|
||||
持久化文件可能因为服务器出错也有错误,因此要先对持久化文件进行验证和修复。对 AOF 文件就行验证和修复很容易,修复操作将第一个出错命令和其后的所有命令都删除;但是只能验证快照文件,无法对快照文件进行修复,因为快照文件进行了压缩,出现在快照文件中间的错误可能会导致整个快照文件的剩余部分无法读取。
|
||||
|
||||
当主服务器出现故障时,Redis 常用的做法是新开一台服务器作为主服务器,具体步骤如下:假设 A 为主服务器,B 为从服务器,当 A 出现故障时,让 B 生成一个快照文件,将快照文件发送给 C,并让 C 恢复快照文件的数据。最后,让 B 成为 C 的从服务器。
|
||||
|
||||
# 九、分片
|
||||
|
||||
Redis 中的分片类似于 MySQL 的分表操作,分片是将数据划分为多个部分的方法,对数据的划分可以基于键包含的 ID、基于键的哈希值,或者基于以上两者的某种组合。通过对数据进行分片,用户可以将数据存储到多台机器里面,也可以从多台机器里面获取数据,这种方法在解决某些问题时可以获得线性级别的性能提升。
|
||||
|
||||
假设有 4 个 Reids 实例 R0,R1,R2,R3,还有很多表示用户的键 user:1,user:2,... 等等,有不同的方式来选择一个指定的键存储在哪个实例中。最简单的方式是范围分片,例如用户 id 从 0\~1000 的存储到实例 R0 中,用户 id 从 1001\~2000 的存储到实例 R1 中,等等。但是这样需要维护一张映射范围表,维护操作代价很高。还有一种方式是哈希分片,使用 CRC32 哈希函数将键转换为一个数字,再对实例数量求模就能知道应该存储的实例。
|
||||
|
||||
## 1. 客户端分片
|
||||
|
||||
客户端使用一致性哈希等算法决定键应当分布到哪个节点。
|
||||
|
||||
## 2. 代理分片
|
||||
|
||||
将客户端请求发送到代理上,由代理转发请求到正确的节点上。
|
||||
|
||||
## 3. 服务器分片
|
||||
|
||||
Redis Cluster。
|
||||
Redis 最简单的事务实现方式是使用 MULTI 和 EXEC 命令将事务操作包围起来。
|
||||
|
||||
# 十、事件
|
||||
|
||||
## 事件类型
|
||||
Redis 服务器是一个事件驱动程序。
|
||||
|
||||
### 1. 文件事件
|
||||
## 文件事件
|
||||
|
||||
服务器有许多套接字,事件产生时会对这些套接字进行操作,服务器通过监听套接字来处理事件。常见的文件事件有:客户端的连接事件;客户端的命令请求事件;服务器向客户端返回命令结果的事件;
|
||||
服务器通过套接字与客户端或者其它服务器进行通信,文件事件就是对套接字操作的抽象。
|
||||
|
||||
### 2. 时间事件
|
||||
Redis 基于 Reactor 模式开发了自己的网络时间处理器,使用 I/O 多路复用程序来同时监听多个套接字,并将到达的时间传送给文件事件分派器,分派器会根据套接字产生的事件类型调用响应的时间处理器。
|
||||
|
||||
又分为两类:定时事件是让一段程序在指定的时间之内执行一次;周期性事件是让一段程序每隔指定时间就执行一次。
|
||||
<div align="center"> <img src="../pics//9ea86eb5-000a-4281-b948-7b567bd6f1d8.png"/> </div><br>
|
||||
|
||||
## 时间事件
|
||||
|
||||
服务器有一些操作需要在给定的时间点执行,时间事件是对这类定时操作的抽象。
|
||||
|
||||
时间事件又分为:
|
||||
|
||||
- 定时事件:是让一段程序在指定的时间之内执行一次;
|
||||
- 周期性事件:是让一段程序每隔指定时间就执行一次。
|
||||
|
||||
Redis 将所有时间事件都放在一个无序链表中,通过遍历整个链表查找出已到达的时间事件,并调用响应的事件处理器。
|
||||
|
||||
## 事件的调度与执行
|
||||
|
||||
|
@ -374,64 +422,41 @@ def main():
|
|||
|
||||
<div align="center"> <img src="../pics//dda1608d-26e0-4f10-8327-a459969b150a.png" width=""/> </div><br>
|
||||
|
||||
# 十一、Redis 与 Memcached 的区别
|
||||
# 十一、复制
|
||||
|
||||
两者都是非关系型内存键值数据库。有以下主要不同:
|
||||
通过使用 slaveof host port 命令来让一个服务器成为另一个服务器的从服务器。
|
||||
|
||||
## 数据类型
|
||||
一个从服务器只能有一个主服务器,并且不支持主主复制。
|
||||
|
||||
Memcached 仅支持字符串类型,而 Redis 支持五种不同种类的数据类型,使得它可以更灵活地解决问题。
|
||||
## 连接过程
|
||||
|
||||
## 数据持久化
|
||||
1. 主服务器创建快照文件,发送给从服务器,并在发送期间使用缓冲区记录执行的写命令。快照文件发送完毕之后,开始向从服务器发送存储在缓冲区中的写命令;
|
||||
|
||||
Redis 支持两种持久化策略:RDB 快照和 AOF 日志,而 Memcached 不支持持久化。
|
||||
2. 从服务器丢弃所有旧数据,载入主服务器发来的快照文件,之后从服务器开始接受主服务器发来的写命令;
|
||||
|
||||
## 分布式
|
||||
3. 主服务器每执行一次写命令,就向从服务器发送相同的写命令。
|
||||
|
||||
Memcached 不支持分布式,只能通过在客户端使用像一致性哈希这样的分布式算法来实现分布式存储,这种方式在存储和查询时都需要先在客户端计算一次数据所在的节点。
|
||||
## 主从链
|
||||
|
||||
Redis Cluster 实现了分布式的支持。
|
||||
随着负载不断上升,主服务器可能无法很快地更新所有从服务器,或者重新连接和重新同步从服务器将导致系统超载。为了解决这个问题,可以创建一个中间层来分担主服务器的复制工作。中间层的服务器是最上层服务器的从服务器,又是最下层服务器的主服务器。
|
||||
|
||||
## 内存管理机制
|
||||
<div align="center"> <img src="../pics//395a9e83-b1a1-4a1d-b170-d081e7bb5bab.png" width="600"/> </div><br>
|
||||
|
||||
在 Redis 中,并不是所有数据都一直存储在内存中,可以将一些很久没用的 value 交换到磁盘。而 Memcached 的数据则会一直在内存中。
|
||||
# 十二、Sentinel
|
||||
|
||||
Memcached 将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题,但是这种方式会使得内存的利用率不高,例如块的大小为 128 bytes,只存储 100 bytes 的数据,那么剩下的 28 bytes 就浪费掉了。
|
||||
Sentinel(哨兵)可以监听主服务器,并在主服务器进入下线状态时,自动从从服务器中选举出新的主服务器。
|
||||
|
||||
# 十二、Redis 适用场景
|
||||
# 十三、分片
|
||||
|
||||
## 缓存
|
||||
分片是将数据划分为多个部分的方法,可以将数据存储到多台机器里面,也可以从多台机器里面获取数据,这种方法在解决某些问题时可以获得线性级别的性能提升。
|
||||
|
||||
将热点数据放到内存中。
|
||||
假设有 4 个 Reids 实例 R0,R1,R2,R3,还有很多表示用户的键 user:1,user:2,... 等等,有不同的方式来选择一个指定的键存储在哪个实例中。最简单的方式是范围分片,例如用户 id 从 0\~1000 的存储到实例 R0 中,用户 id 从 1001\~2000 的存储到实例 R1 中,等等。但是这样需要维护一张映射范围表,维护操作代价很高。还有一种方式是哈希分片,使用 CRC32 哈希函数将键转换为一个数字,再对实例数量求模就能知道应该存储的实例。
|
||||
|
||||
## 消息队列
|
||||
主要有三种分片方式:
|
||||
|
||||
List 类型是双向链表,很适合用于消息队列。
|
||||
|
||||
## 计数器
|
||||
|
||||
Redis 这种内存数据库能支持计数器频繁的读写操作。
|
||||
|
||||
## 好友关系
|
||||
|
||||
使用 Set 类型的交集操作很容易就可以知道两个用户的共同好友。
|
||||
|
||||
# 十三、数据淘汰策略
|
||||
|
||||
可以设置内存最大使用量,当内存使用量超过时施行淘汰策略,具体有 6 种淘汰策略。
|
||||
|
||||
| 策略 | 描述 |
|
||||
| :--: | :--: |
|
||||
| volatile-lru | 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰 |
|
||||
| volatile-ttl | 从已设置过期时间的数据集中挑选将要过期的数据淘汰 |
|
||||
|volatile-random | 从已设置过期时间的数据集中任意选择数据淘汰 |
|
||||
| allkeys-lru | 从所有数据集中挑选最近最少使用的数据淘汰 |
|
||||
| allkeys-random | 从所有数据集中任意选择数据进行淘汰 |
|
||||
| noeviction | 禁止驱逐数据 |
|
||||
|
||||
如果使用 Redis 来缓存数据时,要保证所有数据都是热点数据,可以将内存最大使用量设置为热点数据占用的内存量,然后启用 allkeys-lru 淘汰策略,将最近最少使用的数据淘汰。
|
||||
|
||||
作为内存数据库,出于对性能和内存消耗的考虑,Redis 的淘汰算法(LRU、TTL)实际实现上并非针对所有 key,而是抽样一小部分 key 从中选出被淘汰 key,抽样数量可通过 maxmemory-samples 配置。
|
||||
- 客户端分片:客户端使用一致性哈希等算法决定键应当分布到哪个节点。
|
||||
- 代理分片:将客户端请求发送到代理上,由代理转发请求到正确的节点上。
|
||||
- 服务器分片:Redis Cluster。
|
||||
|
||||
# 十四、一个简单的论坛系统分析
|
||||
|
||||
|
|
39
notes/SQL.md
39
notes/SQL.md
|
@ -374,10 +374,10 @@ HAVING num >= 2;
|
|||
|
||||
分组规定:
|
||||
|
||||
1. GROUP BY 子句出现在 WHERE 子句之后,ORDER BY 子句之前;
|
||||
2. 除了汇总字段外,SELECT 语句中的每一字段都必须在 GROUP BY 子句中给出;
|
||||
3. NULL 的行会单独分为一组;
|
||||
4. 大多数 SQL 实现不支持 GROUP BY 列具有可变长度的数据类型。
|
||||
- GROUP BY 子句出现在 WHERE 子句之后,ORDER BY 子句之前;
|
||||
- 除了汇总字段外,SELECT 语句中的每一字段都必须在 GROUP BY 子句中给出;
|
||||
- NULL 的行会单独分为一组;
|
||||
- 大多数 SQL 实现不支持 GROUP BY 列具有可变长度的数据类型。
|
||||
|
||||
# 十四、子查询
|
||||
|
||||
|
@ -520,10 +520,10 @@ WHERE col =2;
|
|||
|
||||
视图具有如下好处:
|
||||
|
||||
1. 简化复杂的 SQL 操作,比如复杂的连接;
|
||||
2. 只使用实际表的一部分数据;
|
||||
3. 通过只给用户访问视图的权限,保证数据的安全性;
|
||||
4. 更改数据格式和表示。
|
||||
- 简化复杂的 SQL 操作,比如复杂的连接;
|
||||
- 只使用实际表的一部分数据;
|
||||
- 通过只给用户访问视图的权限,保证数据的安全性;
|
||||
- 更改数据格式和表示。
|
||||
|
||||
```sql
|
||||
CREATE VIEW myview AS
|
||||
|
@ -536,11 +536,11 @@ WHERE col5 = val;
|
|||
|
||||
存储过程可以看成是对一系列 SQL 操作的批处理;
|
||||
|
||||
使用存储过程的好处
|
||||
使用存储过程的好处:
|
||||
|
||||
1. 代码封装,保证了一定的安全性;
|
||||
2. 代码复用;
|
||||
3. 由于是预先编译,因此具有很高的性能。
|
||||
- 代码封装,保证了一定的安全性;
|
||||
- 代码复用;
|
||||
- 由于是预先编译,因此具有很高的性能。
|
||||
|
||||
命令行中创建存储过程需要自定义分隔符,因为命令行是以 ; 为结束符,而存储过程中也包含了分号,因此会错误把这部分分号当成是结束符,造成语法错误。
|
||||
|
||||
|
@ -561,6 +561,7 @@ create procedure myprocedure( out ret int )
|
|||
into y;
|
||||
select y*y into ret;
|
||||
end //
|
||||
|
||||
delimiter ;
|
||||
```
|
||||
|
||||
|
@ -632,10 +633,10 @@ MySQL 不允许在触发器中使用 CALL 语句,也就是不能调用存储
|
|||
|
||||
基本术语:
|
||||
|
||||
1. 事务(transaction)指一组 SQL 语句;
|
||||
2. 回退(rollback)指撤销指定 SQL 语句的过程;
|
||||
3. 提交(commit)指将未存储的 SQL 语句结果写入数据库表;
|
||||
4. 保留点(savepoint)指事务处理中设置的临时占位符(placeholder),你可以对它发布回退(与回退整个事务处理不同)。
|
||||
- 事务(transaction)指一组 SQL 语句;
|
||||
- 回退(rollback)指撤销指定 SQL 语句的过程;
|
||||
- 提交(commit)指将未存储的 SQL 语句结果写入数据库表;
|
||||
- 保留点(savepoint)指事务处理中设置的临时占位符(placeholder),你可以对它发布回退(与回退整个事务处理不同)。
|
||||
|
||||
不能回退 SELECT 语句,回退 SELECT 语句也没意义;也不能回退 CREATE 和 DROP 语句。
|
||||
|
||||
|
@ -659,9 +660,9 @@ COMMIT
|
|||
|
||||
基本术语:
|
||||
|
||||
1. 字符集为字母和符号的集合;
|
||||
2. 编码为某个字符集成员的内部表示;
|
||||
3. 校对字符指定如何比较,主要用于排序和分组。
|
||||
- 字符集为字母和符号的集合;
|
||||
- 编码为某个字符集成员的内部表示;
|
||||
- 校对字符指定如何比较,主要用于排序和分组。
|
||||
|
||||
除了给表指定字符集和校对外,也可以给列指定:
|
||||
|
||||
|
|
|
@ -68,7 +68,7 @@
|
|||
|
||||
## AUTOCOMMIT
|
||||
|
||||
MySQL 默认采用自动提交模式。也就是说,如果不显示使用`START TRANSACTION`语句来开始一个事务,那么每个查询都会被当做一个事务自动提交。
|
||||
MySQL 默认采用自动提交模式。也就是说,如果不显式使用`START TRANSACTION`语句来开始一个事务,那么每个查询都会被当做一个事务自动提交。
|
||||
|
||||
# 二、并发一致性问题
|
||||
|
||||
|
@ -131,8 +131,8 @@ MySQL 中提供了两种封锁粒度:行级锁以及表级锁。
|
|||
|
||||
有以下两个规定:
|
||||
|
||||
1. 一个事务对数据对象 A 加了 X 锁,就可以对 A 进行读取和更新。加锁期间其它事务不能对 A 加任何锁。
|
||||
2. 一个事务对数据对象 A 加了 S 锁,可以对 A 进行读取操作,但是不能进行更新操作。加锁期间其它事务能对 A 加 S 锁,但是不能加 X 锁。
|
||||
- 一个事务对数据对象 A 加了 X 锁,就可以对 A 进行读取和更新。加锁期间其它事务不能对 A 加任何锁。
|
||||
- 一个事务对数据对象 A 加了 S 锁,可以对 A 进行读取操作,但是不能进行更新操作。加锁期间其它事务能对 A 加 S 锁,但是不能加 X 锁。
|
||||
|
||||
锁的兼容关系如下:
|
||||
|
||||
|
@ -149,8 +149,8 @@ MySQL 中提供了两种封锁粒度:行级锁以及表级锁。
|
|||
|
||||
意向锁在原来的 X/S 锁之上引入了 IX/IS,IX/IS 都是表锁,用来表示一个事务想要在表中的某个数据行上加 X 锁或 S 锁。有以下两个规定:
|
||||
|
||||
1. 一个事务在获得某个数据行对象的 S 锁之前,必须先获得表的 IS 锁或者更强的锁;
|
||||
2. 一个事务在获得某个数据行对象的 X 锁之前,必须先获得表的 IX 锁。
|
||||
- 一个事务在获得某个数据行对象的 S 锁之前,必须先获得表的 IS 锁或者更强的锁;
|
||||
- 一个事务在获得某个数据行对象的 X 锁之前,必须先获得表的 IX 锁。
|
||||
|
||||
通过引入意向锁,事务 T 想要对表 A 加 X 锁,只需要先检测是否有其它事务对表 A 加了 X/IX/S/IS 锁,如果加了就表示有其它事务正在使用这个表或者表中某一行的锁,因此事务 T 加 X 锁失败。
|
||||
|
||||
|
@ -319,7 +319,7 @@ InnoDB 的 MVCC 使用到的快照存储在 Undo 日志中,该日志通过回
|
|||
|
||||
多个事务必须读取到同一个数据行的快照,并且这个快照是距离现在最近的一个有效快照。但是也有例外,如果有一个事务正在修改该数据行,那么它可以读取事务本身所做的修改,而不用和其它事务的读取结果一致。
|
||||
|
||||
把没对一个数据行做修改的事务称为 T,T 所要读取的数据行快照的创建版本号必须小于 T 的版本号,因为如果大于或者等于 T 的版本号,那么表示该数据行快照是其它事务的最新修改,因此不能去读取它。
|
||||
把没有对一个数据行做修改的事务称为 T,T 所要读取的数据行快照的创建版本号必须小于 T 的版本号,因为如果大于或者等于 T 的版本号,那么表示该数据行快照是其它事务的最新修改,因此不能去读取它。
|
||||
|
||||
除了上面的要求,T 所要读取的数据行快照的删除版本号必须大于 T 的版本号,因为如果小于等于 T 的版本号,那么表示该数据行快照是已经被删除的,不应该去读取它。
|
||||
|
||||
|
@ -339,7 +339,7 @@ InnoDB 的 MVCC 使用到的快照存储在 Undo 日志中,该日志通过回
|
|||
|
||||
### 1. 快照读
|
||||
|
||||
这是 MVCC 的一种方式,读取的是快照中的数据,可以减少加锁所带来的开销。
|
||||
使用 MVCC 读取的是快照中的数据,这样可以减少加锁所带来的开销。
|
||||
|
||||
```sql
|
||||
select * from table ...;
|
||||
|
@ -418,13 +418,14 @@ SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;
|
|||
| 1 | 学生-1 | 学院-1 | 院长-1 | 课程-1 | 90 |
|
||||
| 2 | 学生-2 | 学院-2 | 院长-2 | 课程-2 | 80 |
|
||||
| 2 | 学生-2 | 学院-2 | 院长-2 | 课程-1 | 100 |
|
||||
| 3 | 学生-3 | 学院-2 | 院长-2 | 课程-2 | 95 |
|
||||
|
||||
不符合范式的关系,会产生很多异常,主要有以下四种异常:
|
||||
|
||||
1. 冗余数据:例如 学生-2 出现了两次。
|
||||
2. 修改异常:修改了一个记录中的信息,但是另一个记录中相同的信息却没有被修改。
|
||||
3. 删除异常:删除一个信息,那么也会丢失其它信息。例如如果删除了 课程-1,需要删除第一行和第三行,那么 学生-1 的信息就会丢失。
|
||||
4. 插入异常,例如想要插入一个学生的信息,如果这个学生还没选课,那么就无法插入。
|
||||
- 冗余数据:例如 学生-2 出现了两次。
|
||||
- 修改异常:修改了一个记录中的信息,但是另一个记录中相同的信息却没有被修改。
|
||||
- 删除异常:删除一个信息,那么也会丢失其它信息。例如如果删除了 课程-1,需要删除第一行和第三行,那么 学生-1 的信息就会丢失。
|
||||
- 插入异常,例如想要插入一个学生的信息,如果这个学生还没选课,那么就无法插入。
|
||||
|
||||
## 范式
|
||||
|
||||
|
@ -449,6 +450,7 @@ SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;
|
|||
| 1 | 学生-1 | 学院-1 | 院长-1 | 课程-1 | 90 |
|
||||
| 2 | 学生-2 | 学院-2 | 院长-2 | 课程-2 | 80 |
|
||||
| 2 | 学生-2 | 学院-2 | 院长-2 | 课程-1 | 100 |
|
||||
| 3 | 学生-3 | 学院-2 | 院长-2 | 课程-2 | 95 |
|
||||
|
||||
以上学生课程关系中,{Sno, Cname} 为键码,有如下函数依赖:
|
||||
|
||||
|
@ -468,6 +470,7 @@ Sname, Sdept 和 Mname 都部分依赖于键码,当一个学生选修了多门
|
|||
| :---: | :---: | :---: | :---: |
|
||||
| 1 | 学生-1 | 学院-1 | 院长-1 |
|
||||
| 2 | 学生-2 | 学院-2 | 院长-2 |
|
||||
| 3 | 学生-3 | 学院-2 | 院长-2 |
|
||||
|
||||
有以下函数依赖:
|
||||
|
||||
|
@ -481,6 +484,7 @@ Sname, Sdept 和 Mname 都部分依赖于键码,当一个学生选修了多门
|
|||
| 1 | 课程-1 | 90 |
|
||||
| 2 | 课程-2 | 80 |
|
||||
| 2 | 课程-1 | 100 |
|
||||
| 3 | 课程-2 | 95 |
|
||||
|
||||
有以下函数依赖:
|
||||
|
||||
|
@ -498,6 +502,7 @@ Sname, Sdept 和 Mname 都部分依赖于键码,当一个学生选修了多门
|
|||
| :---: | :---: | :---: |
|
||||
| 1 | 学生-1 | 学院-1 |
|
||||
| 2 | 学生-2 | 学院-2 |
|
||||
| 3 | 学生-3 | 学院-2 |
|
||||
|
||||
关系-12
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ My **name** is Zheng.
|
|||
|
||||
**-** 元字符只有在 [ ] 之间才是元字符,在 [ ] 之外就是一个普通字符;
|
||||
|
||||
**^** 是取非操作,必须在 [ ] 字符集合中使用;
|
||||
**^** 在 [ ] 字符集合中是取非操作。
|
||||
|
||||
**应用**
|
||||
|
||||
|
@ -131,7 +131,7 @@ abc[^0-9]
|
|||
|
||||
```
|
||||
[\w.]+@\w+\.\w+
|
||||
[\w.]+@[\w]+\.[\w]+
|
||||
[\w.]+@[\w]+[\.][\w]+
|
||||
```
|
||||
|
||||
**{n}** 匹配 n 个字符,**{m, n}** 匹配 m\~n 个字符,**{m,}** 至少匹配 m 个字符;
|
||||
|
|
|
@ -725,17 +725,18 @@ Linux 中管道通过空文件实现。
|
|||
|
||||
> NRU, Not Recently Used
|
||||
|
||||
首先,系统为毎一页面设置了两个状态位。当页面被访问(读或写)时设置R位;当页面(即修改页面)被写入时设置M位。当启动一个进程时,它的所有页面的两个位都由操作系统设置成0,R位被定期地(比如在每次时钟中断时)清零,以区别最近没有被访问的页面和被访问的页面。
|
||||
首先,系统为毎一页面设置了两个状态位。当页面被访问 (读或写) 时设置 R 位; 当页面 (即修改页面) 被写入时设置 M 位。当启动一个进程时,它的所有页面的两个位都由操作系统设置成 0,R 位被定期地 (比如在每次时钟中断时) 清零,以区别最近没有被访问的页面和被访问的页面。
|
||||
|
||||
当发生缺页中断时,操作系统检査所有的页面并根据它们当前的R位和M位的值,把它们分为4类:
|
||||
* 第0类:没有被访问,没有被修改
|
||||
* 第1类:没有被访问,已被修改
|
||||
* 第2类:已被访问,没有被修改
|
||||
* 第3类:已被访问,已被修改
|
||||
当发生缺页中断时,操作系统检査所有的页面并根据它们当前的 R 位和 M 位的值,把它们分为 4 类:
|
||||
|
||||
NRU算法随机地从类编号最小的非空类中挑选一个页面淘汰之。
|
||||
* 第 0 类: 没有被访问,没有被修改
|
||||
* 第 1 类: 没有被访问,已被修改
|
||||
* 第 2 类: 已被访问,没有被修改
|
||||
* 第 3 类: 已被访问,已被修改
|
||||
|
||||
算法隐含的意思是,在最近一个时钟滴答中(典型的时间是大约20ms)淘汰一个没有被访问的已修改页面要比一个被频繁使用的“十净”页面好。NRU主要优点是易于理解和能够有效地被实现,虽然它的性能不是最好的,但是已经够用了。
|
||||
NRU 算法随机地从类编号最小的非空类中挑选一个页面淘汰之。
|
||||
|
||||
算法隐含的意思是,在最近一个时钟滴答中 (典型的时间是大约 20ms) 淘汰一个没有被访问的已修改页面要比一个被频繁使用的 “十净” 页面好。NRU 主要优点是易于理解和能够有效地被实现,虽然它的性能不是最好的,但是已经够用了。
|
||||
|
||||
### 4. 最近最久未使用
|
||||
|
||||
|
@ -751,13 +752,13 @@ NRU算法随机地从类编号最小的非空类中挑选一个页面淘汰之
|
|||
|
||||
### 5. 第二次机会算法
|
||||
|
||||
FIFO算法可能会把经常使用的页面置换出去,为了避免这一问题,对该算法做一个简单的修改:
|
||||
FIFO 算法可能会把经常使用的页面置换出去,为了避免这一问题,对该算法做一个简单的修改:
|
||||
|
||||
当页面被访问(读或写)时设置该页面的R位为1。需要替换的时候,检查最老页面的R位。如果R位是0,那么这个页面既老又没有被使用,可以立刻置换掉;如果是1,就将R位清0,并把该页面放到链表的尾端,修改它的装入时间使它就像刚装入的一样,然后继续从链表的头部开始搜索
|
||||
当页面被访问 (读或写) 时设置该页面的 R 位为 1。需要替换的时候,检查最老页面的 R 位。如果 R 位是 0,那么这个页面既老又没有被使用,可以立刻置换掉; 如果是 1,就将 R 位清 0,并把该页面放到链表的尾端,修改它的装入时间使它就像刚装入的一样,然后继续从链表的头部开始搜索
|
||||
|
||||
<div align="center"> <img src="../pics//2018-05-29-15275543393037.jpg"/> </div><br>
|
||||
<div align="center"> <img src="../pics//ecf8ad5d-5403-48b9-b6e7-f2e20ffe8fca.png"/> </div><br>
|
||||
|
||||
第二次机会算法就是寻找一个最近的时钟间隔以来没有被访问过的页面。如果所有的页面都被访问过了,该算法就简化为纯粹的FIFO算法。
|
||||
第二次机会算法就是寻找一个最近的时钟间隔以来没有被访问过的页面。如果所有的页面都被访问过了,该算法就简化为纯粹的 FIFO 算法。
|
||||
|
||||
### 6. 时钟
|
||||
|
||||
|
|
|
@ -78,9 +78,9 @@
|
|||
|
||||
## 主机之间的通信方式
|
||||
|
||||
1. 客户-服务器(C/S):客户是服务的请求方,服务器是服务的提供方。
|
||||
- 客户-服务器(C/S):客户是服务的请求方,服务器是服务的提供方。
|
||||
|
||||
2. 对等(P2P):不区分客户和服务器。
|
||||
- 对等(P2P):不区分客户和服务器。
|
||||
|
||||
<div align="center"> <img src="../pics//2ad244f5-939c-49fa-9385-69bc688677ab.jpg" width=""/> </div><br>
|
||||
|
||||
|
@ -102,7 +102,7 @@
|
|||
|
||||
分组交换也使用了存储转发,但是转发的是分组而不是报文。把整块数据称为一个报文,由于一个报文可能很长,需要先进行切分,来满足分组能处理的大小。在每个切分的数据前面加上首部之后就成为了分组,首部包含了目的地址和源地址等控制信息。
|
||||
|
||||
存储转发允许在一条传输线路上传送多个主机的分组,也就是说两个用户之间的通信不需要占用端到端的线路资源。
|
||||
分组交换允许在一条传输线路上传送多个主机的分组,也就是说两个用户之间的通信不需要占用端到端的线路资源。
|
||||
|
||||
相比于报文交换,由于分组比报文更小,因此分组交换的存储转发速度更加快速。
|
||||
|
||||
|
|
BIN
pics/9ea86eb5-000a-4281-b948-7b567bd6f1d8.png
Normal file
BIN
pics/9ea86eb5-000a-4281-b948-7b567bd6f1d8.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
BIN
pics/ecf8ad5d-5403-48b9-b6e7-f2e20ffe8fca.png
Normal file
BIN
pics/ecf8ad5d-5403-48b9-b6e7-f2e20ffe8fca.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
Loading…
Reference in New Issue
Block a user