diff --git a/notes/Java 并发.md b/notes/Java 并发.md index 031f3290..cf221ce8 100644 --- a/notes/Java 并发.md +++ b/notes/Java 并发.md @@ -569,9 +569,9 @@ ReentrantLock 多了一些高级功能。 ## join() -在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待, 直到目标线程结束。 +在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。 -对于以下代码,虽然 b 线程先启动,但是因为在 b 线程中调用了 a 线程的 join() 方法,因此 b 线程会等待 a 线程结束才继续执行,因此最后能够保证 a 线程的输出先于 b 线程的输出。 +对于以下代码,虽然 b 线程先启动,但是因为在 b 线程中调用了 a 线程的 join() 方法,b 线程会等待 a 线程结束才继续执行,因此最后能够保证 a 线程的输出先于 b 线程的输出。 ```java public class JoinExample { @@ -667,8 +667,8 @@ after **wait() 和 sleep() 的区别** -1. wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法; -2. wait() 会释放锁,sleep() 不会。 +- wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法; +- wait() 会释放锁,sleep() 不会。 ## await() signal() signalAll() @@ -886,7 +886,7 @@ other task is running... java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现: -- **FIFO 队列** :LinkedBlockingQueue、ArrayListBlockingQueue(固定长度) +- **FIFO 队列** :LinkedBlockingQueue、ArrayBlockingQueue(固定长度) - **优先级队列** :PriorityBlockingQueue 提供了阻塞的 take() 和 put() 方法:如果队列为空 take() 将阻塞,直到队列中有内容;如果队列为满 put() 将阻塞,直到队列有空闲位置。 @@ -1387,7 +1387,9 @@ synchronized 和 ReentrantLock。 互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步(Blocking Synchronization)。 -从处理问题的方式上说,互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施(例如加锁),那就肯定会出现问题,无论共享数据是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。随着硬件指令集的发展,我们有了另外一个选择:基于冲突检测的乐观并发策略,通俗地说,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施(最常见的补偿措施就是不断地重试,直到成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步(Non-Blocking Synchronization)。 +从处理问题的方式上说,互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施(例如加锁),那就肯定会出现问题,无论共享数据是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。 + +随着硬件指令集的发展,我们有了另外一个选择:基于冲突检测的乐观并发策略,通俗地说,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施(最常见的补偿措施就是不断地重试,直到成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步(Non-Blocking Synchronization)。 乐观锁需要操作和冲突检测这两个步骤具备原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成。硬件支持的原子性操作最典型的是:比较并交换(Compare-and-Swap,CAS)。 diff --git a/notes/Redis.md b/notes/Redis.md index 5fceeb5e..ee2b27a5 100644 --- a/notes/Redis.md +++ b/notes/Redis.md @@ -6,7 +6,8 @@ * [SET](#set) * [HASH](#hash) * [ZSET](#zset) -* [三、使用场景](#三使用场景) +* [三、字典](#三字典) +* [四、使用场景](#四使用场景) * [缓存](#缓存) * [计数器](#计数器) * [消息队列](#消息队列) @@ -15,28 +16,28 @@ * [排行榜](#排行榜) * [分布式 Session](#分布式-session) * [分布式锁](#分布式锁) -* [四、Redis 与 Memcached](#四redis-与-memcached) +* [五、Redis 与 Memcached](#五redis-与-memcached) * [数据类型](#数据类型) * [数据持久化](#数据持久化) * [分布式](#分布式) * [内存管理机制](#内存管理机制) -* [五、键的过期时间](#五键的过期时间) -* [六、数据淘汰策略](#六数据淘汰策略) -* [七、持久化](#七持久化) +* [六、键的过期时间](#六键的过期时间) +* [七、数据淘汰策略](#七数据淘汰策略) +* [八、持久化](#八持久化) * [快照持久化](#快照持久化) * [AOF 持久化](#aof-持久化) -* [八、发布与订阅](#八发布与订阅) -* [九、事务](#九事务) -* [十、事件](#十事件) +* [九、发布与订阅](#九发布与订阅) +* [十、事务](#十事务) +* [十一、事件](#十一事件) * [文件事件](#文件事件) * [时间事件](#时间事件) * [事件的调度与执行](#事件的调度与执行) -* [十一、复制](#十一复制) +* [十二、复制](#十二复制) * [连接过程](#连接过程) * [主从链](#主从链) -* [十二、Sentinel](#十二sentinel) -* [十三、分片](#十三分片) -* [十四、一个简单的论坛系统分析](#十四一个简单的论坛系统分析) +* [十三、Sentinel](#十三sentinel) +* [十四、分片](#十四分片) +* [十五、一个简单的论坛系统分析](#十五一个简单的论坛系统分析) * [文章信息](#文章信息) * [点赞功能](#点赞功能) * [对文章进行排序](#对文章进行排序) @@ -204,7 +205,111 @@ OK 2) "982" ``` -# 三、使用场景 +# 三、字典 + +以下是 Redis 字典的主要数据结构,从上往下分析,一个 dict 有两个 dictht,一个 dictht 有一个 dictEntry 数组,每个 dictEntry 有 next 指针因此是一个链表结构。从上面的分析可以看出 Redis 的字典是一个基于拉链法解决冲突的哈希表结构。 + +```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 +/* This is our hash table structure. Every dictionary has two of this as we + * implement incremental rehashing, for the old to the new table. */ +typedef struct dictht { + dictEntry **table; + unsigned long size; + unsigned long sizemask; + unsigned long used; +} dictht; +``` + +```c +typedef struct dictEntry { + void *key; + union { + void *val; + uint64_t u64; + int64_t s64; + double d; + } v; + struct dictEntry *next; +} dictEntry; +``` + +哈希表需要具备扩容能力,在扩容时就需要对每个键值对进行 rehash。dict 有两个 dictht,在 rehash 的时候会将一个 dictht 上的键值对重新插入另一个 dictht 上面,完成之后释放空间并交换两个 dictht 的角色。 + +rehash 操作不是一次性完成,而是采用渐进方式,这是为了避免一次性执行过多的 rehash 操作给服务器带来过大的负担。 + +渐进式 rehash 通过记录 dict 的 rehashidx 完成,它从 0 开始然后每执行一次 rehash 都会递增。例如在一次 rehash 中,要把 dict[0] rehash 到 dict[1],这一次会把 dict[0] 上 table[rehashidx] 的键值对 rehash 到 dict[1] 上,table[rehashidx] 指向 null,并令 rehashidx++。 + +在 rehash 期间,每次对字典执行添加、删除、查找或者更新操作时,都会执行一次渐进式 rehash。 + +采用渐进式 rehash 会导致字典中的数据分散在两个 dict 上,因此对字典的操作也需要到对应的 dict 去执行。 + +```c +/* Performs N steps of incremental rehashing. Returns 1 if there are still + * keys to move from the old to the new hash table, otherwise 0 is returned. + * + * Note that a rehashing step consists in moving a bucket (that may have more + * than one key as we use chaining) from the old to the new hash table, however + * since part of the hash table may be composed of empty spaces, it is not + * guaranteed that this function will rehash even a single bucket, since it + * will visit at max N*10 empty buckets in total, otherwise the amount of + * work it does would be unbound and the function may block for a long time. */ +int dictRehash(dict *d, int n) { + int empty_visits = n * 10; /* Max number of empty buckets to visit. */ + if (!dictIsRehashing(d)) return 0; + + while (n-- && d->ht[0].used != 0) { + dictEntry *de, *nextde; + + /* Note that rehashidx can't overflow as we are sure there are more + * elements because ht[0].used != 0 */ + assert(d->ht[0].size > (unsigned long) d->rehashidx); + while (d->ht[0].table[d->rehashidx] == NULL) { + d->rehashidx++; + if (--empty_visits == 0) return 1; + } + de = d->ht[0].table[d->rehashidx]; + /* Move all the keys in this bucket from the old to the new hash HT */ + while (de) { + uint64_t h; + + nextde = de->next; + /* Get the index in the new hash table */ + h = dictHashKey(d, de->key) & d->ht[1].sizemask; + de->next = d->ht[1].table[h]; + d->ht[1].table[h] = de; + d->ht[0].used--; + d->ht[1].used++; + de = nextde; + } + d->ht[0].table[d->rehashidx] = NULL; + d->rehashidx++; + } + + /* Check if we already rehashed the whole table... */ + if (d->ht[0].used == 0) { + zfree(d->ht[0].table); + d->ht[0] = d->ht[1]; + _dictReset(&d->ht[1]); + d->rehashidx = -1; + return 0; + } + + /* More to rehash... */ + return 1; +} +``` + +# 四、使用场景 ## 缓存 @@ -238,7 +343,7 @@ Redis 这种内存数据库能支持计数器频繁的读写操作。 除了可以使用 SETNX 命令实现分布式锁之外,还可以使用官方提供的 RedLock 分布式锁实现。 -# 四、Redis 与 Memcached +# 五、Redis 与 Memcached 两者都是非关系型内存键值数据库。有以下主要不同: @@ -262,14 +367,13 @@ Redis Cluster 实现了分布式的支持。 Memcached 将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题,但是这种方式会使得内存的利用率不高,例如块的大小为 128 bytes,只存储 100 bytes 的数据,那么剩下的 28 bytes 就浪费掉了。 - -# 五、键的过期时间 +# 六、键的过期时间 Redis 可以为每个键设置过期时间,当键过期时,会自动删除该键。 对于散列表这种容器,只能为整个键设置过期时间(整个散列表),而不能为键里面的单个元素设置过期时间。 -# 六、数据淘汰策略 +# 七、数据淘汰策略 可以设置内存最大使用量,当内存使用量超过时施行淘汰策略,具体有 6 种淘汰策略。 @@ -286,7 +390,7 @@ Redis 可以为每个键设置过期时间,当键过期时,会自动删除 作为内存数据库,出于对性能和内存消耗的考虑,Redis 的淘汰算法(LRU、TTL)实际实现上并非针对所有 key,而是抽样一小部分 key 从中选出被淘汰 key。 -# 七、持久化 +# 八、持久化 Redis 是内存型数据库,为了保证数据在断电后不会丢失,需要将内存中的数据持久化到硬盘上。 @@ -320,7 +424,7 @@ Redis 是内存型数据库,为了保证数据在断电后不会丢失,需 随着服务器写请求的增多,AOF 文件会越来越大。Redis 提供了一种将 AOF 重写的特性,能够去除 AOF 文件中的冗余写命令。 -# 八、发布与订阅 +# 九、发布与订阅 订阅者订阅了频道之后,发布者向频道发送字符串消息会被所有订阅者接收到。 @@ -333,7 +437,7 @@ Redis 是内存型数据库,为了保证数据在断电后不会丢失,需

