CS-Notes/notes/Redis.md

616 lines
24 KiB
Java
Raw Normal View History

2019-04-25 18:24:51 +08:00
<!-- GFM-TOC -->
* [概述](#一概述)
* [数据类型](#二数据类型)
* [STRING](#string)
* [LIST](#list)
* [SET](#set)
* [HASH](#hash)
* [ZSET](#zset)
* [数据结构](#三数据结构)
* [字典](#字典)
* [跳跃表](#跳跃表)
* [使用场景](#四使用场景)
* [计数器](#计数器)
* [缓存](#缓存)
* [查找表](#查找表)
* [消息队列](#消息队列)
* [会话缓存](#会话缓存)
* [分布式锁实现](#分布式锁实现)
* [其它](#其它)
* [Redis Memcached](#五redis--memcached)
* [数据类型](#数据类型)
* [数据持久化](#数据持久化)
* [分布式](#分布式)
* [内存管理机制](#内存管理机制)
* [键的过期时间](#六键的过期时间)
* [数据淘汰策略](#七数据淘汰策略)
* [持久化](#八持久化)
* [RDB 持久化](#rdb-持久化)
* [AOF 持久化](#aof-持久化)
* [事务](#九事务)
* [事件](#十事件)
* [文件事件](#文件事件)
* [时间事件](#时间事件)
* [事件的调度与执行](#事件的调度与执行)
* [十一复制](#十一复制)
* [连接过程](#连接过程)
* [主从链](#主从链)
* [十二Sentinel](#十二sentinel)
* [十三分片](#十三分片)
* [十四一个简单的论坛系统分析](#十四一个简单的论坛系统分析)
* [文章信息](#文章信息)
* [点赞功能](#点赞功能)
* [对文章进行排序](#对文章进行排序)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
# 概述
Redis 是速度非常快的非关系型NoSQL内存键值数据库可以存储键和五种不同类型的值之间的映射
键的类型只能为字符串值支持五种数据类型字符串列表集合散列表有序集合
Redis 支持很多特性例如将内存中的数据持久化到硬盘中使用复制来扩展读性能使用分片来扩展写性能
# 数据类型
| 数据类型 | 可以存储的值 | 操作 |
| :--: | :--: | :--: |
| STRING | 字符串整数或者浮点数 | 对整个字符串或者字符串的其中一部分执行操作</br> 对整数和浮点数执行自增或者自减操作 |
2019-06-09 22:32:10 +08:00
| LIST | 列表 | 从两端压入或者弹出元素 </br> 对单个或者多个元素进行修剪</br> 只保留一个范围内的元素 |
2019-04-25 18:24:51 +08:00
| SET | 无序集合 | 添加获取移除单个元素</br> 检查一个元素是否存在于集合中</br> 计算交集并集差集</br> 从集合里面随机获取元素 |
| HASH | 包含键值对的无序散列表 | 添加获取移除单个键值对</br> 获取所有键值对</br> 检查某个键是否存在|
| ZSET | 有序集合 | 添加获取删除元素</br> 根据分值范围或者成员来获取元素</br> 计算一个键的排名 |
> [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/)
## STRING
2019-11-02 17:33:10 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/6019b2db-bc3e-4408-b6d8-96025f4481d6.png" width="400"/> </div><br>
2019-04-25 18:24:51 +08:00
```html
> set hello world
OK
> get hello
"world"
> del hello
(integer) 1
> get hello
(nil)
```
## LIST
2019-11-02 17:33:10 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/fb327611-7e2b-4f2f-9f5b-38592d408f07.png" width="400"/> </div><br>
2019-04-25 18:24:51 +08:00
```html
> rpush list-key item
(integer) 1
> rpush list-key item2
(integer) 2
> rpush list-key item
(integer) 3
> lrange list-key 0 -1
1) "item"
2) "item2"
3) "item"
> lindex list-key 1
"item2"
> lpop list-key
"item"
> lrange list-key 0 -1
1) "item2"
2) "item"
```
## SET
2019-11-02 17:33:10 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/cd5fbcff-3f35-43a6-8ffa-082a93ce0f0e.png" width="400"/> </div><br>
2019-04-25 18:24:51 +08:00
```html
> sadd set-key item
(integer) 1
> sadd set-key item2
(integer) 1
> sadd set-key item3
(integer) 1
> sadd set-key item
(integer) 0
> smembers set-key
1) "item"
2) "item2"
3) "item3"
> sismember set-key item4
(integer) 0
> sismember set-key item
(integer) 1
> srem set-key item2
(integer) 1
> srem set-key item2
(integer) 0
> smembers set-key
1) "item"
2) "item3"
```
## HASH
2019-11-02 17:33:10 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/7bd202a7-93d4-4f3a-a878-af68ae25539a.png" width="400"/> </div><br>
2019-04-25 18:24:51 +08:00
```html
> hset hash-key sub-key1 value1
(integer) 1
> hset hash-key sub-key2 value2
(integer) 1
> hset hash-key sub-key1 value1
(integer) 0
> hgetall hash-key
1) "sub-key1"
2) "value1"
3) "sub-key2"
4) "value2"
> hdel hash-key sub-key2
(integer) 1
> hdel hash-key sub-key2
(integer) 0
> hget hash-key sub-key1
"value1"
> hgetall hash-key
1) "sub-key1"
2) "value1"
```
## ZSET
2019-11-02 17:33:10 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/1202b2d6-9469-4251-bd47-ca6034fb6116.png" width="400"/> </div><br>
2019-04-25 18:24:51 +08:00
```html
> zadd zset-key 728 member1
(integer) 1
> zadd zset-key 982 member0
(integer) 1
> zadd zset-key 982 member0
(integer) 0
> zrange zset-key 0 -1 withscores
1) "member1"
2) "728"
3) "member0"
4) "982"
> zrangebyscore zset-key 0 800 withscores
1) "member1"
2) "728"
> zrem zset-key member1
(integer) 1
> zrem zset-key member1
(integer) 0
> zrange zset-key 0 -1 withscores
1) "member0"
2) "982"
```
# 数据结构
## 字典
2019-06-09 22:32:10 +08:00
dictht 是一个散列表结构使用拉链法解决哈希冲突
2019-04-25 18:24:51 +08:00
```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;
```
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 通过记录 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 会导致字典中的数据分散在两个 dictht 因此对字典的查找操作也需要到对应的 dictht 去执行
```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;
}
```
## 跳跃表
是有序集合的底层实现之一
跳跃表是基于多指针有序链表实现的可以看成多个有序链表
2019-11-02 17:33:10 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/beba612e-dc5b-4fc2-869d-0b23408ac90a.png" width="600px"/> </div><br>
2019-04-25 18:24:51 +08:00
在查找时从上层指针开始查找找到对应的区间之后再到下一层去查找下图演示了查找 22 的过程
2019-11-02 17:33:10 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/0ea37ee2-c224-4c79-b895-e131c6805c40.png" width="600px"/> </div><br>
2019-04-25 18:24:51 +08:00
与红黑树等平衡树相比跳跃表具有以下优点
- 插入速度非常快速因为不需要进行旋转等操作来维护平衡性
- 更容易实现
- 支持无锁操作
# 使用场景
## 计数器
可以对 String 进行自增自减运算从而实现计数器功能
Redis 这种内存型数据库的读写性能非常高很适合存储频繁读写的计数量
## 缓存
将热点数据放到内存中设置内存的最大使用量以及淘汰策略来保证缓存的命中率
## 查找表
例如 DNS 记录就很适合使用 Redis 进行存储
查找表和缓存类似也是利用了 Redis 快速的查找特性但是查找表的内容不能失效而缓存的内容可以失效因为缓存不作为可靠的数据来源
## 消息队列
List 是一个双向链表可以通过 lpush rpop 写入和读取消息
不过最好使用 KafkaRabbitMQ 等消息中间件
## 会话缓存
可以使用 Redis 来统一存储多台应用服务器的会话信息
当应用服务器不再存储用户的会话信息也就不再具有状态一个用户可以请求任意一个应用服务器从而更容易实现高可用性以及可伸缩性
## 分布式锁实现
在分布式场景下无法使用单机环境下的锁来对多个节点上的进程进行同步
可以使用 Redis 自带的 SETNX 命令实现分布式锁除此之外还可以使用官方提供的 RedLock 分布式锁实现
## 其它
Set 可以实现交集并集等操作从而实现共同好友等功能
ZSet 可以实现有序性操作从而实现排行榜等功能
# Redis Memcached
两者都是非关系型内存键值数据库主要有以下不同
## 数据类型
Memcached 仅支持字符串类型 Redis 支持五种不同的数据类型可以更灵活地解决问题
## 数据持久化
Redis 支持两种持久化策略RDB 快照和 AOF 日志 Memcached 不支持持久化
## 分布式
Memcached 不支持分布式只能通过在客户端使用一致性哈希来实现分布式存储这种方式在存储和查询时都需要先在客户端计算一次数据所在的节点
Redis Cluster 实现了分布式的支持
## 内存管理机制
- Redis 并不是所有数据都一直存储在内存中可以将一些很久没用的 value 交换到磁盘 Memcached 的数据则会一直在内存中
- Memcached 将内存分割成特定长度的块来存储数据以完全解决内存碎片的问题但是这种方式会使得内存的利用率不高例如块的大小为 128 bytes只存储 100 bytes 的数据那么剩下的 28 bytes 就浪费掉了
# 键的过期时间
Redis 可以为每个键设置过期时间当键过期时会自动删除该键
对于散列表这种容器只能为整个键设置过期时间整个散列表而不能为键里面的单个元素设置过期时间
# 数据淘汰策略
可以设置内存最大使用量当内存使用量超出时会施行数据淘汰策略
Redis 具体有 6 种淘汰策略
| 策略 | 描述 |
| :--: | :--: |
| volatile-lru | 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰 |
| volatile-ttl | 从已设置过期时间的数据集中挑选将要过期的数据淘汰 |
|volatile-random | 从已设置过期时间的数据集中任意选择数据淘汰 |
| allkeys-lru | 从所有数据集中挑选最近最少使用的数据淘汰 |
| allkeys-random | 从所有数据集中任意选择数据进行淘汰 |
| noeviction | 禁止驱逐数据 |
作为内存数据库出于对性能和内存消耗的考虑Redis 的淘汰算法实际实现上并非针对所有 key而是抽样一小部分并且从中选出被淘汰的 key
使用 Redis 缓存数据时为了提高缓存命中率需要保证缓存数据都是热点数据可以将内存最大使用量设置为热点数据占用的内存量然后启用 allkeys-lru 淘汰策略将最近最少使用的数据淘汰
Redis 4.0 引入了 volatile-lfu allkeys-lfu 淘汰策略LFU 策略通过统计访问频率将访问频率最少的键值对淘汰
# 持久化
Redis 是内存型数据库为了保证数据在断电后不会丢失需要将内存中的数据持久化到硬盘上
## RDB 持久化
将某个时间点的所有数据都存放到硬盘上
可以将快照复制到其它服务器从而创建具有相同数据的服务器副本
如果系统发生故障将会丢失最后一次创建快照之后的数据
如果数据量很大保存快照的时间会很长
## AOF 持久化
将写命令添加到 AOF 文件Append Only File的末尾
2019-06-09 22:32:10 +08:00
使用 AOF 持久化需要设置同步选项从而确保写命令同步到磁盘文件上的时机这是因为对文件进行写入并不会马上将内容同步到磁盘上而是先存储到缓冲区然后由操作系统决定什么时候同步到磁盘有以下同步选项
2019-04-25 18:24:51 +08:00
| 选项 | 同步频率 |
| :--: | :--: |
| always | 每个写命令都同步 |
| everysec | 每秒同步一次 |
| no | 让操作系统来决定何时同步 |
- always 选项会严重减低服务器的性能
- everysec 选项比较合适可以保证系统崩溃时只会丢失一秒左右的数据并且 Redis 每秒执行一次同步对服务器性能几乎没有任何影响
- no 选项并不能给服务器性能带来多大的提升而且也会增加系统崩溃时数据丢失的数量
随着服务器写请求的增多AOF 文件会越来越大Redis 提供了一种将 AOF 重写的特性能够去除 AOF 文件中的冗余写命令
# 事务
一个事务包含了多个命令服务器在执行事务期间不会改去执行其它客户端的命令请求
事务中的多个命令被一次性发送给服务器而不是一条一条发送这种方式被称为流水线它可以减少客户端与服务器之间的网络通信次数从而提升性能
Redis 最简单的事务实现方式是使用 MULTI EXEC 命令将事务操作包围起来
# 事件
Redis 服务器是一个事件驱动程序
## 文件事件
服务器通过套接字与客户端或者其它服务器进行通信文件事件就是对套接字操作的抽象
Redis 基于 Reactor 模式开发了自己的网络事件处理器使用 I/O 多路复用程序来同时监听多个套接字并将到达的事件传送给文件事件分派器分派器会根据套接字产生的事件类型调用相应的事件处理器
2019-11-02 17:33:10 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/9ea86eb5-000a-4281-b948-7b567bd6f1d8.png" width=""/> </div><br>
2019-04-25 18:24:51 +08:00
## 时间事件
服务器有一些操作需要在给定的时间点执行时间事件是对这类定时操作的抽象
时间事件又分为
- 定时事件是让一段程序在指定的时间之内执行一次
- 周期性事件是让一段程序每隔指定时间就执行一次
Redis 将所有时间事件都放在一个无序链表中通过遍历整个链表查找出已到达的时间事件并调用相应的事件处理器
## 事件的调度与执行
服务器需要不断监听文件事件的套接字才能得到待处理的文件事件但是不能一直监听否则时间事件无法在规定的时间内执行因此监听时间应该根据距离现在最近的时间事件来决定
事件调度与执行由 aeProcessEvents 函数负责伪代码如下
```python
def aeProcessEvents():
# 获取到达时间离当前时间最接近的时间事件
time_event = aeSearchNearestTimer()
# 计算最接近的时间事件距离到达还有多少毫秒
remaind_ms = time_event.when - unix_ts_now()
# 如果事件已到达那么 remaind_ms 的值可能为负数将它设为 0
if remaind_ms < 0:
remaind_ms = 0
# 根据 remaind_ms 的值创建 timeval
timeval = create_timeval_with_ms(remaind_ms)
# 阻塞并等待文件事件产生最大阻塞时间由传入的 timeval 决定
aeApiPoll(timeval)
# 处理所有已产生的文件事件
procesFileEvents()
# 处理所有已到达的时间事件
processTimeEvents()
```
aeProcessEvents 函数置于一个循环里面加上初始化和清理函数就构成了 Redis 服务器的主函数伪代码如下
```python
def main():
# 初始化服务器
init_server()
# 一直处理事件直到服务器关闭为止
while server_is_not_shutdown():
aeProcessEvents()
# 服务器关闭执行清理操作
clean_server()
```
从事件处理的角度来看服务器运行流程如下
2019-11-02 17:33:10 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/c0a9fa91-da2e-4892-8c9f-80206a6f7047.png" width="350"/> </div><br>
2019-04-25 18:24:51 +08:00
# 十一复制
通过使用 slaveof host port 命令来让一个服务器成为另一个服务器的从服务器
一个从服务器只能有一个主服务器并且不支持主主复制
## 连接过程
1. 主服务器创建快照文件发送给从服务器并在发送期间使用缓冲区记录执行的写命令快照文件发送完毕之后开始向从服务器发送存储在缓冲区中的写命令
2. 从服务器丢弃所有旧数据载入主服务器发来的快照文件之后从服务器开始接受主服务器发来的写命令
3. 主服务器每执行一次写命令就向从服务器发送相同的写命令
## 主从链
随着负载不断上升主服务器可能无法很快地更新所有从服务器或者重新连接和重新同步从服务器将导致系统超载为了解决这个问题可以创建一个中间层来分担主服务器的复制工作中间层的服务器是最上层服务器的从服务器又是最下层服务器的主服务器
2019-11-02 17:33:10 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/395a9e83-b1a1-4a1d-b170-d081e7bb5bab.png" width="600"/> </div><br>
2019-04-25 18:24:51 +08:00
# 十二Sentinel
Sentinel哨兵可以监听集群中的服务器并在主服务器进入下线状态时自动从从服务器中选举出新的主服务器
# 十三分片
分片是将数据划分为多个部分的方法可以将数据存储到多台机器里面这种方法在解决某些问题时可以获得线性级别的性能提升
假设有 4 Redis 实例 R0R1R2R3还有很多表示用户的键 user:1user:2... 有不同的方式来选择一个指定的键存储在哪个实例中
- 最简单的方式是范围分片例如用户 id 0\~1000 的存储到实例 R0 用户 id 1001\~2000 的存储到实例 R1 等等但是这样需要维护一张映射范围表维护操作代价很高
- 还有一种方式是哈希分片使用 CRC32 哈希函数将键转换为一个数字再对实例数量求模就能知道应该存储的实例
根据执行分片的位置可以分为三种分片方式
- 客户端分片客户端使用一致性哈希等算法决定键应当分布到哪个节点
- 代理分片将客户端请求发送到代理上由代理转发请求到正确的节点上
- 服务器分片Redis Cluster
# 十四一个简单的论坛系统分析
该论坛系统功能如下
- 可以发布文章
- 可以对文章进行点赞
- 在首页可以按文章的发布时间或者文章的点赞数进行排序显示
## 文章信息
文章包括标题作者赞数等信息在关系型数据库中很容易构建一张表来存储这些信息 Redis 中可以使用 HASH 来存储每种信息以及其对应的值的映射
Redis 没有关系型数据库中的表这一概念来将同种类型的数据存放在一起而是使用命名空间的方式来实现这一功能键名的前面部分存储命名空间后面部分的内容存储 ID通常使用 : 来进行分隔例如下面的 HASH 的键名为 article:92617其中 article 为命名空间ID 92617
2019-11-02 17:33:10 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/7c54de21-e2ff-402e-bc42-4037de1c1592.png" width="400"/> </div><br>
2019-04-25 18:24:51 +08:00
## 点赞功能
当有用户为一篇文章点赞时除了要对该文章的 votes 字段进行加 1 操作还必须记录该用户已经对该文章进行了点赞防止用户点赞次数超过 1可以建立文章的已投票用户集合来进行记录
为了节约内存规定一篇文章发布满一周之后就不能再对它进行投票而文章的已投票集合也会被删除可以为文章的已投票集合设置一个一周的过期时间就能实现这个规定
2019-11-02 17:33:10 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/485fdf34-ccf8-4185-97c6-17374ee719a0.png" width="400"/> </div><br>
2019-04-25 18:24:51 +08:00
## 对文章进行排序
为了按发布时间和点赞数进行排序可以建立一个文章发布时间的有序集合和一个文章点赞数的有序集合下图中的 score 就是这里所说的点赞数下面所示的有序集合分值并不直接是时间和点赞数而是根据时间和点赞数间接计算出来的
2019-11-02 17:33:10 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/f7d170a3-e446-4a64-ac2d-cb95028f81a8.png" width="800"/> </div><br>
2019-04-25 18:24:51 +08:00
# 参考资料
- Carlson J L. Redis in Action[J]. Media.johnwiley.com.au, 2013.
- [黄健宏. Redis 设计与实现 [M]. 机械工业出版社, 2014.](http://redisbook.com/index.html)
- [REDIS IN ACTION](https://redislabs.com/ebook/foreword/)
- [Skip Lists: Done Right](http://ticki.github.io/blog/skip-lists-done-right/)
- [论述 Redis Memcached 的差异](http://www.cnblogs.com/loveincode/p/7411911.html)
- [Redis 3.0 中文版- 分片](http://wiki.jikexueyuan.com/project/redis-guide)
- [Redis 应用场景](http://www.scienjus.com/redis-use-case/)
- [Using Redis as an LRU cache](https://redis.io/topics/lru-cache)
2019-10-28 00:25:00 +08:00
2019-11-02 17:33:10 +08:00
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>