在上一篇文章中《Redis 命令执行过程(上)》中,我们首先了解 Redis 命令执行的整体流程,然后细致分析了从 Redis 启动到建立 socket 连接,再到读取 socket 数据到输入缓冲区,解析命令,执行命令等过程的原理和实现细节。接下来,我们来具体看一下 set 和 get 命令的实现细节和如何将命令结果通过输出缓冲区和 socket 发送给 Redis 客户端。

set 和 get 命令具体实现

前文讲到 processCommand 方法会从输入缓冲区中解析出对应的 redisCommand,然后调用 call 方法执行解析出来的 redisCommand的 proc 方法。不同命令的的 proc 方法是不同的,比如说名为 set 的 redisCommand 的 proc 是 setCommand 方法,而 get 的则是 getCommand 方法。通过这种形式,实际上实现在Java 中特别常见的多态策略。

  1. void call(client *c, int flags) {
  2. ....
  3. c->cmd->proc(c);
  4. ....
  5. }
  6. // redisCommand结构体
  7. struct redisCommand {
  8. char *name;
  9. // 对应方法的函数范式
  10. redisCommandProc *proc;
  11. .... // 其他定义
  12. };
  13. // 使用 typedef 定义的别名
  14. typedef void redisCommandProc(client *c);
  15. // 不同的命令,调用不同的方法。
  16. struct redisCommand redisCommandTable[] = {
  17. {"get",getCommand,2,"rF",0,NULL,1,1,1,0,0},
  18. {"set",setCommand,-3,"wm",0,NULL,1,1,1,0,0},
  19. {"hmset",hsetCommand,-4,"wmF",0,NULL,1,1,1,0,0},
  20. .... // 所有的 redis 命令都有
  21. }

setCommand 会判断set命令是否携带了nx、xx、ex或者px等可选参数,然后调用setGenericCommand命令。我们直接来看 setGenericCommand 方法。

setGenericCommand 方法的处理逻辑如下所示:

  • 首先判断 set 的类型是 set_nx 还是 set_xx,如果是 nx 并且 key 已经存在则直接返回;如果是 xx 并且 key 不存在则直接返回。
  • 调用 setKey 方法将键值添加到对应的 Redis 数据库中。
  • 如果有过期时间,则调用 setExpire 将设置过期时间
  • 进行键空间通知
  • 返回对应的值给客户端。
  1. // t_string.c
  2. void setGenericCommand(client *c, int flags, robj *key, robj *val, robj *expire, int unit, robj *ok_reply, robj *abort_reply) {
  3. long long milliseconds = 0;
  4. /**
  5. * 设置了过期时间;expire是robj类型,获取整数值
  6. */
  7. if (expire) {
  8. if (getLongLongFromObjectOrReply(c, expire, &milliseconds, NULL) != C_OK)
  9. return;
  10. if (milliseconds <= 0) {
  11. addReplyErrorFormat(c,"invalid expire time in %s",c->cmd->name);
  12. return;
  13. }
  14. if (unit == UNIT_SECONDS) milliseconds *= 1000;
  15. }
  16. /**
  17. * NX,key存在时直接返回;XX,key不存在时直接返回
  18. * lookupKeyWrite 是在对应的数据库中寻找键值是否存在
  19. */
  20. if ((flags & OBJ_SET_NX && lookupKeyWrite(c->db,key) != NULL) ||
  21. (flags & OBJ_SET_XX && lookupKeyWrite(c->db,key) == NULL))
  22. {
  23. addReply(c, abort_reply ? abort_reply : shared.nullbulk);
  24. return;
  25. }
  26. /**
  27. * 添加到数据字典
  28. */
  29. setKey(c->db,key,val);
  30. server.dirty++;
  31. /**
  32. * 过期时间添加到过期字典
  33. */
  34. if (expire) setExpire(c,c->db,key,mstime()+milliseconds);
  35. /**
  36. * 键空间通知
  37. */
  38. notifyKeyspaceEvent(NOTIFY_STRING,"set",key,c->db->id);
  39. if (expire) notifyKeyspaceEvent(NOTIFY_GENERIC,
  40. "expire",key,c->db->id);
  41. /**
  42. * 返回值,addReply 在 get 命令时再具体讲解
  43. */
  44. addReply(c, ok_reply ? ok_reply : shared.ok);
  45. }