-# 九、事务 +# 十、事务 一个事务包含了多个命令,服务器在执行事务期间,不会改去执行其它客户端的命令请求。 @@ -341,7 +445,7 @@ Redis 是内存型数据库,为了保证数据在断电后不会丢失,需 Redis 最简单的事务实现方式是使用 MULTI 和 EXEC 命令将事务操作包围起来。 -# 十、事件 +# 十一、事件 Redis 服务器是一个事件驱动程序。 @@ -416,7 +520,7 @@ def main():

-# 十一、复制 +# 十二、复制 通过使用 slaveof host port 命令来让一个服务器成为另一个服务器的从服务器。 @@ -436,11 +540,11 @@ def main():

-# 十二、Sentinel +# 十三、Sentinel Sentinel(哨兵)可以监听主服务器,并在主服务器进入下线状态时,自动从从服务器中选举出新的主服务器。 -# 十三、分片 +# 十四、分片 分片是将数据划分为多个部分的方法,可以将数据存储到多台机器里面,也可以从多台机器里面获取数据,这种方法在解决某些问题时可以获得线性级别的性能提升。 @@ -452,7 +556,7 @@ Sentinel(哨兵)可以监听主服务器,并在主服务器进入下线状 - 代理分片:将客户端请求发送到代理上,由代理转发请求到正确的节点上。 - 服务器分片:Redis Cluster。 -# 十四、一个简单的论坛系统分析 +# 十五、一个简单的论坛系统分析 该论坛系统功能如下: