[Redis原始碼閱讀]實現一個redis命令--nonzerodecr

hoohack發表於2018-06-21

上篇文章介紹了命令的執行流程,對redis如何執行命令也有了初步的瞭解,通過實現一個redis命令來再次加深印象。

筆者平時主要語言是PHP,有些功能PHP無法滿足就會用到PHP的擴充套件,比如swoole。因此,就想到redis可不可以以做擴充套件?為了滿足一些特殊的需求,可不可以為redis開發一個命令?

前期準備

因為redis是用C開發的,為了能開發redis命令,首先也是必須的是,你要懂一點C語言基礎,另一個就是,需要了解一下redis命令是如何執行的,知道redis執行命令大概的流程,最簡單的一個流程描述就是:

讀取命令->解析命令->呼叫命令函式->返回執行結果
複製程式碼

或者再讀一次上篇文章

我們要做的就是,確保redis能解析到新增的命令,能根據輸入的命令找到對應的方法並執行。

函式需求

要實現一個命令,說明當前redis的命令無法滿足開發的需求。考慮這樣的一個需求,在秒殺的情景下,達到了這樣的case:商品剩下最後一件,兩個使用者同時搶購,使用業務程式碼:

if (redis->decr(key) >= 0) {
        return success
}
複製程式碼

這樣做有個問題就是活動結束後,key的值可能為-1,這樣對於最終查詢庫存時會出現負值,不利於資料對賬及統計。那麼,能不能新增一個命令,讓redis在計算時判斷key的值,如果是0就不進行扣減呢?

將函式命名為nonzerodecr,開始實現。

新增函式名到命令表

把函式名稱新增到命令表,參照decr命令的命令表:

{"decr",decrCommand,2,"wmF",0,NULL,1,1,1,0,0}
複製程式碼

增加nonzerodecrCommand:

{"nonzerodecr",nonzerodecrCommand,2,"wmF",0,NULL,1,1,1,0,0},
複製程式碼

宣告nonzerodecr,因為新增的命令nonzerodecr只是內部增加一個非0的判斷,其餘操作沒有變化,因此只需要跟decrCommand一樣的宣告即可:

void nonzerodecrCommand(client *c);
複製程式碼

實現函式

在實現新的函式之前,先看看decrComamnd命令的實現:

void decrCommand(client *c) {
    incrDecrCommand(c, -1);
}
複製程式碼

函式呼叫了incrDecrCommand實現自增和自減,實現如下:

/*
* incr、decr具體的實現
*/

void incrDecrCommand(client *c, long long incr) {
  long long value, oldvalue;
  robj *o, *new;

  // 檢查key和value的型別
  o = lookupKeyWrite(c->db,c->argv[1]);
  if (o != NULL && checkType(c,o,OBJ_STRING)) return;
  if (getLongLongFromObjectOrReply(c,o,&value,NULL) != C_OK) return;

  // 處理執行後溢位的情況
  oldvalue = value;
  if ((incr < 0 && oldvalue < 0 && incr < (LLONG_MIN-oldvalue)) ||
      (incr > 0 && oldvalue > 0 && incr > (LLONG_MAX-oldvalue))) {
      addReplyError(c,"increment or decrement would overflow");
      return;
  }

  value += incr;
  /*
  * 在long範圍內,直接賦值,否則使用longlong建立字串後再賦值
  */
  if (o && o->refcount == 1 && o->encoding == OBJ_ENCODING_INT &&
    (value < 0 || value >= OBJ_SHARED_INTEGERS) &&
    value >= LONG_MIN && value <= LONG_MAX) {
    new = o;
    o->ptr = (void*)((long)value);
  } else {
    new = createStringObjectFromLongLong(value);
    if (o) {
        dbOverwrite(c->db,c->argv[1],new);
    } else {
        dbAdd(c->db,c->argv[1],new);
    }
  }
  signalModifiedKey(c->db,c->argv[1]);
  notifyKeyspaceEvent(NOTIFY_STRING,"incrby",c->argv[1],c->db->id);
  server.dirty++;
  addReply(c,shared.colon);
  addReply(c,new);
  addReply(c,shared.crlf);
}

複製程式碼

函式的流程圖如下:

incrDecrCommand

如圖所示,要實現函式nonzerodecrCommand,只需要在進行增/減操作前增加一個大於等於0 的判斷即可,其餘的邏輯不變,實現如下:

void nonzerodecrCommand(client *c) {
    long long incr = -1;
    long long value, oldvalue;
    robj *o, *new;
    // 檢查key和value的型別
    o = lookupKeyWrite(c->db,c->argv[1]);
    if (o != NULL && checkType(c,o,OBJ_STRING)) return;
    if (getLongLongFromObjectOrReply(c,o,&value,NULL) != C_OK) return;
    // 處理執行後溢位的情況
    oldvalue = value;
    if (incr < 0 && oldvalue < 0 && incr < (LLONG_MIN-oldvalue)) {
        addReplyError(c,"increment or decrement would overflow");
        return;
    }
    value += incr;
    // 判斷,如果操作後結果小於0,直接返回
    if (value < 0) {
        addReply(c,shared.czero);
        return;
    }
    
    if (o && o->refcount == 1 && o->encoding == OBJ_ENCODING_INT &&
      (value < 0 || value >= OBJ_SHARED_INTEGERS) &&
      value >= LONG_MIN && value <= LONG_MAX) {
      new = o;
      o->ptr = (void*)((long)value);
    } else {
      new = createStringObjectFromLongLong(value);
      if (o) {
          dbOverwrite(c->db,c->argv[1],new);
      } else {
          dbAdd(c->db,c->argv[1],new);
      }
    }
    signalModifiedKey(c->db,c->argv[1]);
    notifyKeyspaceEvent(NOTIFY_STRING,"incrby",c->argv[1],c->db->id);
    server.dirty++;
    addReply(c,shared.colon);
    addReply(c,new);
    addReply(c,shared.crlf);
}
複製程式碼

編譯測試

編寫完程式碼後,對程式碼進行編譯測試:

127.0.0.1:6379> set totalCount 1
OK
127.0.0.1:6379> get totalCount
"1"
127.0.0.1:6379> nonzerodecr totalCount
(integer) 0
127.0.0.1:6379> get totalCount
"0"
127.0.0.1:6379> nonzerodecr totalCount
(integer) 0
127.0.0.1:6379> get totalCount
"0"
複製程式碼

結果符合最初的需求,在值等於0之後,再進行扣減,值不會變為負數。

總結與思考

通過介紹實現nonzerodecr命令的過程,對如何實現一個命令有了一個初步的認識,之後如果有新的需求也可以根據這個步驟去實現一個新的命令。

上面介紹的命令實現方式比較粗暴,可能會有隱藏的bug,但對於入門實現一個命令這個目的來說,這個程式碼時可以的,另外,一開始提到的秒殺場景除了可以使用新命令來解決,也可以使用redis-lua指令碼的形式來實現,實現方法是多樣的,具體的技術選型需要根據業務的場景來選擇,如果你有更好的方案,歡迎評論留下你的方案。

原創文章,文筆有限,才疏學淺,文中若有不正之處,萬望告知。

如果本文對你有幫助,請點個贊吧,謝謝^_^

更多精彩內容,請關注個人公眾號。

[Redis原始碼閱讀]實現一個redis命令--nonzerodecr

相關文章