具体 setKey 和 setExpire 的方法实现我们这里就不细讲,其实就是将键值添加到db的 dict 数据哈希表中,将键和过期时间添加到 expires 哈希表中,如下图所示。

接下来看 getCommand 的具体实现,同样的,它底层会调用 getGenericCommand 方法。

getGenericCommand 方法会调用 lookupKeyReadOrReply 来从 dict 数据哈希表中查找对应的 key值。如果找不到,则直接返回 C_OK;如果找到了,则根据值的类型,调用 addReply 或者 addReplyBulk 方法将值添加到输出缓冲区中。

  1. int getGenericCommand(client *c) {
  2. robj *o;
  3. // 调用 lookupKeyReadOrReply 从数据字典中查找对应的键
  4. if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk)) == NULL)
  5. return C_OK;
  6. // 如果是string类型,调用 addReply 单行返回。如果是其他对象类型,则调用 addReplyBulk
  7. if (o->type != OBJ_STRING) {
  8. addReply(c,shared.wrongtypeerr);
  9. return C_ERR;
  10. } else {
  11. addReplyBulk(c,o);
  12. return C_OK;
  13. }
  14. }

lookupKeyReadWithFlags 会从 redisDb 中查找对应的键值对,它首先会调用 expireIfNeeded判断键是否过期并且需要删除,如果为过期,则调用 lookupKey 方法从 dict 哈希表中查找并返回。具体解释可以看代码中的详细注释

  1. /*
  2. * 查找key的读操作,如果key找不到或者已经逻辑上过期返回 NULL,有一些副作用
  3. * 1 如果key到达过期时间,它会被设备为过期,并且删除
  4. * 2 更新key的最近访问时间
  5. * 3 更新全局缓存击中概率
  6. * flags 有两个值: LOOKUP_NONE 一般都是这个;LOOKUP_NOTOUCH 不修改最近访问时间
  7. */
  8. robj *lookupKeyReadWithFlags(redisDb *db, robj *key, int flags) { // db.c
  9. robj *val;
  10. // 检查键是否过期
  11. if (expireIfNeeded(db,key) == 1) {
  12. .... // master和 slave 对这种情况的特殊处理
  13. }
  14. // 查找键值字典
  15. val = lookupKey(db,key,flags);
  16. // 更新全局缓存命中率
  17. if (val == NULL)
  18. server.stat_keyspace_misses++;
  19. else
  20. server.stat_keyspace_hits++;
  21. return val;
  22. }

Redis 在调用查找键值系列方法前都会先调用 expireIfNeeded 来判断键是否过期,然后根据 Redis 是否配置了懒删除来进行同步删除或者异步删除。关于键删除的细节可以查看《详解 Redis 内存管理机制和实现》一文。

在判断键释放过期的逻辑中有两个特殊情况:

  • 如果当前 Redis 是主从结构中的从实例,则只判断键是否过期,不直接对键进行删除,而是要等待主实例发送过来的删除命令后再进行删除。如果当前 Redis 是主实例,则调用 propagateExpire 来传播过期指令。
  • 如果当前正在进行 Lua 脚本执行,因为其原子性和事务性,整个执行过期中时间都按照其开始执行的那一刻计算,也就是说lua执行时未过期的键,在它整个执行过程中也都不会过期。

  1. /*
  2. * 在调用 lookupKey*系列方法前调用该方法。
  3. * 如果是slave:
  4. * slave 并不主动过期删除key,但是返回值仍然会返回键已经被删除。
  5. * master 如果key过期了,会主动删除过期键,并且触发 AOF 和同步操作。
  6. * 返回值为0表示键仍然有效,否则返回1
  7. */
  8. int expireIfNeeded(redisDb *db, robj *key) { // db.c
  9. // 获取键的过期时间
  10. mstime_t when = getExpire(db,key);
  11. mstime_t now;
  12. if (when < 0) return 0;
  13. /*
  14. * 如果当前是在执行lua脚本,根据其原子性,整个执行过期中时间都按照其开始执行的那一刻计算
  15. * 也就是说lua执行时未过期的键,在它整个执行过程中也都不会过期。
  16. */
  17. now = server.lua_caller ? server.lua_time_start : mstime();
  18. // slave 直接返回键是否过期
  19. if (server.masterhost != NULL) return now > when;
  20. // master时,键未过期直接返回
  21. if (now <= when) return 0;
  22. // 键过期,删除键
  23. server.stat_expiredkeys++;
  24. // 触发命令传播
  25. propagateExpire(db,key,server.lazyfree_lazy_expire);
  26. // 和键空间事件
  27. notifyKeyspaceEvent(NOTIFY_EXPIRED,
  28. "expired",key,db->id);
  29. // 根据是否懒删除,调用不同的函数
  30. return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) :
  31. dbSyncDelete(db,key);
  32. }

