漫话Redis源码之七十二

2024-02-06 09:38
文章标签 源码 redis 漫话 七十二

本文主要是介绍漫话Redis源码之七十二,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

这里主要是枚举和配置相关的:

configEnum syslog_facility_enum[] = {{"user",    LOG_USER},{"local0",  LOG_LOCAL0},{"local1",  LOG_LOCAL1},{"local2",  LOG_LOCAL2},{"local3",  LOG_LOCAL3},{"local4",  LOG_LOCAL4},{"local5",  LOG_LOCAL5},{"local6",  LOG_LOCAL6},{"local7",  LOG_LOCAL7},{NULL, 0}
};configEnum loglevel_enum[] = {{"debug", LL_DEBUG},{"verbose", LL_VERBOSE},{"notice", LL_NOTICE},{"warning", LL_WARNING},{NULL,0}
};configEnum supervised_mode_enum[] = {{"upstart", SUPERVISED_UPSTART},{"systemd", SUPERVISED_SYSTEMD},{"auto", SUPERVISED_AUTODETECT},{"no", SUPERVISED_NONE},{NULL, 0}
};configEnum aof_fsync_enum[] = {{"everysec", AOF_FSYNC_EVERYSEC},{"always", AOF_FSYNC_ALWAYS},{"no", AOF_FSYNC_NO},{NULL, 0}
};configEnum repl_diskless_load_enum[] = {{"disabled", REPL_DISKLESS_LOAD_DISABLED},{"on-empty-db", REPL_DISKLESS_LOAD_WHEN_DB_EMPTY},{"swapdb", REPL_DISKLESS_LOAD_SWAPDB},{NULL, 0}
};configEnum tls_auth_clients_enum[] = {{"no", TLS_CLIENT_AUTH_NO},{"yes", TLS_CLIENT_AUTH_YES},{"optional", TLS_CLIENT_AUTH_OPTIONAL},{NULL, 0}
};configEnum oom_score_adj_enum[] = {{"no", OOM_SCORE_ADJ_NO},{"yes", OOM_SCORE_RELATIVE},{"relative", OOM_SCORE_RELATIVE},{"absolute", OOM_SCORE_ADJ_ABSOLUTE},{NULL, 0}
};configEnum acl_pubsub_default_enum[] = {{"allchannels", USER_FLAG_ALLCHANNELS},{"resetchannels", 0},{NULL, 0}
};configEnum sanitize_dump_payload_enum[] = {{"no", SANITIZE_DUMP_NO},{"yes", SANITIZE_DUMP_YES},{"clients", SANITIZE_DUMP_CLIENTS},{NULL, 0}
};/* Output buffer limits presets. */
clientBufferLimitsConfig clientBufferLimitsDefaults[CLIENT_TYPE_OBUF_COUNT] = {{0, 0, 0}, /* normal */{1024*1024*256, 1024*1024*64, 60}, /* slave */{1024*1024*32, 1024*1024*8, 60}  /* pubsub */
};/* OOM Score defaults */
int configOOMScoreAdjValuesDefaults[CONFIG_OOM_COUNT] = { 0, 200, 800 };/* Generic config infrastructure function pointers* int is_valid_fn(val, err)*     Return 1 when val is valid, and 0 when invalid.*     Optionally set err to a static error string.* int update_fn(val, prev, err)*     This function is called only for CONFIG SET command (not at config file parsing)*     It is called after the actual config is applied,*     Return 1 for success, and 0 for failure.*     Optionally set err to a static error string.*     On failure the config change will be reverted.*//* Configuration values that require no special handling to set, get, load or* rewrite. */
typedef struct boolConfigData {int *config; /* The pointer to the server config this value is stored in */const int default_value; /* The default value of the config on rewrite */int (*is_valid_fn)(int val, const char **err); /* Optional function to check validity of new value (generic doc above) */int (*update_fn)(int val, int prev, const char **err); /* Optional function to apply new value at runtime (generic doc above) */
} boolConfigData;typedef struct stringConfigData {char **config; /* Pointer to the server config this value is stored in. */const char *default_value; /* Default value of the config on rewrite. */int (*is_valid_fn)(char* val, const char **err); /* Optional function to check validity of new value (generic doc above) */int (*update_fn)(char* val, char* prev, const char **err); /* Optional function to apply new value at runtime (generic doc above) */int convert_empty_to_null; /* Boolean indicating if empty strings shouldbe stored as a NULL value. */
} stringConfigData;typedef struct sdsConfigData {sds *config; /* Pointer to the server config this value is stored in. */const char *default_value; /* Default value of the config on rewrite. */int (*is_valid_fn)(sds val, const char **err); /* Optional function to check validity of new value (generic doc above) */int (*update_fn)(sds val, sds prev, const char **err); /* Optional function to apply new value at runtime (generic doc above) */int convert_empty_to_null; /* Boolean indicating if empty SDS strings shouldbe stored as a NULL value. */
} sdsConfigData;typedef struct enumConfigData {int *config; /* The pointer to the server config this value is stored in */configEnum *enum_value; /* The underlying enum type this data represents */const int default_value; /* The default value of the config on rewrite */int (*is_valid_fn)(int val, const char **err); /* Optional function to check validity of new value (generic doc above) */int (*update_fn)(int val, int prev, const char **err); /* Optional function to apply new value at runtime (generic doc above) */
} enumConfigData;typedef enum numericType {NUMERIC_TYPE_INT,NUMERIC_TYPE_UINT,NUMERIC_TYPE_LONG,NUMERIC_TYPE_ULONG,NUMERIC_TYPE_LONG_LONG,NUMERIC_TYPE_ULONG_LONG,NUMERIC_TYPE_SIZE_T,NUMERIC_TYPE_SSIZE_T,NUMERIC_TYPE_OFF_T,NUMERIC_TYPE_TIME_T,
} numericType;typedef struct numericConfigData {union {int *i;unsigned int *ui;long *l;unsigned long *ul;long long *ll;unsigned long long *ull;size_t *st;ssize_t *sst;off_t *ot;time_t *tt;} config; /* The pointer to the numeric config this value is stored in */int is_memory; /* Indicates if this value can be loaded as a memory value */numericType numeric_type; /* An enum indicating the type of this value */long long lower_bound; /* The lower bound of this numeric value */long long upper_bound; /* The upper bound of this numeric value */const long long default_value; /* The default value of the config on rewrite */int (*is_valid_fn)(long long val, const char **err); /* Optional function to check validity of new value (generic doc above) */int (*update_fn)(long long val, long long prev, const char **err); /* Optional function to apply new value at runtime (generic doc above) */
} numericConfigData;typedef union typeData {boolConfigData yesno;stringConfigData string;sdsConfigData sds;enumConfigData enumd;numericConfigData numeric;
} typeData;typedef struct typeInterface {/* Called on server start, to init the server with default value */void (*init)(typeData data);/* Called on server startup and CONFIG SET, returns 1 on success, 0 on error* and can set a verbose err string, update is true when called from CONFIG SET */int (*set)(typeData data, sds value, int update, const char **err);/* Called on CONFIG GET, required to add output to the client */void (*get)(client *c, typeData data);/* Called on CONFIG REWRITE, required to rewrite the config state */void (*rewrite)(typeData data, const char *name, struct rewriteConfigState *state);
} typeInterface;typedef struct standardConfig {const char *name; /* The user visible name of this config */const char *alias; /* An alias that can also be used for this config */const unsigned int flags; /* Flags for this specific config */typeInterface interface; /* The function pointers that define the type interface */typeData data; /* The type specific data exposed used by the interface */
} standardConfig;#define MODIFIABLE_CONFIG 0 /* This is the implied default for a standard * config, which is mutable. */
#define IMMUTABLE_CONFIG (1ULL<<0) /* Can this value only be set at startup? */
#define SENSITIVE_CONFIG (1ULL<<1) /* Does this value contain sensitive information */standardConfig configs[];/*-----------------------------------------------------------------------------* Enum access functions*----------------------------------------------------------------------------*//* Get enum value from name. If there is no match INT_MIN is returned. */
int configEnumGetValue(configEnum *ce, char *name) {while(ce->name != NULL) {if (!strcasecmp(ce->name,name)) return ce->val;ce++;}return INT_MIN;
}/* Get enum name from value. If no match is found NULL is returned. */
const char *configEnumGetName(configEnum *ce, int val) {while(ce->name != NULL) {if (ce->val == val) return ce->name;ce++;}return NULL;
}/* Wrapper for configEnumGetName() returning "unknown" instead of NULL if* there is no match. */
const char *configEnumGetNameOrUnknown(configEnum *ce, int val) {const char *name = configEnumGetName(ce,val);return name ? name : "unknown";
}/* Used for INFO generation. */
const char *evictPolicyToString(void) {return configEnumGetNameOrUnknown(maxmemory_policy_enum,server.maxmemory_policy);
}/*-----------------------------------------------------------------------------* Config file parsing*----------------------------------------------------------------------------*/int yesnotoi(char *s) {if (!strcasecmp(s,"yes")) return 1;else if (!strcasecmp(s,"no")) return 0;else return -1;
}void appendServerSaveParams(time_t seconds, int changes) {server.saveparams = zrealloc(server.saveparams,sizeof(struct saveparam)*(server.saveparamslen+1));server.saveparams[server.saveparamslen].seconds = seconds;server.saveparams[server.saveparamslen].changes = changes;server.saveparamslen++;
}void resetServerSaveParams(void) {zfree(server.saveparams);server.saveparams = NULL;server.saveparamslen = 0;
}void queueLoadModule(sds path, sds *argv, int argc) {int i;struct moduleLoadQueueEntry *loadmod;loadmod = zmalloc(sizeof(struct moduleLoadQueueEntry));loadmod->argv = zmalloc(sizeof(robj*)*argc);loadmod->path = sdsnew(path);loadmod->argc = argc;for (i = 0; i < argc; i++) {loadmod->argv[i] = createRawStringObject(argv[i],sdslen(argv[i]));}listAddNodeTail(server.loadmodule_queue,loadmod);
}/* Parse an array of CONFIG_OOM_COUNT sds strings, validate and populate* server.oom_score_adj_values if valid.*/static int updateOOMScoreAdjValues(sds *args, const char **err, int apply) {int i;int values[CONFIG_OOM_COUNT];for (i = 0; i < CONFIG_OOM_COUNT; i++) {char *eptr;long long val = strtoll(args[i], &eptr, 10);if (*eptr != '\0' || val < -2000 || val > 2000) {if (err) *err = "Invalid oom-score-adj-values, elements must be between -2000 and 2000.";return C_ERR;}values[i] = val;}/* Verify that the values make sense. If they don't omit a warning but* keep the configuration, which may still be valid for privileged processes.*/if (values[CONFIG_OOM_REPLICA] < values[CONFIG_OOM_MASTER] ||values[CONFIG_OOM_BGCHILD] < values[CONFIG_OOM_REPLICA]) {serverLog(LOG_WARNING,"The oom-score-adj-values configuration may not work for non-privileged processes! ""Please consult the documentation.");}/* Store values, retain previous config for rollback in case we fail. */int old_values[CONFIG_OOM_COUNT];for (i = 0; i < CONFIG_OOM_COUNT; i++) {old_values[i] = server.oom_score_adj_values[i];server.oom_score_adj_values[i] = values[i];}/* When parsing the config file, we want to apply only when all is done. */if (!apply)return C_OK;/* Update */if (setOOMScoreAdj(-1) == C_ERR) {/* Roll back */for (i = 0; i < CONFIG_OOM_COUNT; i++)server.oom_score_adj_values[i] = old_values[i];if (err)*err = "Failed to apply oom-score-adj-values configuration, check server logs.";return C_ERR;}return C_OK;
}void initConfigValues() {for (standardConfig *config = configs; config->name != NULL; config++) {config->interface.init(config->data);}
}void loadServerConfigFromString(char *config) {const char *err = NULL;int linenum = 0, totlines, i;int slaveof_linenum = 0;sds *lines;int save_loaded = 0;lines = sdssplitlen(config,strlen(config),"\n",1,&totlines);for (i = 0; i < totlines; i++) {sds *argv;int argc;linenum = i+1;lines[i] = sdstrim(lines[i]," \t\r\n");/* Skip comments and blank lines */if (lines[i][0] == '#' || lines[i][0] == '\0') continue;/* Split into arguments */argv = sdssplitargs(lines[i],&argc);if (argv == NULL) {err = "Unbalanced quotes in configuration line";goto loaderr;}/* Skip this line if the resulting command vector is empty. */if (argc == 0) {sdsfreesplitres(argv,argc);continue;}sdstolower(argv[0]);/* Iterate the configs that are standard */int match = 0;for (standardConfig *config = configs; config->name != NULL; config++) {if ((!strcasecmp(argv[0],config->name) ||(config->alias && !strcasecmp(argv[0],config->alias)))){if (argc != 2) {err = "wrong number of arguments";goto loaderr;}if (!config->interface.set(config->data, argv[1], 0, &err)) {goto loaderr;}match = 1;break;}}if (match) {sdsfreesplitres(argv,argc);continue;}/* Execute config directives */if (!strcasecmp(argv[0],"bind") && argc >= 2) {int j, addresses = argc-1;if (addresses > CONFIG_BINDADDR_MAX) {err = "Too many bind addresses specified"; goto loaderr;}/* Free old bind addresses */for (j = 0; j < server.bindaddr_count; j++) {zfree(server.bindaddr[j]);}for (j = 0; j < addresses; j++)server.bindaddr[j] = zstrdup(argv[j+1]);server.bindaddr_count = addresses;} else if (!strcasecmp(argv[0],"unixsocketperm") && argc == 2) {errno = 0;server.unixsocketperm = (mode_t)strtol(argv[1], NULL, 8);if (errno || server.unixsocketperm > 0777) {err = "Invalid socket file permissions"; goto loaderr;}} else if (!strcasecmp(argv[0],"save")) {/* We don't reset save params before loading, because if they're not part* of the file the defaults should be used.*/if (!save_loaded) {save_loaded = 1;resetServerSaveParams();}if (argc == 3) {int seconds = atoi(argv[1]);int changes = atoi(argv[2]);if (seconds < 1 || changes < 0) {err = "Invalid save parameters"; goto loaderr;}appendServerSaveParams(seconds,changes);} else if (argc == 2 && !strcasecmp(argv[1],"")) {resetServerSaveParams();}} else if (!strcasecmp(argv[0],"dir") && argc == 2) {if (chdir(argv[1]) == -1) {serverLog(LL_WARNING,"Can't chdir to '%s': %s",argv[1], strerror(errno));exit(1);}} else if (!strcasecmp(argv[0],"logfile") && argc == 2) {FILE *logfp;zfree(server.logfile);server.logfile = zstrdup(argv[1]);if (server.logfile[0] != '\0') {/* Test if we are able to open the file. The server will not* be able to abort just for this problem later... */logfp = fopen(server.logfile,"a");if (logfp == NULL) {err = sdscatprintf(sdsempty(),"Can't open the log file: %s", strerror(errno));goto loaderr;}fclose(logfp);}} else if (!strcasecmp(argv[0],"include") && argc == 2) {loadServerConfig(argv[1], 0, NULL);} else if ((!strcasecmp(argv[0],"slaveof") ||!strcasecmp(argv[0],"replicaof")) && argc == 3) {slaveof_linenum = linenum;sdsfree(server.masterhost);if (!strcasecmp(argv[1], "no") && !strcasecmp(argv[2], "one")) {server.masterhost = NULL;continue;}server.masterhost = sdsnew(argv[1]);char *ptr;server.masterport = strtol(argv[2], &ptr, 10);if (server.masterport < 0 || server.masterport > 65535 || *ptr != '\0') {err = "Invalid master port"; goto loaderr;}server.repl_state = REPL_STATE_CONNECT;} else if (!strcasecmp(argv[0],"list-max-ziplist-entries") && argc == 2){/* DEAD OPTION */} else if (!strcasecmp(argv[0],"list-max-ziplist-value") && argc == 2) {/* DEAD OPTION */} else if (!strcasecmp(argv[0],"rename-command") && argc == 3) {struct redisCommand *cmd = lookupCommand(argv[1]);int retval;if (!cmd) {err = "No such command in rename-command";goto loaderr;}/* If the target command name is the empty string we just* remove it from the command table. */retval = dictDelete(server.commands, argv[1]);serverAssert(retval == DICT_OK);/* Otherwise we re-add the command under a different name. */if (sdslen(argv[2]) != 0) {sds copy = sdsdup(argv[2]);retval = dictAdd(server.commands, copy, cmd);if (retval != DICT_OK) {sdsfree(copy);err = "Target command name already exists"; goto loaderr;}}} else if (!strcasecmp(argv[0],"cluster-config-file") && argc == 2) {zfree(server.cluster_configfile);server.cluster_configfile = zstrdup(argv[1]);} else if (!strcasecmp(argv[0],"client-output-buffer-limit") &&argc == 5){int class = getClientTypeByName(argv[1]);unsigned long long hard, soft;int soft_seconds;if (class == -1 || class == CLIENT_TYPE_MASTER) {err = "Unrecognized client limit class: the user specified ""an invalid one, or 'master' which has no buffer limits.";goto loaderr;}hard = memtoll(argv[2],NULL);soft = memtoll(argv[3],NULL);soft_seconds = atoi(argv[4]);if (soft_seconds < 0) {err = "Negative number of seconds in soft limit is invalid";goto loaderr;}server.client_obuf_limits[class].hard_limit_bytes = hard;server.client_obuf_limits[class].soft_limit_bytes = soft;server.client_obuf_limits[class].soft_limit_seconds = soft_seconds;} else if (!strcasecmp(argv[0],"oom-score-adj-values") && argc == 1 + CONFIG_OOM_COUNT) {if (updateOOMScoreAdjValues(&argv[1], &err, 0) == C_ERR) goto loaderr;} else if (!strcasecmp(argv[0],"notify-keyspace-events") && argc == 2) {int flags = keyspaceEventsStringToFlags(argv[1]);if (flags == -1) {err = "Invalid event class character. Use 'g$lshzxeA'.";goto loaderr;}server.notify_keyspace_events = flags;} else if (!strcasecmp(argv[0],"user") && argc >= 2) {int argc_err;if (ACLAppendUserForLoading(argv,argc,&argc_err) == C_ERR) {char buf[1024];const char *errmsg = ACLSetUserStringError();snprintf(buf,sizeof(buf),"Error in user declaration '%s': %s",argv[argc_err],errmsg);err = buf;goto loaderr;}} else if (!strcasecmp(argv[0],"loadmodule") && argc >= 2) {queueLoadModule(argv[1],&argv[2],argc-2);} else if (!strcasecmp(argv[0],"sentinel")) {/* argc == 1 is handled by main() as we need to enter the sentinel* mode ASAP. */if (argc != 1) {if (!server.sentinel_mode) {err = "sentinel directive while not in sentinel mode";goto loaderr;}queueSentinelConfig(argv+1,argc-1,linenum,lines[i]);}} else {err = "Bad directive or wrong number of arguments"; goto loaderr;}sdsfreesplitres(argv,argc);}/* Sanity checks. */if (server.cluster_enabled && server.masterhost) {linenum = slaveof_linenum;i = linenum-1;err = "replicaof directive not allowed in cluster mode";goto loaderr;}/* To ensure backward compatibility and work while hz is out of range */if (server.config_hz < CONFIG_MIN_HZ) server.config_hz = CONFIG_MIN_HZ;if (server.config_hz > CONFIG_MAX_HZ) server.config_hz = CONFIG_MAX_HZ;sdsfreesplitres(lines,totlines);return;loaderr:fprintf(stderr, "\n*** FATAL CONFIG FILE ERROR (Redis %s) ***\n",REDIS_VERSION);fprintf(stderr, "Reading the configuration file, at line %d\n", linenum);fprintf(stderr, ">>> '%s'\n", lines[i]);fprintf(stderr, "%s\n", err);exit(1);
}/* Load the server configuration from the specified filename.* The function appends the additional configuration directives stored* in the 'options' string to the config file before loading.** Both filename and options can be NULL, in such a case are considered* empty. This way loadServerConfig can be used to just load a file or* just load a string. */
void loadServerConfig(char *filename, char config_from_stdin, char *options) {sds config = sdsempty();char buf[CONFIG_MAX_LINE+1];FILE *fp;/* Load the file content */if (filename) {if ((fp = fopen(filename,"r")) == NULL) {serverLog(LL_WARNING,"Fatal error, can't open config file '%s': %s",filename, strerror(errno));exit(1);}while(fgets(buf,CONFIG_MAX_LINE+1,fp) != NULL)config = sdscat(config,buf);fclose(fp);}/* Append content from stdin */if (config_from_stdin) {serverLog(LL_WARNING,"Reading config from stdin");fp = stdin;while(fgets(buf,CONFIG_MAX_LINE+1,fp) != NULL)config = sdscat(config,buf);}/* Append the additional options */if (options) {config = sdscat(config,"\n");config = sdscat(config,options);}loadServerConfigFromString(config);sdsfree(config);
}

这篇关于漫话Redis源码之七十二的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/683873

相关文章

Redis 基本数据类型和使用详解

《Redis基本数据类型和使用详解》String是Redis最基本的数据类型,一个键对应一个值,它的功能十分强大,可以存储字符串、整数、浮点数等多种数据格式,本文给大家介绍Redis基本数据类型和... 目录一、Redis 入门介绍二、Redis 的五大基本数据类型2.1 String 类型2.2 Hash

Redis中Hash从使用过程到原理说明

《Redis中Hash从使用过程到原理说明》RedisHash结构用于存储字段-值对,适合对象数据,支持HSET、HGET等命令,采用ziplist或hashtable编码,通过渐进式rehash优化... 目录一、开篇:Hash就像超市的货架二、Hash的基本使用1. 常用命令示例2. Java操作示例三

Redis中Set结构使用过程与原理说明

《Redis中Set结构使用过程与原理说明》本文解析了RedisSet数据结构,涵盖其基本操作(如添加、查找)、集合运算(交并差)、底层实现(intset与hashtable自动切换机制)、典型应用场... 目录开篇:从购物车到Redis Set一、Redis Set的基本操作1.1 编程常用命令1.2 集

Redis中的有序集合zset从使用到原理分析

《Redis中的有序集合zset从使用到原理分析》Redis有序集合(zset)是字符串与分值的有序映射,通过跳跃表和哈希表结合实现高效有序性管理,适用于排行榜、延迟队列等场景,其时间复杂度低,内存占... 目录开篇:排行榜背后的秘密一、zset的基本使用1.1 常用命令1.2 Java客户端示例二、zse

Redis中的AOF原理及分析

《Redis中的AOF原理及分析》Redis的AOF通过记录所有写操作命令实现持久化,支持always/everysec/no三种同步策略,重写机制优化文件体积,与RDB结合可平衡数据安全与恢复效率... 目录开篇:从日记本到AOF一、AOF的基本执行流程1. 命令执行与记录2. AOF重写机制二、AOF的

Redis高性能Key-Value存储与缓存利器常见解决方案

《Redis高性能Key-Value存储与缓存利器常见解决方案》Redis是高性能内存Key-Value存储系统,支持丰富数据类型与持久化方案(RDB/AOF),本文给大家介绍Redis高性能Key-... 目录Redis:高性能Key-Value存储与缓存利器什么是Redis?为什么选择Redis?Red

java 恺撒加密/解密实现原理(附带源码)

《java恺撒加密/解密实现原理(附带源码)》本文介绍Java实现恺撒加密与解密,通过固定位移量对字母进行循环替换,保留大小写及非字母字符,由于其实现简单、易于理解,恺撒加密常被用作学习加密算法的入... 目录Java 恺撒加密/解密实现1. 项目背景与介绍2. 相关知识2.1 恺撒加密算法原理2.2 Ja

Nginx屏蔽服务器名称与版本信息方式(源码级修改)

《Nginx屏蔽服务器名称与版本信息方式(源码级修改)》本文详解如何通过源码修改Nginx1.25.4,移除Server响应头中的服务类型和版本信息,以增强安全性,需重新配置、编译、安装,升级时需重复... 目录一、背景与目的二、适用版本三、操作步骤修改源码文件四、后续操作提示五、注意事项六、总结一、背景与

Android实现图片浏览功能的示例详解(附带源码)

《Android实现图片浏览功能的示例详解(附带源码)》在许多应用中,都需要展示图片并支持用户进行浏览,本文主要为大家介绍了如何通过Android实现图片浏览功能,感兴趣的小伙伴可以跟随小编一起学习一... 目录一、项目背景详细介绍二、项目需求详细介绍三、相关技术详细介绍四、实现思路详细介绍五、完整实现代码

Redis 的 SUBSCRIBE命令详解

《Redis的SUBSCRIBE命令详解》Redis的SUBSCRIBE命令用于订阅一个或多个频道,以便接收发送到这些频道的消息,本文给大家介绍Redis的SUBSCRIBE命令,感兴趣的朋友跟随... 目录基本语法工作原理示例消息格式相关命令python 示例Redis 的 SUBSCRIBE 命令用于订