auto commit
This commit is contained in:
parent
6d0d5cb163
commit
8dfa8ab7d2
|
@ -1177,7 +1177,6 @@ Java 注解是附加在代码中的一些元信息,用于一些工具在编译
|
||||||
- Java 支持自动垃圾回收,而 C++ 需要手动回收。
|
- Java 支持自动垃圾回收,而 C++ 需要手动回收。
|
||||||
- Java 不支持多重继承,只能通过实现多个接口来达到相同目的,而 C++ 支持多重继承。
|
- Java 不支持多重继承,只能通过实现多个接口来达到相同目的,而 C++ 支持多重继承。
|
||||||
- Java 不支持操作符重载,虽然可以对两个 String 对象支持加法运算,但是这是语言内置支持的操作,不属于操作符重载,而 C++ 可以。
|
- Java 不支持操作符重载,虽然可以对两个 String 对象支持加法运算,但是这是语言内置支持的操作,不属于操作符重载,而 C++ 可以。
|
||||||
- Java 内置了线程的支持,而 C++ 需要依靠第三方库。
|
|
||||||
- Java 的 goto 是保留字,但是不可用,C++ 可以使用 goto。
|
- Java 的 goto 是保留字,但是不可用,C++ 可以使用 goto。
|
||||||
- Java 不支持条件编译,C++ 通过 #ifdef #ifndef 等预处理命令从而实现条件编译。
|
- Java 不支持条件编译,C++ 通过 #ifdef #ifndef 等预处理命令从而实现条件编译。
|
||||||
|
|
||||||
|
|
|
@ -499,7 +499,7 @@ public synchronized static void fun() {
|
||||||
|
|
||||||
## ReentrantLock
|
## ReentrantLock
|
||||||
|
|
||||||
ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁.
|
ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁。
|
||||||
|
|
||||||
```java
|
```java
|
||||||
public class LockExample {
|
public class LockExample {
|
||||||
|
@ -952,7 +952,7 @@ produce..produce..consume..consume..produce..consume..produce..consume..produce.
|
||||||
|
|
||||||
```java
|
```java
|
||||||
public class ForkJoinExample extends RecursiveTask<Integer> {
|
public class ForkJoinExample extends RecursiveTask<Integer> {
|
||||||
private final int threhold = 5;
|
private final int threshold = 5;
|
||||||
private int first;
|
private int first;
|
||||||
private int last;
|
private int last;
|
||||||
|
|
||||||
|
@ -964,7 +964,7 @@ public class ForkJoinExample extends RecursiveTask<Integer> {
|
||||||
@Override
|
@Override
|
||||||
protected Integer compute() {
|
protected Integer compute() {
|
||||||
int result = 0;
|
int result = 0;
|
||||||
if (last - first <= threhold) {
|
if (last - first <= threshold) {
|
||||||
// 任务足够小则直接计算
|
// 任务足够小则直接计算
|
||||||
for (int i = first; i <= last; i++) {
|
for (int i = first; i <= last; i++) {
|
||||||
result += i;
|
result += i;
|
||||||
|
@ -1134,7 +1134,7 @@ public static void main(String[] args) throws InterruptedException {
|
||||||
1000
|
1000
|
||||||
```
|
```
|
||||||
|
|
||||||
除了使用原子类之外,也可以使用 synchronized 互斥锁来保证操作的完整性,它对应的内存间交互操作为:lock 和 unlock,在虚拟机实现上对应的字节码指令为 monitorenter 和 monitorexit。
|
除了使用原子类之外,也可以使用 synchronized 互斥锁来保证操作的原子性。它对应的内存间交互操作为:lock 和 unlock,在虚拟机实现上对应的字节码指令为 monitorenter 和 monitorexit。
|
||||||
|
|
||||||
```java
|
```java
|
||||||
public class AtomicSynchronizedExample {
|
public class AtomicSynchronizedExample {
|
||||||
|
@ -1176,9 +1176,13 @@ public static void main(String[] args) throws InterruptedException {
|
||||||
|
|
||||||
可见性指当一个线程修改了共享变量的值,其它线程能够立即得知这个修改。Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性的。
|
可见性指当一个线程修改了共享变量的值,其它线程能够立即得知这个修改。Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性的。
|
||||||
|
|
||||||
volatile 可保证可见性。synchronized 也能够保证可见性,对一个变量执行 unlock 操作之前,必须把变量值同步回主内存。final 关键字也能保证可见性:被 final 关键字修饰的字段在构造器中一旦初始化完成,并且没有发生 this 逃逸(其它线程可以通过 this 引用访问到初始化了一半的对象),那么其它线程就能看见 final 字段的值。
|
主要有有三种实现可见性的方式:
|
||||||
|
|
||||||
对前面的线程不安全示例中的 cnt 变量用 volatile 修饰,不能解决线程不安全问题,因为 volatile 并不能保证操作的原子性。
|
- volatile
|
||||||
|
- synchronized,对一个变量执行 unlock 操作之前,必须把变量值同步回主内存。
|
||||||
|
- final,被 final 关键字修饰的字段在构造器中一旦初始化完成,并且没有发生 this 逃逸(其它线程通过 this 引用访问到初始化了一半的对象),那么其它线程就能看见 final 字段的值。
|
||||||
|
|
||||||
|
对前面的线程不安全示例中的 cnt 变量使用 volatile 修饰,不能解决线程不安全问题,因为 volatile 并不能保证操作的原子性。
|
||||||
|
|
||||||
### 3. 有序性
|
### 3. 有序性
|
||||||
|
|
||||||
|
@ -1662,9 +1666,9 @@ JDK 1.6 引入了偏向锁和轻量级锁,从而让锁拥有了四个状态:
|
||||||
|
|
||||||
- 缩小同步范围,例如对于 synchronized,应该尽量使用同步块而不是同步方法。
|
- 缩小同步范围,例如对于 synchronized,应该尽量使用同步块而不是同步方法。
|
||||||
|
|
||||||
- 多用同步类少用 wait() 和 notify()。首先,CountDownLatch, Semaphore, CyclicBarrier 和 Exchanger 这些同步类简化了编码操作,而用 wait() 和 notify() 很难实现对复杂控制流的控制。其次,这些类是由最好的企业编写和维护,在后续的 JDK 中它们还会不断优化和完善,使用这些更高等级的同步工具你的程序可以不费吹灰之力获得优化。
|
- 多用同步类少用 wait() 和 notify()。首先,CountDownLatch, CyclicBarrier, Semaphore 和 Exchanger 这些同步类简化了编码操作,而用 wait() 和 notify() 很难实现对复杂的控制流;其次,这些同步类是由最好的企业编写和维护,在后续的 JDK 中还会不断优化和完善,使用这些更高等级的同步工具你的程序可以不费吹灰之力获得优化。
|
||||||
|
|
||||||
- 多用并发集合少用同步集合。并发集合比同步集合的可扩展性更好,例如应该使用 ConcurrentHashMap 而不是 Hashtable。
|
- 多用并发集合少用同步集合,例如应该使用 ConcurrentHashMap 而不是 Hashtable。
|
||||||
|
|
||||||
- 使用本地变量和不可变类来保证线程安全。
|
- 使用本地变量和不可变类来保证线程安全。
|
||||||
|
|
||||||
|
|
|
@ -871,7 +871,9 @@ $ ls -al /etc | less
|
||||||
|
|
||||||
## 提取指令
|
## 提取指令
|
||||||
|
|
||||||
cut 对数据进行切分,取出想要的部分。切分过程一行一行地进行。
|
cut 对数据进行切分,取出想要的部分。
|
||||||
|
|
||||||
|
切分过程一行一行地进行。
|
||||||
|
|
||||||
```html
|
```html
|
||||||
$ cut
|
$ cut
|
||||||
|
@ -891,7 +893,7 @@ root pts/1 192.168.201.254 Thu Feb 5 22:37 - 23:53 (01:16)
|
||||||
$ last | cut -d ' ' -f 1
|
$ last | cut -d ' ' -f 1
|
||||||
```
|
```
|
||||||
|
|
||||||
示例 2:将 export 输出的讯息,取出第 12 字符以后的所有字符串。
|
示例 2:将 export 输出的信息,取出第 12 字符以后的所有字符串。
|
||||||
|
|
||||||
```html
|
```html
|
||||||
$ export
|
$ export
|
||||||
|
@ -906,7 +908,7 @@ $ export | cut -c 12
|
||||||
|
|
||||||
## 排序指令
|
## 排序指令
|
||||||
|
|
||||||
**sort** 进行排序。
|
**sort** 用于排序。
|
||||||
|
|
||||||
```html
|
```html
|
||||||
$ sort [-fbMnrtuk] [file or stdin]
|
$ sort [-fbMnrtuk] [file or stdin]
|
||||||
|
@ -1204,7 +1206,7 @@ pid_t wait(int *status)
|
||||||
|
|
||||||
如果成功,返回被收集的子进程的进程 ID;如果调用进程没有子进程,调用就会失败,此时返回 -1,同时 errno 被置为 ECHILD。
|
如果成功,返回被收集的子进程的进程 ID;如果调用进程没有子进程,调用就会失败,此时返回 -1,同时 errno 被置为 ECHILD。
|
||||||
|
|
||||||
参数 status 用来保存被收集的子进程退出时的一些状态,如果我们对这个子进程是如何死掉的毫不在意,只想把这个子进程消灭掉,可以设置这个参数为 NULL:
|
参数 status 用来保存被收集的子进程退出时的一些状态,如果对这个子进程是如何死掉的毫不在意,只想把这个子进程消灭掉,可以设置这个参数为 NULL:
|
||||||
|
|
||||||
```c
|
```c
|
||||||
pid = wait(NULL);
|
pid = wait(NULL);
|
||||||
|
@ -1218,7 +1220,7 @@ pid_t waitpid(pid_t pid, int *status, int options)
|
||||||
|
|
||||||
作用和 wait() 完全相同,但是多了两个可由用户控制的参数 pid 和 options。
|
作用和 wait() 完全相同,但是多了两个可由用户控制的参数 pid 和 options。
|
||||||
|
|
||||||
pid 参数指示一个子进程的 ID,表示只关心这个子进程的退出 SIGCHLD 信号。如果 pid=-1 时,那么和 wait() 作用相同,都是关心所有子进程退出的 SIGCHLD 信号。
|
pid 参数指示一个子进程的 ID,表示只关心这个子进程退出的 SIGCHLD 信号。如果 pid=-1 时,那么和 wait() 作用相同,都是关心所有子进程退出的 SIGCHLD 信号。
|
||||||
|
|
||||||
options 参数主要有 WNOHANG 和 WUNTRACED 两个选项,WNOHANG 可以使 waitpid() 调用变成非阻塞的,也就是说它会立即返回,父进程可以继续执行其它任务。
|
options 参数主要有 WNOHANG 和 WUNTRACED 两个选项,WNOHANG 可以使 waitpid() 调用变成非阻塞的,也就是说它会立即返回,父进程可以继续执行其它任务。
|
||||||
|
|
||||||
|
@ -1238,7 +1240,7 @@ options 参数主要有 WNOHANG 和 WUNTRACED 两个选项,WNOHANG 可以使 w
|
||||||
|
|
||||||
系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程。
|
系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程。
|
||||||
|
|
||||||
要消灭系统中大量的僵尸进程,只需要将其父进程杀死,此时所有的僵尸进程就会变成孤儿进程,从而被 init 所收养,这样 init 就会释放所有的僵死进程所占有的资源,从而结束僵尸进程。
|
要消灭系统中大量的僵尸进程,只需要将其父进程杀死,此时僵尸进程就会变成孤儿进程,从而被 init 所收养,这样 init 就会释放所有的僵死进程所占有的资源,从而结束僵尸进程。
|
||||||
|
|
||||||
# 参考资料
|
# 参考资料
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
* [六、键的过期时间](#六键的过期时间)
|
* [六、键的过期时间](#六键的过期时间)
|
||||||
* [七、数据淘汰策略](#七数据淘汰策略)
|
* [七、数据淘汰策略](#七数据淘汰策略)
|
||||||
* [八、持久化](#八持久化)
|
* [八、持久化](#八持久化)
|
||||||
* [快照持久化](#快照持久化)
|
* [RDB 持久化](#rdb-持久化)
|
||||||
* [AOF 持久化](#aof-持久化)
|
* [AOF 持久化](#aof-持久化)
|
||||||
* [九、发布与订阅](#九发布与订阅)
|
* [九、发布与订阅](#九发布与订阅)
|
||||||
* [十、事务](#十事务)
|
* [十、事务](#十事务)
|
||||||
|
@ -210,17 +210,7 @@ OK
|
||||||
|
|
||||||
## 字典
|
## 字典
|
||||||
|
|
||||||
以下是 Redis 字典的主要数据结构,从上往下分析,一个 dict 有两个 dictht,一个 dictht 有一个 dictEntry 数组,每个 dictEntry 有 next 指针因此是一个链表结构。从上面的分析可以看出 Redis 的字典是一个基于拉链法解决冲突的哈希表结构。
|
dictht 是一个散列表结构,使用拉链法保存哈希冲突的 dictEntry。
|
||||||
|
|
||||||
```c
|
|
||||||
typedef struct dict {
|
|
||||||
dictType *type;
|
|
||||||
void *privdata;
|
|
||||||
dictht ht[2];
|
|
||||||
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
|
|
||||||
unsigned long iterators; /* number of iterators currently running */
|
|
||||||
} dict;
|
|
||||||
```
|
|
||||||
|
|
||||||
```c
|
```c
|
||||||
/* This is our hash table structure. Every dictionary has two of this as we
|
/* This is our hash table structure. Every dictionary has two of this as we
|
||||||
|
@ -246,11 +236,21 @@ typedef struct dictEntry {
|
||||||
} dictEntry;
|
} dictEntry;
|
||||||
```
|
```
|
||||||
|
|
||||||
哈希表需要具备扩容能力,在扩容时就需要对每个键值对进行 rehash。dict 有两个 dictht,在 rehash 的时候会将一个 dictht 上的键值对重新插入另一个 dictht 上面,完成之后释放空间并交换两个 dictht 的角色。
|
Redis 的字典 dict 中包含两个哈希表 dictht,这是为了方便进行 rehash 操作。在扩容时,将其中一个 dictht 上的键值对 rehash 到另一个 dictht 上面,完成之后释放空间并交换两个 dictht 的角色。
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef struct dict {
|
||||||
|
dictType *type;
|
||||||
|
void *privdata;
|
||||||
|
dictht ht[2];
|
||||||
|
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
|
||||||
|
unsigned long iterators; /* number of iterators currently running */
|
||||||
|
} dict;
|
||||||
|
```
|
||||||
|
|
||||||
rehash 操作不是一次性完成,而是采用渐进方式,这是为了避免一次性执行过多的 rehash 操作给服务器带来过大的负担。
|
rehash 操作不是一次性完成,而是采用渐进方式,这是为了避免一次性执行过多的 rehash 操作给服务器带来过大的负担。
|
||||||
|
|
||||||
渐进式 rehash 通过记录 dict 的 rehashidx 完成,它从 0 开始然后每执行一次 rehash 都会递增。例如在一次 rehash 中,要把 dict[0] rehash 到 dict[1],这一次会把 dict[0] 上 table[rehashidx] 的键值对 rehash 到 dict[1] 上,dict[0] 的 table[rehashidx] 指向 null,并令 rehashidx++。
|
渐进式 rehash 通过记录 dict 的 rehashidx 完成,它从 0 开始,然后每执行一次 rehash 都会递增。例如在一次 rehash 中,要把 dict[0] rehash 到 dict[1],这一次会把 dict[0] 上 table[rehashidx] 的键值对 rehash 到 dict[1] 上,dict[0] 的 table[rehashidx] 指向 null,并令 rehashidx++。
|
||||||
|
|
||||||
在 rehash 期间,每次对字典执行添加、删除、查找或者更新操作时,都会执行一次渐进式 rehash。
|
在 rehash 期间,每次对字典执行添加、删除、查找或者更新操作时,都会执行一次渐进式 rehash。
|
||||||
|
|
||||||
|
@ -320,13 +320,13 @@ int dictRehash(dict *d, int n) {
|
||||||
|
|
||||||
<div align="center"> <img src="../pics//beba612e-dc5b-4fc2-869d-0b23408ac90a.png"/> </div><br>
|
<div align="center"> <img src="../pics//beba612e-dc5b-4fc2-869d-0b23408ac90a.png"/> </div><br>
|
||||||
|
|
||||||
在查找时,从上层指针开始查找,找到对应的区间之后再到下一层去查找。例如下图演示了查找 22 的过程。
|
在查找时,从上层指针开始查找,找到对应的区间之后再到下一层去查找。下图演示了查找 22 的过程。
|
||||||
|
|
||||||
<div align="center"> <img src="../pics//0ea37ee2-c224-4c79-b895-e131c6805c40.png"/> </div><br>
|
<div align="center"> <img src="../pics//0ea37ee2-c224-4c79-b895-e131c6805c40.png"/> </div><br>
|
||||||
|
|
||||||
与红黑树等平衡树相比,跳跃表具有以下优点:
|
与红黑树等平衡树相比,跳跃表具有以下优点:
|
||||||
|
|
||||||
- 插入速度非常快速,因为不需要平衡树的旋转操作;
|
- 插入速度非常快速,因为不需要进行旋转等操作来维护平衡性;
|
||||||
- 更容易实现;
|
- 更容易实现;
|
||||||
- 支持无锁操作。
|
- 支持无锁操作。
|
||||||
|
|
||||||
|
@ -336,7 +336,7 @@ int dictRehash(dict *d, int n) {
|
||||||
|
|
||||||
可以对 String 进行自增自减运算,从而实现计数器功能。
|
可以对 String 进行自增自减运算,从而实现计数器功能。
|
||||||
|
|
||||||
例如对于网站访问量,如果使用 MySQL 数据库进行存储,那么每访问一次网站就要对磁盘进行读写操作。而对 Redis 这种内存型数据库的读写性能非常高,很适合存储这种频繁读写的计数量。
|
Redis 这种内存型数据库的读写性能非常高,很适合存储频繁读写的计数量。
|
||||||
|
|
||||||
## 缓存
|
## 缓存
|
||||||
|
|
||||||
|
@ -346,7 +346,7 @@ int dictRehash(dict *d, int n) {
|
||||||
|
|
||||||
例如 DNS 记录就很适合使用 Redis 进行存储。
|
例如 DNS 记录就很适合使用 Redis 进行存储。
|
||||||
|
|
||||||
查找表和缓存类似,也是利用了 Redis 快速的查找特性。但是查找表的内容不能失效,而缓存的内容可以失效。
|
查找表和缓存类似,也是利用了 Redis 快速的查找特性。但是查找表的内容不能失效,而缓存的内容可以失效,因为缓存不作为可靠的数据来源。
|
||||||
|
|
||||||
## 消息队列
|
## 消息队列
|
||||||
|
|
||||||
|
@ -356,27 +356,29 @@ List 是一个双向链表,可以通过 lpop 和 lpush 写入和读取消息
|
||||||
|
|
||||||
## 会话缓存
|
## 会话缓存
|
||||||
|
|
||||||
在分布式场景下具有多个应用服务器,可以使用 Redis 来统一存储这些应用服务器的会话信息,使得某个应用服务器宕机时不会丢失会话信息,从而保证高可用。
|
在分布式场景下具有多个应用服务器,可以使用 Redis 来统一存储这些应用服务器的会话信息。
|
||||||
|
|
||||||
|
当应用服务器不再存储用户的会话信息,也就不再具有状态,一个用户可以请求任意一个应用服务器。
|
||||||
|
|
||||||
## 分布式锁实现
|
## 分布式锁实现
|
||||||
|
|
||||||
在分布式场景下,无法使用单机环境下的锁实现。当多个节点上的进程都需要获取同一个锁时,就需要使用分布式锁来进行同步。
|
在分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步。
|
||||||
|
|
||||||
除了可以使用 Redis 自带的 SETNX 命令实现分布式锁之外,还可以使用官方提供的 RedLock 分布式锁实现。
|
可以使用 Reids 自带的 SETNX 命令实现分布式锁,除此之外,还可以使用官方提供的 RedLock 分布式锁实现。
|
||||||
|
|
||||||
## 其它
|
## 其它
|
||||||
|
|
||||||
Set 可以实现交集、并集等操作,例如共同好友功能。
|
Set 可以实现交集、并集等操作,从而实现共同好友等功能。
|
||||||
|
|
||||||
ZSet 可以实现有序性操作,例如排行榜功能。
|
ZSet 可以实现有序性操作,从而实现排行榜等功能。
|
||||||
|
|
||||||
# 五、Redis 与 Memcached
|
# 五、Redis 与 Memcached
|
||||||
|
|
||||||
两者都是非关系型内存键值数据库。有以下主要不同:
|
两者都是非关系型内存键值数据库,主要有以下不同:
|
||||||
|
|
||||||
## 数据类型
|
## 数据类型
|
||||||
|
|
||||||
Memcached 仅支持字符串类型,而 Redis 支持五种不同种类的数据类型,使得它可以更灵活地解决问题。
|
Memcached 仅支持字符串类型,而 Redis 支持五种不同的数据类型,可以更灵活地解决问题。
|
||||||
|
|
||||||
## 数据持久化
|
## 数据持久化
|
||||||
|
|
||||||
|
@ -384,15 +386,15 @@ Redis 支持两种持久化策略:RDB 快照和 AOF 日志,而 Memcached 不
|
||||||
|
|
||||||
## 分布式
|
## 分布式
|
||||||
|
|
||||||
Memcached 不支持分布式,只能通过在客户端使用一致性哈希这样的分布式算法来实现分布式存储,这种方式在存储和查询时都需要先在客户端计算一次数据所在的节点。
|
Memcached 不支持分布式,只能通过在客户端使用一致性哈希来实现分布式存储,这种方式在存储和查询时都需要先在客户端计算一次数据所在的节点。
|
||||||
|
|
||||||
Redis Cluster 实现了分布式的支持。
|
Redis Cluster 实现了分布式的支持。
|
||||||
|
|
||||||
## 内存管理机制
|
## 内存管理机制
|
||||||
|
|
||||||
在 Redis 中,并不是所有数据都一直存储在内存中,可以将一些很久没用的 value 交换到磁盘。而 Memcached 的数据则会一直在内存中。
|
- 在 Redis 中,并不是所有数据都一直存储在内存中,可以将一些很久没用的 value 交换到磁盘,而 Memcached 的数据则会一直在内存中。
|
||||||
|
|
||||||
Memcached 将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题,但是这种方式会使得内存的利用率不高,例如块的大小为 128 bytes,只存储 100 bytes 的数据,那么剩下的 28 bytes 就浪费掉了。
|
- Memcached 将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题,但是这种方式会使得内存的利用率不高,例如块的大小为 128 bytes,只存储 100 bytes 的数据,那么剩下的 28 bytes 就浪费掉了。
|
||||||
|
|
||||||
# 六、键的过期时间
|
# 六、键的过期时间
|
||||||
|
|
||||||
|
@ -402,7 +404,9 @@ Redis 可以为每个键设置过期时间,当键过期时,会自动删除
|
||||||
|
|
||||||
# 七、数据淘汰策略
|
# 七、数据淘汰策略
|
||||||
|
|
||||||
可以设置内存最大使用量,当内存使用量超过时施行淘汰策略,具体有 6 种淘汰策略。
|
可以设置内存最大使用量,当内存使用量超出时,会施行数据淘汰策略。
|
||||||
|
|
||||||
|
Reids 具体有 6 种淘汰策略:
|
||||||
|
|
||||||
| 策略 | 描述 |
|
| 策略 | 描述 |
|
||||||
| :--: | :--: |
|
| :--: | :--: |
|
||||||
|
@ -413,15 +417,15 @@ Redis 可以为每个键设置过期时间,当键过期时,会自动删除
|
||||||
| allkeys-random | 从所有数据集中任意选择数据进行淘汰 |
|
| allkeys-random | 从所有数据集中任意选择数据进行淘汰 |
|
||||||
| noeviction | 禁止驱逐数据 |
|
| noeviction | 禁止驱逐数据 |
|
||||||
|
|
||||||
如果使用 Redis 来缓存数据时,要保证所有数据都是热点数据,可以将内存最大使用量设置为热点数据占用的内存量,然后启用 allkeys-lru 淘汰策略,将最近最少使用的数据淘汰。
|
作为内存数据库,出于对性能和内存消耗的考虑,Redis 的淘汰算法实际实现上并非针对所有 key,而是抽样一小部分 key 从中选出被淘汰 key。
|
||||||
|
|
||||||
作为内存数据库,出于对性能和内存消耗的考虑,Redis 的淘汰算法(LRU、TTL)实际实现上并非针对所有 key,而是抽样一小部分 key 从中选出被淘汰 key。
|
使用 Redis 缓存数据时,为了提高缓存命中率,需要保证缓存数据都是热点数据。可以将内存最大使用量设置为热点数据占用的内存量,然后启用 allkeys-lru 淘汰策略,将最近最少使用的数据淘汰。
|
||||||
|
|
||||||
# 八、持久化
|
# 八、持久化
|
||||||
|
|
||||||
Redis 是内存型数据库,为了保证数据在断电后不会丢失,需要将内存中的数据持久化到硬盘上。
|
Redis 是内存型数据库,为了保证数据在断电后不会丢失,需要将内存中的数据持久化到硬盘上。
|
||||||
|
|
||||||
## 快照持久化
|
## RDB 持久化
|
||||||
|
|
||||||
将某个时间点的所有数据都存放到硬盘上。
|
将某个时间点的所有数据都存放到硬盘上。
|
||||||
|
|
||||||
|
@ -435,9 +439,7 @@ Redis 是内存型数据库,为了保证数据在断电后不会丢失,需
|
||||||
|
|
||||||
将写命令添加到 AOF 文件(Append Only File)的末尾。
|
将写命令添加到 AOF 文件(Append Only File)的末尾。
|
||||||
|
|
||||||
对硬盘的文件进行写入时,写入的内容首先会被存储到缓冲区,然后由操作系统决定什么时候将该内容同步到硬盘,用户可以调用 file.flush() 方法请求操作系统尽快将缓冲区存储的数据同步到硬盘。可以看出写入文件的数据不会立即同步到硬盘上,在将写命令添加到 AOF 文件时,要根据需求来保证何时同步到硬盘上。
|
使用 AOF 持久化需要设置同步选项,从而确保写命令什么时候会同步到磁盘文件上。这是因为对硬盘的文件进行写入并不会马上将内容同步到磁盘文件上,而是先存储到缓冲区,然后由操作系统决定什么时候同步到硬盘。有以下同步选项:
|
||||||
|
|
||||||
有以下同步选项:
|
|
||||||
|
|
||||||
| 选项 | 同步频率 |
|
| 选项 | 同步频率 |
|
||||||
| :--: | :--: |
|
| :--: | :--: |
|
||||||
|
|
|
@ -90,12 +90,20 @@
|
||||||
|
|
||||||
## 题目描述
|
## 题目描述
|
||||||
|
|
||||||
在一个长度为 n 的数组里的所有数字都在 0 到 n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字是重复的,也不知道每个数字重复几次。请找出数组中任意一个重复的数字。例如,如果输入长度为 7 的数组 {2, 3, 1, 0, 2, 5},那么对应的输出是第一个重复的数字 2。
|
在一个长度为 n 的数组里的所有数字都在 0 到 n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字是重复的,也不知道每个数字重复几次。请找出数组中任意一个重复的数字。
|
||||||
|
|
||||||
要求复杂度为 O(N) + O(1),也就是时间复杂度 O(N),空间复杂度 O(1)。因此不能使用排序的方法,也不能使用额外的标记数组。牛客网讨论区这一题的首票答案使用 nums[i] + length 来将元素标记,这么做会有加法溢出问题。
|
```html
|
||||||
|
Input:
|
||||||
|
{2, 3, 1, 0, 2, 5}
|
||||||
|
|
||||||
|
Output:
|
||||||
|
2
|
||||||
|
```
|
||||||
|
|
||||||
## 解题思路
|
## 解题思路
|
||||||
|
|
||||||
|
要求复杂度为 O(N) + O(1),也就是时间复杂度 O(N),空间复杂度 O(1)。因此不能使用排序的方法,也不能使用额外的标记数组。牛客网讨论区这一题的首票答案使用 nums[i] + length 来将元素标记,这么做会有加法溢出问题。
|
||||||
|
|
||||||
这种数组元素在 [0, n-1] 范围内的问题,可以将值为 i 的元素放到第 i 个位置上。
|
这种数组元素在 [0, n-1] 范围内的问题,可以将值为 i 的元素放到第 i 个位置上。
|
||||||
|
|
||||||
以 (2, 3, 1, 0, 2, 5) 为例:
|
以 (2, 3, 1, 0, 2, 5) 为例:
|
||||||
|
@ -158,7 +166,11 @@ Given target = 20, return false.
|
||||||
|
|
||||||
## 解题思路
|
## 解题思路
|
||||||
|
|
||||||
从右上角开始查找。因为矩阵中的一个数,它左边的数都比它小,下边的数都比它大。因此,从右上角开始查找,就可以根据 target 和当前元素的大小关系来缩小查找区间。
|
从右上角开始查找。矩阵中的一个数,它左边的数都比它小,下边的数都比它大。因此,从右上角开始查找,就可以根据 target 和当前元素的大小关系来缩小查找区间。
|
||||||
|
|
||||||
|
当前元素的查找区间为左下角的所有元素,例如元素 12 的查找区间如下:
|
||||||
|
|
||||||
|
<div align="center"> <img src="../pics//026d3cb4-67f7-4a83-884d-8032f57ec446.png" width="200"/> </div><br>
|
||||||
|
|
||||||
复杂度:O(M + N) + O(1)
|
复杂度:O(M + N) + O(1)
|
||||||
|
|
||||||
|
@ -261,25 +273,14 @@ public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 使用 Collections.reverse()
|
|
||||||
|
|
||||||
```java
|
|
||||||
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
|
|
||||||
ArrayList<Integer> ret = new ArrayList<>();
|
|
||||||
while (listNode != null) {
|
|
||||||
ret.add(listNode.val);
|
|
||||||
listNode = listNode.next;
|
|
||||||
}
|
|
||||||
Collections.reverse(ret);
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 使用头插法
|
### 使用头插法
|
||||||
|
|
||||||
利用链表头插法为逆序的特点。
|
利用链表头插法为逆序的特点。
|
||||||
|
|
||||||
头结点和第一个节点的区别:头结点是在头插法中使用的一个额外节点,这个节点不存储值;第一个节点就是链表的第一个真正存储值的节点。
|
头结点和第一个节点的区别:
|
||||||
|
|
||||||
|
- 头结点是在头插法中使用的一个额外节点,这个节点不存储值;
|
||||||
|
- 第一个节点就是链表的第一个真正存储值的节点。
|
||||||
|
|
||||||
```java
|
```java
|
||||||
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
|
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
|
||||||
|
@ -302,6 +303,20 @@ public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 使用 Collections.reverse()
|
||||||
|
|
||||||
|
```java
|
||||||
|
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
|
||||||
|
ArrayList<Integer> ret = new ArrayList<>();
|
||||||
|
while (listNode != null) {
|
||||||
|
ret.add(listNode.val);
|
||||||
|
listNode = listNode.next;
|
||||||
|
}
|
||||||
|
Collections.reverse(ret);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
# 7. 重建二叉树
|
# 7. 重建二叉树
|
||||||
|
|
||||||
[NowCoder](https://www.nowcoder.com/practice/8a19cbe657394eeaac2f6ea9b0f6fcf6?tpId=13&tqId=11157&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
|
[NowCoder](https://www.nowcoder.com/practice/8a19cbe657394eeaac2f6ea9b0f6fcf6?tpId=13&tqId=11157&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
|
||||||
|
@ -322,22 +337,23 @@ inorder = [9,3,15,20,7]
|
||||||
前序遍历的第一个值为根节点的值,使用这个值将中序遍历结果分成两部分,左部分为树的左子树中序遍历结果,右部分为树的右子树中序遍历的结果。
|
前序遍历的第一个值为根节点的值,使用这个值将中序遍历结果分成两部分,左部分为树的左子树中序遍历结果,右部分为树的右子树中序遍历的结果。
|
||||||
|
|
||||||
```java
|
```java
|
||||||
private Map<Integer, Integer> inOrderNumsIndexs = new HashMap<>(); // 缓存中序遍历数组的每个值对应的索引
|
// 缓存中序遍历数组的每个值对应的索引
|
||||||
|
private Map<Integer, Integer> inOrderNumsIndexs = new HashMap<>();
|
||||||
|
|
||||||
public TreeNode reConstructBinaryTree(int[] pre, int[] in) {
|
public TreeNode reConstructBinaryTree(int[] pre, int[] in) {
|
||||||
for (int i = 0; i < in.length; i++)
|
for (int i = 0; i < in.length; i++)
|
||||||
inOrderNumsIndexs.put(in[i], i);
|
inOrderNumsIndexs.put(in[i], i);
|
||||||
return reConstructBinaryTree(pre, 0, pre.length - 1, in, 0, in.length - 1);
|
return reConstructBinaryTree(pre, 0, pre.length - 1, 0, in.length - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
private TreeNode reConstructBinaryTree(int[] pre, int preL, int preR, int[] in, int inL, int inR) {
|
private TreeNode reConstructBinaryTree(int[] pre, int preL, int preR, int inL, int inR) {
|
||||||
if (preL > preR)
|
if (preL > preR)
|
||||||
return null;
|
return null;
|
||||||
TreeNode root = new TreeNode(pre[preL]);
|
TreeNode root = new TreeNode(pre[preL]);
|
||||||
int inIndex = inOrderNumsIndexs.get(root.val);
|
int inIndex = inOrderNumsIndexs.get(root.val);
|
||||||
int leftTreeSize = inIndex - inL;
|
int leftTreeSize = inIndex - inL;
|
||||||
root.left = reConstructBinaryTree(pre, preL + 1, preL + leftTreeSize, in, inL, inL + leftTreeSize - 1);
|
root.left = reConstructBinaryTree(pre, preL + 1, preL + leftTreeSize, inL, inL + leftTreeSize - 1);
|
||||||
root.right = reConstructBinaryTree(pre, preL + leftTreeSize + 1, preR, in, inL + leftTreeSize + 1, inR);
|
root.right = reConstructBinaryTree(pre, preL + leftTreeSize + 1, preR, inL + leftTreeSize + 1, inR);
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -350,16 +366,6 @@ private TreeNode reConstructBinaryTree(int[] pre, int preL, int preR, int[] in,
|
||||||
|
|
||||||
给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。
|
给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。
|
||||||
|
|
||||||
## 解题思路
|
|
||||||
|
|
||||||
① 如果一个节点的右子树不为空,那么该节点的下一个节点是右子树的最左节点;
|
|
||||||
|
|
||||||
<div align="center"> <img src="../pics//cb0ed469-27ab-471b-a830-648b279103c8.png" width="250"/> </div><br>
|
|
||||||
|
|
||||||
② 否则,向上找第一个左链接指向的树包含该节点的祖先节点。
|
|
||||||
|
|
||||||
<div align="center"> <img src="../pics//e143f6da-d114-4ba4-8712-f65299047fa2.png" width="250"/> </div><br>
|
|
||||||
|
|
||||||
```java
|
```java
|
||||||
public class TreeLinkNode {
|
public class TreeLinkNode {
|
||||||
int val;
|
int val;
|
||||||
|
@ -373,6 +379,16 @@ public class TreeLinkNode {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 解题思路
|
||||||
|
|
||||||
|
① 如果一个节点的右子树不为空,那么该节点的下一个节点是右子树的最左节点;
|
||||||
|
|
||||||
|
<div align="center"> <img src="../pics//cb0ed469-27ab-471b-a830-648b279103c8.png" width="250"/> </div><br>
|
||||||
|
|
||||||
|
② 否则,向上找第一个左链接指向的树包含该节点的祖先节点。
|
||||||
|
|
||||||
|
<div align="center"> <img src="../pics//e143f6da-d114-4ba4-8712-f65299047fa2.png" width="250"/> </div><br>
|
||||||
|
|
||||||
```java
|
```java
|
||||||
public TreeLinkNode GetNext(TreeLinkNode pNode) {
|
public TreeLinkNode GetNext(TreeLinkNode pNode) {
|
||||||
if (pNode.right != null) {
|
if (pNode.right != null) {
|
||||||
|
@ -398,7 +414,7 @@ public TreeLinkNode GetNext(TreeLinkNode pNode) {
|
||||||
|
|
||||||
## 题目描述
|
## 题目描述
|
||||||
|
|
||||||
用两个栈来实现一个队列,完成队列的 Push 和 Pop 操作。队列中的元素为 int 类型。
|
用两个栈来实现一个队列,完成队列的 Push 和 Pop 操作。
|
||||||
|
|
||||||
## 解题思路
|
## 解题思路
|
||||||
|
|
||||||
|
@ -442,7 +458,7 @@ public int pop() throws Exception {
|
||||||
|
|
||||||
<div align="center"> <img src="../pics//a0df8edc-581b-4977-95c2-d7025795b899.png" width="300"/> </div><br>
|
<div align="center"> <img src="../pics//a0df8edc-581b-4977-95c2-d7025795b899.png" width="300"/> </div><br>
|
||||||
|
|
||||||
递归方法是将一个问题划分成多个子问题求解,动态规划也是如此,但是动态规划会把子问题的解缓存起来,避免重复求解子问题。
|
递归是将一个问题划分成多个子问题求解,动态规划也是如此,但是动态规划会把子问题的解缓存起来,从而避免重复求解子问题。
|
||||||
|
|
||||||
```java
|
```java
|
||||||
public int Fibonacci(int n) {
|
public int Fibonacci(int n) {
|
||||||
|
@ -521,11 +537,11 @@ public int JumpFloor(int n) {
|
||||||
|
|
||||||
```java
|
```java
|
||||||
public int JumpFloor(int n) {
|
public int JumpFloor(int n) {
|
||||||
if (n <= 1)
|
if (n <= 2)
|
||||||
return n;
|
return n;
|
||||||
int pre2 = 0, pre1 = 1;
|
int pre2 = 1, pre1 = 2;
|
||||||
int result = 0;
|
int result = 1;
|
||||||
for (int i = 1; i <= n; i++) {
|
for (int i = 2; i < n; i++) {
|
||||||
result = pre2 + pre1;
|
result = pre2 + pre1;
|
||||||
pre2 = pre1;
|
pre2 = pre1;
|
||||||
pre1 = result;
|
pre1 = result;
|
||||||
|
@ -613,6 +629,8 @@ public int RectCover(int n) {
|
||||||
|
|
||||||
因为 h 的赋值表达式为 h = m,因此循环体的循环条件应该为 l < h,详细解释请见 [Leetcode 题解](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/Leetcode%20%E9%A2%98%E8%A7%A3.md#%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE) 二分查找部分。
|
因为 h 的赋值表达式为 h = m,因此循环体的循环条件应该为 l < h,详细解释请见 [Leetcode 题解](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/Leetcode%20%E9%A2%98%E8%A7%A3.md#%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE) 二分查找部分。
|
||||||
|
|
||||||
|
但是如果出现 nums[l] == nums[m] == nums[h],那么此时无法确定解在哪个区间,因此需要切换到顺序查找。
|
||||||
|
|
||||||
复杂度:O(logN) + O(1)
|
复杂度:O(logN) + O(1)
|
||||||
|
|
||||||
```java
|
```java
|
||||||
|
@ -622,13 +640,22 @@ public int minNumberInRotateArray(int[] nums) {
|
||||||
int l = 0, h = nums.length - 1;
|
int l = 0, h = nums.length - 1;
|
||||||
while (l < h) {
|
while (l < h) {
|
||||||
int m = l + (h - l) / 2;
|
int m = l + (h - l) / 2;
|
||||||
if (nums[m] <= nums[h])
|
if (nums[l] == nums[m] && nums[m] == nums[h])
|
||||||
|
return minNumber(nums, l, h);
|
||||||
|
else if (nums[m] <= nums[h])
|
||||||
h = m;
|
h = m;
|
||||||
else
|
else
|
||||||
l = m + 1;
|
l = m + 1;
|
||||||
}
|
}
|
||||||
return nums[l];
|
return nums[l];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int minNumber(int[] nums, int l, int h) {
|
||||||
|
for (int i = l; i < h; i++)
|
||||||
|
if (nums[i] > nums[i + 1])
|
||||||
|
return nums[i + 1];
|
||||||
|
return nums[l];
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
# 12. 矩阵中的路径
|
# 12. 矩阵中的路径
|
||||||
|
@ -2022,7 +2049,7 @@ public int getDigitAtIndex(int index)
|
||||||
{
|
{
|
||||||
if (index < 0)
|
if (index < 0)
|
||||||
return -1;
|
return -1;
|
||||||
int place = 1; // 位数,1 表示个位,2 表示 十位...
|
int place = 1; // 1 表示个位,2 表示 十位...
|
||||||
while (true) {
|
while (true) {
|
||||||
int amount = getAmountOfPlace(place);
|
int amount = getAmountOfPlace(place);
|
||||||
int totalAmount = amount * place;
|
int totalAmount = amount * place;
|
||||||
|
@ -2640,7 +2667,7 @@ private void swap(char[] c, int i, int j)
|
||||||
|
|
||||||
## 解题思路
|
## 解题思路
|
||||||
|
|
||||||
将 "abcXYZdef" 旋转左移三位,可以先将 "abc" 和 "XYZdef" 分别旋转,得到 "cbafedZYX",然后再把整个字符串旋转得到 "XYZdefabc"。
|
先将 "abc" 和 "XYZdef" 分别翻转,得到 "cbafedZYX",然后再把整个字符串翻转得到 "XYZdefabc"。
|
||||||
|
|
||||||
```java
|
```java
|
||||||
public String LeftRotateString(String str, int n)
|
public String LeftRotateString(String str, int n)
|
||||||
|
|
|
@ -352,7 +352,7 @@ end monitor;
|
||||||
|
|
||||||
管程引入了 **条件变量** 以及相关的操作:**wait()** 和 **signal()** 来实现同步操作。对条件变量执行 wait() 操作会导致调用进程阻塞,把管程让出来给另一个进程持有。signal() 操作用于唤醒被阻塞的进程。
|
管程引入了 **条件变量** 以及相关的操作:**wait()** 和 **signal()** 来实现同步操作。对条件变量执行 wait() 操作会导致调用进程阻塞,把管程让出来给另一个进程持有。signal() 操作用于唤醒被阻塞的进程。
|
||||||
|
|
||||||
<font size=3> **使用管程实现生成者-消费者问题** </font><br>
|
<font size=3> **使用管程实现生产者-消费者问题** </font><br>
|
||||||
|
|
||||||
```pascal
|
```pascal
|
||||||
// 管程
|
// 管程
|
||||||
|
|
|
@ -139,25 +139,17 @@
|
||||||
|
|
||||||
- **物理层** :考虑的是怎样在传输媒体上传输数据比特流,而不是指具体的传输媒体。物理层的作用是尽可能屏蔽传输媒体和通信手段的差异,使数据链路层感觉不到这些差异。
|
- **物理层** :考虑的是怎样在传输媒体上传输数据比特流,而不是指具体的传输媒体。物理层的作用是尽可能屏蔽传输媒体和通信手段的差异,使数据链路层感觉不到这些差异。
|
||||||
|
|
||||||
### 2. 七层协议
|
### 2. OSI
|
||||||
|
|
||||||
其中表示层和会话层用途如下:
|
其中表示层和会话层用途如下:
|
||||||
|
|
||||||
- **表示层** :数据压缩、加密以及数据描述。这使得应用程序不必担心在各台主机中表示/存储的内部格式不同的问题。
|
- **表示层** :数据压缩、加密以及数据描述,这使得应用程序不必担心在各台主机中数据内部格式不同的问题。
|
||||||
|
|
||||||
- **会话层** :建立及管理会话。
|
- **会话层** :建立及管理会话。
|
||||||
|
|
||||||
五层协议没有表示层和会话层,而是将这些功能留给应用程序开发者处理。
|
五层协议没有表示层和会话层,而是将这些功能留给应用程序开发者处理。
|
||||||
|
|
||||||
### 3. 数据在各层之间的传递过程
|
### 3. TCP/IP
|
||||||
|
|
||||||
在向下的过程中,需要添加下层协议所需要的首部或者尾部,而在向上的过程中不断拆开首部和尾部。
|
|
||||||
|
|
||||||
路由器只有下面三层协议,因为路由器位于网络核心中,不需要为进程或者应用程序提供服务,因此也就不需要运输层和应用层。
|
|
||||||
|
|
||||||
<div align="center"> <img src="../pics//ac106e7e-489a-4082-abd9-dabebe48394c.jpg" width="800"/> </div><br>
|
|
||||||
|
|
||||||
### 4. TCP/IP
|
|
||||||
|
|
||||||
它只有四层,相当于五层协议中数据链路层和物理层合并为网络接口层。
|
它只有四层,相当于五层协议中数据链路层和物理层合并为网络接口层。
|
||||||
|
|
||||||
|
@ -169,6 +161,14 @@ TCP/IP 协议族是一种沙漏形状,中间小两边大,IP 协议在其中
|
||||||
|
|
||||||
<div align="center"> <img src="../pics//d4eef1e2-5703-4ca4-82ab-8dda93d6b81f.png" width="500"/> </div><br>
|
<div align="center"> <img src="../pics//d4eef1e2-5703-4ca4-82ab-8dda93d6b81f.png" width="500"/> </div><br>
|
||||||
|
|
||||||
|
### 4. 数据在各层之间的传递过程
|
||||||
|
|
||||||
|
在向下的过程中,需要添加下层协议所需要的首部或者尾部,而在向上的过程中不断拆开首部和尾部。
|
||||||
|
|
||||||
|
路由器只有下面三层协议,因为路由器位于网络核心中,不需要为进程或者应用程序提供服务,因此也就不需要运输层和应用层。
|
||||||
|
|
||||||
|
<div align="center"> <img src="../pics//ac106e7e-489a-4082-abd9-dabebe48394c.jpg" width="800"/> </div><br>
|
||||||
|
|
||||||
# 二、物理层
|
# 二、物理层
|
||||||
|
|
||||||
## 通信方式
|
## 通信方式
|
||||||
|
|
BIN
pics/026d3cb4-67f7-4a83-884d-8032f57ec446.png
Normal file
BIN
pics/026d3cb4-67f7-4a83-884d-8032f57ec446.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
Loading…
Reference in New Issue
Block a user