lookupKey 方法则是通过 dictFind 方法从 redisDb 的 dict 哈希表中查找键值,如果能找到,则根据 redis 的 maxmemory_policy 策略来判断是更新 lru 的最近访问时间,还是调用 updateFU 方法更新其他指标,这些指标可以在后续内存不足时对键值进行回收。

  1. robj *lookupKey(redisDb *db, robj *key, int flags) {
  2. // dictFind 根据 key 获取字典的entry
  3. dictEntry *de = dictFind(db->dict,key->ptr);
  4. if (de) {
  5. // 获取 value
  6. robj *val = dictGetVal(de);
  7. // 当处于 rdb aof 子进程复制阶段或者 flags 不是 LOOKUP_NOTOUCH
  8. if (server.rdb_child_pid == -1 &&
  9. server.aof_child_pid == -1 &&
  10. !(flags & LOOKUP_NOTOUCH))
  11. {
  12. // 如果是 MAXMEMORY_FLAG_LFU 则进行相应操作
  13. if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
  14. updateLFU(val);
  15. } else {
  16. // 更新最近访问时间
  17. val->lru = LRU_CLOCK();
  18. }
  19. }
  20. return val;
  21. } else {
  22. return NULL;
  23. }
  24. }

将命令结果写入输出缓冲区

在所有的 redisCommand 执行的最后,一般都会调用 addReply 方法进行结果返回,我们的分析也来到了 Redis 命令执行的返回数据阶段。

addReply 方法做了两件事情:

  • prepareClientToWrite 判断是否需要返回数据,并且将当前 client 添加到等待写返回数据队列中。
  • 调用 _addReplyToBuffer 和 _addReplyObjectToList 方法将返回值写入到输出缓冲区中,等待写入 socekt。
  1. void addReply(client *c, robj *obj) {
  2. if (prepareClientToWrite(c) != C_OK) return;
  3. if (sdsEncodedObject(obj)) {
  4. // 需要将响应内容添加到output buffer中。总体思路是,先尝试向固定buffer添加,添加失败的话,在尝试添加到响应链表
  5. if (_addReplyToBuffer(c,obj->ptr,sdslen(obj->ptr)) != C_OK)
  6. _addReplyObjectToList(c,obj);
  7. } else if (obj->encoding == OBJ_ENCODING_INT) {
  8. .... // 特殊情况的优化
  9. } else {
  10. serverPanic("Wrong obj->encoding in addReply()");
  11. }
  12. }

prepareClientToWrite 首先判断了当前 client是否需要返回数据:

  • Lua 脚本执行的 client 则需要返回值;
  • 如果客户端发送来 REPLY OFF 或者 SKIP 命令,则不需要返回值;
  • 如果是主从复制时的主实例 client,则不需要返回值;
  • 当前是在 AOF loading 状态的假 client,则不需要返回值。

接着如果这个 client 还未处于延迟等待写入 (CLIENT_PENDING_WRITE)的状态,则将其设置为该状态,并将其加入到 Redis 的等待写入返回值客户端队列中,也就是 clients_pending_write队列。

  1. int prepareClientToWrite(client *c) {
  2. // 如果是 lua client 则直接OK
  3. if (c->flags & (CLIENT_LUA|CLIENT_MODULE)) return C_OK;
  4. // 客户端发来过 REPLY OFF 或者 SKIP 命令,不需要发送返回值
  5. if (c->flags & (CLIENT_REPLY_OFF|CLIENT_REPLY_SKIP)) return C_ERR;
  6. // master 作为client 向 slave 发送命令,不需要接收返回值
  7. if ((c->flags & CLIENT_MASTER) &&
  8. !(c->flags & CLIENT_MASTER_FORCE_REPLY)) return C_ERR;
  9. // AOF loading 时的假client 不需要返回值
  10. if (c->fd <= 0) return C_ERR;
  11. // 将client加入到等待写入返回值队列中,下次事件周期会进行返回值写入。
  12. if (!clientHasPendingReplies(c) &&
  13. !(c->flags & CLIENT_PENDING_WRITE) &&
  14. (c->replstate == REPL_STATE_NONE ||
  15. (c->replstate == SLAVE_STATE_ONLINE && !c->repl_put_online_on_ack)))
  16. {
  17. // 设置标志位并且将client加入到 clients_pending_write 队列中
  18. c->flags |= CLIENT_PENDING_WRITE;
  19. listAddNodeHead(server.clients_pending_write,c);
  20. }
  21. // 表示已经在排队,进行返回数据
  22. return C_OK;
  23. }

