博客园  :: 首页  :: 新随笔  :: 管理

4.2.2 Redis协议与异步交互

Posted on 2023-04-02 02:07  wsg_blog  阅读(78)  评论(0编辑  收藏  举报

Linux C/C++服务器

Redis协议与异步交互

redis网络层

io多路复用(单reactor)+非阻塞io


哪个管道先构成一个完整的数据包(读事件),谁就先得到处理;
1.一个数据包可能由多个读事件才能组装完成
2.管道就是连接
3.人推车相当于网络线程

redis pipeline

redis pipeline 是一个客户端提供的机制(异步请求),而不是服务端提供的;
pipeline 不具备事务性
目的:节约网络传输时间



redis事务

事务:用户定义一系列数据库操作,这些操作视为一个完整的逻辑处理工作单元,要么全部执行,要么全部不执行,是不可分割的工作单元。
MULTI 开启事务,事务执行过程中,单个命令是入队列操作,直到调用 EXEC 才会一起执行;
缺点:WATCH为乐观锁实现,所以失败需要重试,增加业务逻辑的复杂性

命令

  • MULTI(开启事务)
  • EXEC (提交事务)
  • DISCARD (取消事务)
  • WATCH (检测事务) 检测 key 的变动,若在事务执行中,key 变动则取消事务;在事务开启前调用,乐观锁实现(cas);若被取消则事务返回 nil;

应用

事务实现 z pop

WATCH zset
element = ZRANGE zset 0 0
MULTI
ZREM zset element
EXEC

事务实现 加倍操作

WATCH score:10001
val = GET score:10001
MULTI
SET score:10001 val*2
EXEC

lua脚本

lua 脚本满足原子性和隔离性;并不满足一致性(lua不会回滚);持久性和aof配置有关
redis 中加载了一个 lua 虚拟机;用来执行 redis lua 脚本;lua 脚本的执行是原子性的;当某个脚本正在执行的时候,不会有其他命令或者脚本被执行;
lua 脚本当中的命令会直接修改数据状态;
注意:如果项目中使用了 lua 脚本,不需要使用上面的事务命令;

# 从文件中读取 lua脚本内容
cat test1.lua | redis-cli script load --pipe
# 加载 lua脚本字符串 生成 sha1
> script load 'local val = KEYS[1]; return val'
"b8059ba43af6ffe8bed3db65bac35d452f8115d8"
# 检查脚本缓存中,是否有该 sha1 散列值的lua脚本
> script exists "b8059ba43af6ffe8bed3db65bac35d452f8115d8" 1
(integer) 1
# 清除所有脚本缓存
> script flush
OK
# 如果当前脚本运行时间过长,可以通过 script kill 杀死当前运行的脚本
> script kill
(error) NOTBUSY No scripts in execution right now.

EVAL

# 测试使用
EVAL script numkeys key [key ...] arg [arg ...]

EVALSHA

# 线上使用
EVALSHA sha1 numkeys key [key ...] arg [arg ...]

应用
1:项目启动时,建立redis连接并验证后,先加载所有项目中使用的lua脚本(script load);
2:项目中若需要热更新,通过redis-cli script flush;然后可以通过订阅发布功能通知所有服务器重新加载lua脚本;
3:若项目中lua脚本发生阻塞,可通过script kill暂停当前阻塞脚本的执行;

ACID特性分析

  • A 原子性
    事务是一个不可分割的工作单位,事务中的操作要么全部成功,要么全部失败;redis 不支持回滚;即使事务队列中的某个命令在执行期间出现了错误,整个事务也会继续执行下去,直到将事务队列中的所有命令都执行完毕为止。
  • C 一致性
    事务的前后,所有的数据都保持一个一致的状态,不能违反数据的一致性检测;这里的一致性是指预期的一致性而不是异常后的一致性;所以 redis 也不满足;这个争议很大:redis 能确保事务执行前后的数据的完整约束;但是并不满足业务功能上的一致性;比如转账功能,一个扣钱一个加钱;可能出现扣钱执行错误,加钱执行正确,那么最终还是会加钱成功;系统凭空多了钱;
  • I 隔离性
    各个事务之间互相影响的程度;redis 是单线程执行,天然具备隔离性;
  • D 持久性
    redis 只有在 aof 持久化策略的时候,并且需要在redis.conf 中 appendfsync=always 才具备持久性;实际项目中几乎不会使用 aof 持久化策略;

redis异步连接模块

同步连接方案采用阻塞 io 来实现;优点是代码书写是同步的,业务逻辑没有割裂;缺点是阻塞当前线程,直至 redis 返回结果;通常用多个线程来实现线程池来解决效率问题;
异步连接方案采用非阻塞 io 来实现;优点是没有阻塞当前线程,redis 没有返回,依然可以往 redis 发送命令;缺点是代码书写是异步的(回调函数),业务逻辑割裂,可以通过协程解决(openresty,skynet);配合 redis6.0 以后的 io 多线程(前提是有大量并发请求),异步连接池,能更好解决应用层的数据访问性能;

hiredis库

在Redis6.0源码中有个deps/hiredis目录,在此目录下编译libhiredis.so文件
hiredis提供了事件操作的接口,不同平台对事件操作的接口不一致,我们只需要适配这些事件操作接口即可

mkdir build
cd build
cmake ..
make
sudo make install

成功后,会生成libhiredis.so文件

hiredis+reactor 使用

hiredis 提供异步连接方式,提供可以替换 IO 检测的接口;关键替换 addRead, delRead , addWrite, delWrite,cleanup, scheduleTimer,这几个检测接口;其他 io 操作,比如 connect, read, write, close等都交由 hiredis 来处理;同时需要提供连接建立成功以及断开连接的回调;用户可以使用当前项目的网络框架来替换相应的操作;从而实现跟项目网络层兼容的异步连接方案;

hiredis+reactor demo

gcc redis-test-sync.c -o sync -lhiredis      //生成同步连接可执行文件
gcc redis-test-async.c chainbuffer/buffer.c -o async -lhiredis    //生成异步连接可执行文件
gcc redis-cmd-async.c chainbuffer/buffer.c -o cmd-async -lhiredis
cd redis-data
redis-server redis.conf  //启动redis-server
./sync
./async

./sync

./async

适配器原理

适配器代码 adapter_async.h,c语言一贯的封装两个对象的做法,通过地址,强转传输获取数据:我们自己的reactor代码只接收event_t *e指针,那传递的时候类型就为event_t的地址,当我们想获取ctx数据时,通过强转(redis_event_t *)(e),因为首地址都是一样的

typedef struct {
    event_t e;  //reactor event
    int mask;
    redisAsyncContext *ctx;
} redis_event_t;
  1. 创建redis_event_t对象
  2. add_event传入event_t对象指针,放到我们的reactor中去
  3. 我们自己的reactor事件循环,检测到读/写事件触发时,将event_t对象指针强转为redis_event_t指针,从而得到ctx,并触发hiredis的读写接口

hiredis+libevent example

/* Context for a connection to Redis */
typedef struct redisContext {
  const redisContextFuncs *funcs; /* Functiontable */
  int err; /* Error flags, 0 when there is no error */
  char errstr[128]; /* String representation of error when applicable */
  redisFD fd;
  int flags;
  char *obuf; /* Write buffer */
  redisReader *reader; /* Protocol reader */
  enum redisConnectionType connection_type;
  struct timeval *connect_timeout;
  struct timeval *command_timeout;

  struct {
    char *host;
    char *source_addr;
    int port;
  } tcp;

  struct {
    char *path;
  } unix_sock;

  /* For non-blocking connect */
  struct sockadr *saddr;
  size_t addrlen;
  /* Optional data and corresponding destructor users can use to provide * context to a given redisContext. Not used by hiredis. */
  void *privdata;
  void (*free_privdata)(void *);
  /* Internal context pointer presently used by hiredis to manage * SSL connections. */
  void *privctx;
  /* An optional RESP3 PUSH handler */
  redisPushFn *push_cb;
} redisContext;

static int redisLibeventAttach(redisAsyncContext *ac, struct event_base *base) {
  redisContext *c = &(ac->c);
  redisLibeventEvents *e;
  /* Nothing should be attached when something is already attached */
  if (ac->ev.data != NULL)
    return REDIS_ERR;
  /* Create container for context and r/w events */
  e = (redisLibeventEvents*)hi_calloc(1, sizeof(*e));
  if (e == NULL)
    return REDIS_ERR;
  e->context = ac;
  /* Register functions to start/stop listening for events */
  ac->ev.addRead = redisLibeventAddRead;
  ac->ev.delRead = redisLibeventDelRead;
  ac->ev.addWrite = redisLibeventAddWrite;
  ac->ev.delWrite = redisLibeventDelWrite;
  ac->ev.cleanup = redisLibeventCleanup;
  ac->ev.scheduleTimer = redisLibeventSetTimeout;
  ac->ev.data = e;
  /* Initialize and install read/write events*/
  e->ev = event_new(base, c->fd, EV_READ | EV_WRITE, redisLibeventHandler, e);
  e->base = base;
  return REDIS_OK;
}

自定义协议实现

有时候,用户除了需要与项目网络层兼容,同时需要考虑与项目中数据结构契合;这个时候可以考虑自己实现 redis 协议,从解析协议开始转换成项目中的数据结构;下面代码是 Mark 老师在之前项目中的实现;之前项目中实现了一个类似 lua 中 table 的数据对象(SVar),所以希望操作redis 的时候,希望直接传 SVar 对象,然后在协议层进行转换;

协议解压缩

static bool readline(u_char *start, u_char *last, int &pos)
{
  for (pos = 0; start+pos <= last-1; pos++) {
    if (start[pos] == '\r' && start[pos+1] =='\n') {
      pos--;
      return true;
    }
  }
  return false;
}
/*
-2 包解析错误
-1 未读取完整的包
0 正确读取
1 是错误信息
*/
static int read_sub_response(u_char *start, u_char *last, SVar &s, int &usz)
{
  int pos = 0;
  if (!readline(start, last, pos)) return -1;
  u_char *tail = start+pos+1; //
  u_char ch = start[0];
  usz += pos+2+1; // pos+1 + strlen("\r\n")
  switch (ch){
    case '$':
    {
      string str(start+1, tail);
      int len = atoi(str.c_str());
      if (len < 0) return 0; // nil
      if (tail+2+len > last) return -1;
      s = string(tail+2, tail+2+len);
      usz += len+2;
      return 0;
    }
    case '+':
    {
      s = string(start+1, tail);
      return 0;
    }
    case '-':
    {
      s = string(start+1, tail);
      return 1;
    }
    case ':':
    {
      string str(start+1, tail);
      s = atof(str.c_str());
      return 0;
    }
    case '*':
    {
      string str(start+1, tail);
      int n = atoi(str.c_str());
      if (n == 0) return 0; // 空数组
      if (n < 0) return 0; // 超时
      int ok = 0;
      u_char *pt = tail+2;
      for (int i=0; i<n; i++) {
        if (pt > last) return -1;
        int sz = 0;
        SVar t;
        int ret = read_sub_response(pt, last, t, sz);
        if (ret < 0) return -1;
        s.Insert(t);
        usz += sz;
        pt += sz;
        if (ret == 1) ok = 1;
      }
      return ok;
    }
  }
  return -2;
}

static int read_response(SHandle *pHandle, SVar &s, int &size)
{
  int len = pHandle->GetCurBufSize();
  u_char *start = pHandle->m_pBuffer;
  u_char *last = pHandle->m_pBuffer+len;
  return read_sub_response(start, last, s, size);
}

协议压缩

static void write_header(string &req, size_t n)
{
  char chv[16] = {0};
  _itoa(n, chv, 10);
  req.append("\r\n$");
  req.append(chv);
  req.append("\r\n");
}
static void write_count(string &req, size_t n)
{
  char chv[16] = {0};
  _itoa(n, chv, 10);
  req.append("*");
  req.append(chv);
}
static void write_command(string &req, const char *cmd)
{
  int n = strlen(cmd);
  write_header(req, n);
  req.append(cmd);
  //req.append("\r\n");
}
void SRedisClient::RunCommand(const char* cmd, vector<string> &params)
{
  string req;
  size_t nsize = params.size();
  write_count(req, nsize+1);
  write_command(req, cmd);
  for (size_t i = 0; i < params.size(); i++) {
    size_t n = params[i].size();
    write_header(req, n);
    req.append(params[i]);
  }
  req.append("\r\n");
  Send(req);
}