Redis 将存储等待返回的响应数据的空间,也就是输出缓冲区分成两部分,一个固定大小的 buffer 和一个响应内容数据的链表。在链表为空并且 buffer 有足够空间时,则将响应添加到 buffer 中。如果 buffer 满了则创建一个节点追加到链表上。_addReplyToBuffer 和 _addReplyObjectToList 就是分别向这两个空间写数据的方法。

固定buffer和响应链表,整体上构成了一个队列。这么组织的好处是,既可以节省内存,不需一开始预先分配大块内存,并且可以避免频繁分配、回收内存。

上面就是响应内容写入输出缓冲区的过程,下面看一下将数据从输出缓冲区写入 socket 的过程。

prepareClientToWrite 函数,将客户端加入到了Redis 的等待写入返回值客户端队列中,也就是 clients_pending_write 队列。请求处理的事件处理逻辑就结束了,等待 Redis 下一次事件循环处理时,将响应从输出缓冲区写入到 socket 中。

将命令返回值从输出缓冲区写入 socket

《Redis 事件机制详解》

一文中我们知道,Redis 在两次事件循环之间会调用 beforeSleep 方法处理一些事情,而对 clients_pending_write 列表的处理就在其中。

下面的 aeMain 方法就是 Redis 事件循环的主逻辑,可以看到每次循环时都会调用 beforesleep 方法。

  1. void aeMain(aeEventLoop *eventLoop) { // ae.c
  2. eventLoop->stop = 0;
  3. while (!eventLoop->stop) {
  4. /* 如果有需要在事件处理前执行的函数,那么执行它 */
  5. if (eventLoop->beforesleep != NULL)
  6. eventLoop->beforesleep(eventLoop);
  7. /* 开始处理事件*/
  8. aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP);
  9. }
  10. }

beforeSleep 函数会调用 handleClientsWithPendingWrites 函数来处理 clients_pending_write 列表。

handleClientsWithPendingWrites 方法会遍历 clients_pending_write 列表,对于每个 client 都会先调用 writeToClient 方法来尝试将返回数据从输出缓存区写入到 socekt中,如果还未写完,则只能调用 aeCreateFileEvent 方法来注册一个写数据事件处理器 sendReplyToClient,等待 Redis 事件机制的再次调用。

这样的好处是对于返回数据较少的客户端,不需要麻烦的注册写数据事件,等待事件触发再写数据到 socket,而是在下一次事件循环周期就直接将数据写到 socket中,加快了数据返回的响应速度。

但是从这里也会发现,如果 clients_pending_write 队列过长,则处理时间也会很久,阻塞正常的事件响应处理,导致 Redis 后续命令延时增加。

  1. // 直接将返回值写到client的输出缓冲区中,不需要进行系统调用,也不需要注册写事件处理器
  2. int handleClientsWithPendingWrites(void) {
  3. listIter li;
  4. listNode *ln;
  5. // 获取系统延迟写队列的长度
  6. int processed = listLength(server.clients_pending_write);
  7. listRewind(server.clients_pending_write,&li);
  8. // 依次处理
  9. while((ln = listNext(&li))) {
  10. client *c = listNodeValue(ln);
  11. c->flags &= ~CLIENT_PENDING_WRITE;
  12. listDelNode(server.clients_pending_write,ln);
  13. // 将缓冲值写入client的socket中,如果写完,则跳过之后的操作。
  14. if (writeToClient(c->fd,c,0) == C_ERR) continue;
  15. // 还有数据未写入,只能注册写事件处理器了
  16. if (clientHasPendingReplies(c)) {
  17. int ae_flags = AE_WRITABLE;
  18. if (server.aof_state == AOF_ON &&
  19. server.aof_fsync == AOF_FSYNC_ALWAYS)
  20. {
  21. ae_flags |= AE_BARRIER;
  22. }
  23. // 注册写事件处理器 sendReplyToClient,等待执行
  24. if (aeCreateFileEvent(server.el, c->fd, ae_flags,
  25. sendReplyToClient, c) == AE_ERR)
  26. {
  27. freeClientAsync(c);
  28. }
  29. }
  30. }
  31. return processed;
  32. }

sendReplyToClient 方法其实也会调用 writeToClient 方法,该方法就是将输出缓冲区中的 buf 和 reply 列表中的数据都尽可能多的写入到对应的 socket中。

  1. // 将输出缓冲区中的数据写入socket,如果还有数据未处理则返回C_OK
  2. int writeToClient(int fd, client *c, int handler_installed) {
  3. ssize_t nwritten = 0, totwritten = 0;
  4. size_t objlen;
  5. sds o;
  6. // 仍然有数据未写入
  7. while(clientHasPendingReplies(c)) {
  8. // 如果缓冲区有数据
  9. if (c->bufpos > 0) {
  10. // 写入到 fd 代表的 socket 中
  11. nwritten = write(fd,c->buf+c->sentlen,c->bufpos-c->sentlen);
  12. if (nwritten <= 0) break;
  13. c->sentlen += nwritten;
  14. // 统计本次一共输出了多少子节
  15. totwritten += nwritten;
  16. // buffer中的数据已经发送,则重置标志位,让响应的后续数据写入buffer
  17. if ((int)c->sentlen == c->bufpos) {
  18. c->bufpos = 0;
  19. c->sentlen = 0;
  20. }
  21. } else {
  22. // 缓冲区没有数据,从reply队列中拿
  23. o = listNodeValue(listFirst(c->reply));
  24. objlen = sdslen(o);
  25. if (objlen == 0) {
  26. listDelNode(c->reply,listFirst(c->reply));
  27. continue;
  28. }
  29. // 将队列中的数据写入 socket
  30. nwritten = write(fd, o + c->sentlen, objlen - c->sentlen);
  31. if (nwritten <= 0) break;
  32. c->sentlen += nwritten;
  33. totwritten += nwritten;
  34. // 如果写入成功,则删除队列
  35. if (c->sentlen == objlen) {
  36. listDelNode(c->reply,listFirst(c->reply));
  37. c->sentlen = 0;
  38. c->reply_bytes -= objlen;
  39. if (listLength(c->reply) == 0)
  40. serverAssert(c->reply_bytes == 0);
  41. }
  42. }
  43. // 如果输出的字节数量已经超过NET_MAX_WRITES_PER_EVENT限制,break
  44. if (totwritten > NET_MAX_WRITES_PER_EVENT &&
  45. (server.maxmemory == 0 ||
  46. zmalloc_used_memory() < server.maxmemory) &&
  47. !(c->flags & CLIENT_SLAVE)) break;
  48. }
  49. server.stat_net_output_bytes += totwritten;
  50. if (nwritten == -1) {
  51. if (errno == EAGAIN) {
  52. nwritten = 0;
  53. } else {
  54. serverLog(LL_VERBOSE,
  55. "Error writing to client: %s", strerror(errno));
  56. freeClient(c);
  57. return C_ERR;
  58. }
  59. }
  60. if (!clientHasPendingReplies(c)) {
  61. c->sentlen = 0;
  62. //如果内容已经全部输出,删除事件处理器
  63. if (handler_installed) aeDeleteFileEvent(server.el,c->fd,AE_WRITABLE);
  64. // 数据全部返回,则关闭client和连接
  65. if (c->flags & CLIENT_CLOSE_AFTER_REPLY) {
  66. freeClient(c);
  67. return C_ERR;
  68. }
  69. }
  70. return C_OK;
  71. }

个人博客地址,欢迎查看

Redis 命令执行过程(下)的更多相关文章

  1. Redis 命令执行过程(上)

    今天我们来了解一下 Redis 命令执行的过程.在之前的文章中<当 Redis 发生高延迟时,到底发生了什么>我们曾简单的描述了一条命令的执行过程,本篇文章展示深入说明一下,加深读者对 R ...

  2. Redis 命令执行全过程分析

    今天我们来了解一下 Redis 命令执行的过程.我们曾简单的描述了一条命令的执行过程,本篇文章展示深入说明一下,加深大家对 Redis 的了解. 如下图所示,一条命令执行完成并且返回数据一共涉及三部分 ...

  3. ping命令执行过程详解

    [TOC] ping命令执行过程详解 机器A ping 机器B 同一网段 ping通知系统建立一个固定格式的ICMP请求数据包 ICMP协议打包这个数据包和机器B的IP地址转交给IP协议层(一组后台运 ...

  4. Linux命令执行过程

    目录 一.命令分类 二.命令执行顺序 三.命令分类及查找基本命令 四.命令执行过程 一.命令分类 Linux命令分为两类,具体为内部命令和外部命令 内部命令: 指shell内部集成的命令,此类命令无需 ...

  5. saltstack命令执行过程

    saltstack命令执行过程 具体步骤如下 Salt stack的Master与Minion之间通过ZeroMq进行消息传递,使用了ZeroMq的发布-订阅模式,连接方式包括tcp,ipc salt ...

  6. U-Boot添加menu命令的方法及U-Boot命令执行过程

    转;http://chenxing777414.blog.163.com/blog/static/186567350201141791224740/ 下面以添加menu命令(启动菜单)为例讲解U-Bo ...

  7. 探索Redis设计与实现10:Redis的事件驱动模型与命令执行过程

    本文转自互联网 本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial ...

  8. Linux下shell命令执行过程简介

    Linux是如何寻找命令路径的:http://c.biancheng.net/view/5969.html Linux上命令运行的基本过程:https://blog.csdn.net/hjx5200/ ...

  9. memcahced&redis命令行cmd下的操作

    一.memcahced   1.安装 执行memcached.exe -d install 把memcached加入到服务中 执行memcached.exe -d uninstall 卸载memcac ...

随机推荐

  1. vue登录功能和将商品添加至购物车实现

     2.1: 学子商城--用户登录 用户登录商城用户操作行为,操作用户输入用户名和密码 点击登录按钮,一种情况登录成功 一种情况登录失败 "用户名或密码有误请检查" 2.2:如何实现 ...

  2. HTML——基础知识点1

  3. PowerDesigner列名、注释内容互换

    资料来源:PowerDesigner列名.注释内容互换 文中一共提供了2种操作的代码. (1)将Name中的字符COPY至Comment中 (2)将Comment中的字符COPY至Name中 使用方法 ...

  4. NetCore3.0 文件上传与大文件上传的限制

    NetCore文件上传两种方式 NetCore官方给出的两种文件上传方式分别为“缓冲”.“流式”.我简单的说说两种的区别, 1.缓冲:通过模型绑定先把整个文件保存到内存,然后我们通过IFormFile ...

  5. mongodb存储二进制数据

    mongodb 3.x存储二进制数据并不是以base64的方式,虽然在mongo客户端的查询结果以base64方式显示,请放心使用.下面来分析存储文件的存储内容.base64编码数据会增长1/3成为顾 ...

  6. Win10专业版和企业版的区别

    微软最新的Windows 10版本诸多,包括精简版(S).家庭版(Home).专业版(Pro).企业版(Enterprise),而论功能体验,Win10专业版和企业版无疑是最完善的.那么,Win10专 ...

  7. 小白学 Python 爬虫(3):前置准备(二)Linux基础入门

    人生苦短,我用 Python 前文传送门: 小白学 Python 爬虫(1):开篇 小白学 Python 爬虫(2):前置准备(一)基本类库的安装 Linux 基础 CentOS 官网: https: ...

  8. 并行模式之Guarded Suspension模式

    并行模式之Guarded Suspension模式 一).Guarded Suspension: 保护暂存模式 应用场景:当多个客户进程去请求服务进程时,客户进程的请求速度比服务进程处里请求的速度快, ...

  9. 标准库flag和cobra

    package main import "flag" var b bool var q *bool func init(){ var b bool //方式一 flag.Type( ...

  10. IDEA导入MySQL的jdbc驱动,并操作数据库

    将MySQL的jdbc驱动,导入IDEA的方式,虽然也能连接并且操作数据库,但并不推荐这种方式,推荐使用Maven工程的方式:https://www.cnblogs.com/dadian/p/1193 ...