当前位置:   article > 正文

Go与Redis

go与redis

Redis

安装与启动

Windows

我用的是Windows,所以先说说Windows下的安装过程。在多数情况下,你下载到的可能是一个压缩包,安装过程就是找个地方解压出来就OK了。解压之后能看到redis-cli.exe和redis-server.exe这两个文件,这就是Redis的客户端和服务器了。然后在该目录下打开cmd,输入下面的命令启动Redis服务器。

redis-server.exe redis.conf
  • 1

然后你就会看到刷刷刷的输出了很多东西,有些版本甚至会输出一个蛋糕。将这个cmd窗口最小化,注意千万别关了,否则Redis服务器就停了。

redis.conf是Redis服务器默认的配置文件,有些版本可能是redis.windows.conf,后面会详细介绍它。但是你要注意,这两个配置文件是不太一样的,建议学习redis.conf,它才是完整的。默认意味着如果你不写这个参数,Redis也知道用这个配置文件启动服务器。既然可以指定配置文件,也就是说你也可以用自己写的配置文件去启动Redis服务器。

然后还是在原来的地方,再打开一个新的cmd窗口输入如下命令启动客户端。

redis-cli.exe -h 127.0.0.1 -p 6379
  • 1

-h选项指定IP地址,-p选项指定端口号。传说6379这个端口号是有特殊意义的,在九宫格键盘上6379对应着MERZ,它是意大利女歌手Alessia Merz的名字。

如果客户端成功启动,那么你的命令提示符将变成下面这样:

redis 127.0.0.1:6379>
  • 1
Linux

虽然我一直用的是Windows,但是保不齐那天就干到Linux上了,为了防患于未然,还是记录一下Linux下的安装过程。

首先需要输入如下命令:

$ wget http://download.redis.io/releases/redis-2.8.17.tar.gz
$ tar xzf redis-2.8.17.tar.gz
$ cd redis-2.8.17
$ make
  • 1
  • 2
  • 3
  • 4

make完后redis-2.8.17目录下会出现编译后的redis服务程序redis-server,还有用于测试的客户端程序redis-cli。

启动Redis服务器如下,配置文件依然是可自定义的,不带入就使用默认的。

$ ./redis-server redis.conf
  • 1

启动Redis客户端。启动之后命令提示符会变成redis>

$ ./redis-cli
redis>
  • 1
  • 2

如果要检查客户端与服务器是否是连接的,可以在客户端中输入ping这个命令,如果回车后客户端打印出PONG就说明连接成功。Windows下也是一样的。

配置Redis

redis.conf是Redis的配置文件,你完全可以用一个文本编辑器打开它,并进行修改。建议是拷贝一份再修改附件。如果可以,打开这个文件看一看也是不错的,当然对照下面的配置选项说明看会更好。

Redis的配置文件分成了很多模块,这些区分是人为的。另外配置文件的开始处有一段对单位的说明:

# 1k => 1000 bytes
# 1kb => 1024 bytes
# 1m => 1000000 bytes
# 1mb => 1024*1024 bytes
# 1g => 1000000000 bytes
# 1gb => 1024*1024*1024 bytes
#
# units are case insensitive so 1GB 1Gb 1gB are all the same.
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

Redis中带b的单位和不带b的单位大小是不一样的,而且单位不区分大小写。

GENERAL

  1. Redis默认不是以守护进程的方式运行的,通过配置daemonize,将no改成yes启用守护进程,注意这个配置项在Windows下是不支持的。
daemonize no
  • 1
  1. 当Redis以守护进程方式运行时,Redis默认会把pid写入/var/run/redis.pid文件,可以通过pidfile修改。不出意外,这项配置在Windows下也是不受支持的。
pidfile /var/run/redis.pid
  • 1
  1. 指定日志级别,Redis支持4个级别:debugverbosenoticewarning,从前往后日志级别越来越高,输出信息越来越少。默认为notice
loglevel verbose
  • 1
  1. 日志默认是关闭的,而且是被注释掉的。
# syslog-enabled no
  • 1
  1. 日志标识,默认为redis,也是被注释了的。
# syslog-ident redis
  • 1
  1. 日志设备,可以是LOCAL0LOCAL7,默认是LOCAL0。也是被注释掉的。
# syslog-facility local0
  • 1
  1. 设置日志文件名。默认为空字符串,日志将发送到标准输出,这和设置为stdout的效果是一样的。如果以守护进程的方式运行,日志将发送到dev/null而不是stdout
logfile ""
  • 1
  1. 设置数据库的数量,默认16个。默认使用的0号库,可以通过SELECT id命令来选择使用哪个数据库。
databases 16
  • 1

NETWORK

  1. 指定Redis监听端口,默认是6379。
port 6379
  • 1
  1. 绑定的服务器主机地址。
bind 127.0.0.1
  • 1
  1. 设置tcp的backlog,backlog是一个连接队列。backlog队列总和=未完成三次握手队列+已完成三次握手队列。在高并发环境下需要一个高backlog值来避免慢客户端连接问题。注意Linux内核会将这个值减小到/proc/sys/net/core/somaxconn的值,所以需要确保增大somaxconn和tcp_max_syn_backlog两个值来达到想要的效果。
tcp_backlog 511
  • 1
  1. 当客户端闲置多长时间后关闭连接,默认为0,表示关闭超时功能。
timeout 0
  • 1
  1. 每隔多长时间进行一次Keepalive检测,单位为秒,默认为0,建议设置为60。
tcp_keepalive 0
  • 1

LIMITS

  1. 同一时间最大客户端连接数,默认为10000,设置为0标识不做限制。当客户端连接数达到这个限制值时,Redis会关闭新的连接并向客户端返回max number of clients reached错误信息 。

    maxclients 10000
    
    • 1
  2. 最大内存限制。达到最大内存后,Redis会尝试清除已过期或即将过期的键值对,当清除后任然超过内存的最大限制,将无法进行写入,但依然可以读。Redis新的vm机制会将键存放到内存,值存放到swap区。

    maxmemory <bytes>
    
    • 1
  3. 缓存过期清除策略。Redis支持5种缓存过期策略:

    • volatile-lru:针对设置了过期时间的键,使用LRU(least recently used:最近最少使用)算法清除key。
    • allkeys-lru:针对所有键,使用LRU(最近最少使用)算法清除key。
    • volatile-random:针对设置了过期时间的键,在过期集合中随机移除key。
    • allkeys-random:针对所有键,随机移除key。
    • volatile-ttl:清除TTL(time to live:生存时间)值最小的键,即那些最近要过期的key。
    • noeviction:永不过期策略,不清除key,当缓存已满时,针对写操作只是返回错误信息。

    Redis默认使用的是noeviction永不过期策略,实际生产不能这么虎。

    maxmemory-policy noeviction
    
    • 1
  4. 样本数量,LRU算法和TTL算法都不是精确的算法,而是估算值,也即是说LRU算法和TTL算法并不是应用于所有key,而是在一个样本内使用该算法移除样本内应该被移除的key。比如Redis默认会检查5个key,然后选一个最近最少使用的。

    maxmemory-sample 5
    
    • 1

VIRTUAL MEMORY

  1. 是否使用虚拟内存机制,默认为no,不使用。VM机制将数据分页存放,由Redis将访问量较少的页,即冷数据,swap到硬盘上,访问多的页面由磁盘自动换出到内存中。

    vm-enabled no
    
    • 1
  2. 虚拟内存文件路径,默认值为/tmp/redis.swap,不可多个Redis实例共享,如果要启动多个Redis实例,此处的配置可能需要修改。

    vm-swap-file /tmp/redis.swap
    
    • 1
  3. 将所有大于vm-max-memory的数据存入虚拟内存。这里的数据是指值,也就是说该配置是针对值的,无论该值设置的多小,所有索引数据,也就是key,都是内存存储的。默认vm-max-memory设置为0,也就是说,其实所有value都存储在硬盘上。

    vm-max-memory 0
    
    • 1
  4. Redis swap文件的页的大小。Redis swap文件分成了很多page,一个对象可以被保存到多个连续的page上,但一个page不能被不同的对象共享,也就是说一个page上不能存储多于一个的对象。这就好比写论文,一个章节可以写很多页,但是下一个章节必须另起一页,即使前面的页还有空也不行。如果page太大,存储的对象又很小就会造成大量空间浪费,如果page太小,对象很大也不好。建议是如果是存储很多小对象,page最好设置为32或64字节,如果存储的对象很大可以用更大的page,如果不确定就用默认的。

    vm-page-size 32
    
    • 1
  5. swap文件中的page数量。由于页表(表示page是空闲还是已被使用的位图)也存放于内存,所以硬盘上的每8个page就要消耗1字节的内存。因为一个page需要用1bit来表示状态,8个page需要8bit,也就是1字节,把页表称为位图还是很形象的。swap文件的总大小等于vm-page-size * vm-pages,默认配置下,swap文件的大小为4GB,需要16MB的内存来存放页表。

    vm-pages 134217728
    
    • 1
  6. 访问swap文件的线程数。最好不要超过机器核数。如果设置为0,那么所有对swap文件的操作都是串行的,可能造成较大的延时。默认值为4。

    vm-max-threads 4
    
    • 1

INCLUDES

  1. 包含其他配置文件。可以在同一主机上多个Redis实例之间使用同一份配置文件,同时各个实例又可以包含自己特定的配置文件。

    include /path/to/local.conf
    
    • 1

有些配置如果在这里将会让人觉得匪夷所思,所以,我不打算在这里把所有的配置选项都罗列出来。有些配置会在后面讲到相关内容时再做介绍。

最后要说的是,除了可以直接修改配置文件外,Redis也提供了CONFIG GETConfig SET这两个命令来查看和设置配置项。

CONFIG GET后接的是配置项的名字,如果是*则会列出所有配置项。

CONFIG GET loglevel
  • 1

CONFIG SET后接配置项名字和对应的值。

CONFIG SET loglevel "debug"
  • 1

Redis命令

前面讲安装配置Redis时就已经讲过几个命令了,reids-server.exeredis-cli.exe以及和CONFIG有关的命令,还有PING命令。Redis命令和SQL命令一样不区分大小写。下面我会都采用小写。

这里再介绍四个常用命令。

select命令选择数据库。我们知道Redis默认有16个数据库,并且默认使用的是0号数据库。通过select index命令可以选择使用index号库,注意数据库下标是从0开始的。

clear命令清空命令窗口。

echo message打印message。

shutdown命令关闭数据库。

exit命令退出Redis客户端。

flushdb命令清空当前数据库。

flushall命令清空所有数据库。

dbsize命令返回当前数据库的键的数量。

swapdb index index命令交换两个数据库,index是数据库下标,Redis 4.0.0以上版本才有。要查看最新最全的Redis命令,点击传送门

我们一直说Redis有16个数据库,那么我们如何知道正在操作的是几号库呢?Redis没有提供这样的命令,不过我们可以从命令提示符中看出来。启动时默认使用的是0号库,命令提示符如下:

127.0.0.1:6379>
  • 1

当我们切换到1号库时,命令提示符会变成下面这样:

127.0.0.1:6379[1]>
  • 1

命令提示符的端口号后面多了一个[1]提示当前是在1号库,Redis通过这种方式来告诉你当前操作的是几号库。所以Redis没有提供获取当前数据库下标的命令,它认为这是没有意义的。但是编程语言的接口,是提供了这样的API的。

以上都是一些和数据无关的命令,下面介绍的命令都是和键值对有关的命令。

Redis键

Redis是一个存储键值对的数据库。值有五种类型,每种类型的值对应的命令是不同的。而针对键的命令则是通用的。

  • del key [key...]

    删除key以及对应的值。返回成功删除的键值对个数。

  • dump key

    序列化key对应的值,并返回序列化的结果。序列化就是将内存中的对象转化成可以存储到磁盘或进行网络传输的数据,逆过程是反序列化。不要试图去理解序列化出来的数据,你看不懂的。

  • exists key [key...]

    检查key是否存在。返回存在的键的个数。

  • expire key seconds

    设定key经过多少秒以后过期。seconds必须是正整数。

  • expireat key timestamp

    设定key过期的时间点。时间点是Unix时间戳。Unix时间戳(Unix timestamp),或称Unix时间(Unix time)、POSIX时间(POSIX time),是一种时间表示方式,定义为从格林威治时间1970年01月01日00时00分00秒起至现在的总秒数。

  • pexpire key milliseconds

    设定key经过多少毫秒以后过期。milliseconds必须是正整数。

  • pexpireat key milliseconds-timestamp

    设定key的过期时间点,时间点是以毫秒计的Unix时间戳。

  • keys pattern

    查找所有符合patern模式的键。你可以用一个*代表任意数量的任意字符,也可以用?代表任意一个字符。所以keys *会输出所有键。

  • move key db

    将当前数据库的key和对应的值移动到db号库。db不能是当前数据库,否则会出错。

  • persist key

    移除key的过期时间,key将持久保存。

  • ttl key

    以秒为单位返回key剩余的过期时间。ttl–>time to live

  • pttl key

    以毫秒为单位返回key剩余的过期时间。

  • randomkey

    从当前数据库中随机返回一个key。

  • rename key newkey

    将key重命名为newkey。key必须已存在。如果newkey也存在,那么newkey对应的值会被key对应的值覆盖。也就是说原本newkey的值会变成key的值,而key会消失。

  • renamenx key newkey

    仅当newkey不存在时才将key重命名为newkey。

  • type key

    返回key对应值的类型。

Redis数据类型

Redis支持5中数据类型:string(字符串),list(列表),hash(哈希),set(集合),zset(有序集合)。

String:是Redis最基本的数据类型。string类型是二进制安全的,意思是Redis的string可以包含任何类型,比如图片或序列化的对象。字符串最多不超过512M。

记住:一个不存在的key相当于一个空字符串。

  • set key value [ex seconds] [px milliseconds] [nx|xx]

设置一个键值对。ex表示以秒为单位的过期时间,px表示以毫秒为单位的过期时间,expx不能同时设置。nx表示仅当key不存在时才设置,xx表示仅当可以存在时才设置。

  • setex key seconds value

设置键值对的同时指定过期时间。单位为秒,过期时间不能是负数。

  • psetex key milliseconds value

设置键值对的同时指定过期时间。单位为微秒,过期时间不能是负数。

  • setnx key value

仅当key不存在时,才存储这个键值对。

  • get key

获取key对应的值。

  • getset key value

将key的值设置为value,并返回key的旧值。在功能上和set是一样的,也就是说不管key存不存在都会将这个键值对存到数据库。注意只是功能上一样,不是语法。只不过它多了一个get的功能,因此如果键不存在,返回的将是<nil>

  • setrange key offset value

将key对应字符串中下标从offset开始的value长的子串替换成valueoffset可以是任意非负整数,也就是说offset大于原字符串的长度也是可以的,value依然会追加到offet指定的下标处,中间的部分会用\0填充。如果key不存在会创建一个新的键值对。

  • getrange key start end

获取key对应字符串下标从startend之间的子串,包括end,-1表示末尾。。如果下标是负数,会加上字符串的长度再计算,如果加上字符串长度后还是负数,那就直接变成0。负的太离谱Redis也是不伺候的。所以现在知道为什么-1表示末尾了吧。

  • setbit key offset value

设置key对应字符串指定偏移上的比特位。是设置字符串二进制表示中的某一个比特的值,显然value只能是0或1。offset只能是非负整数。从setrangesetbit这两个命令可以看出,Redis对字符串的下标上限似乎没有限制,可以说Redis中字符串是无限长的。

  • getbit key offset:获取key对应字符串指定偏移上的比特位。也就是字符串的二进制表示中,对应偏移上的比特。很显然,返回值只可能是0或1。

  • mset key value [key value ...]

同时设置多个键值对。

  • msetnx key value [key value ...]

仅当所有key都不存在时,才设置这些键值对。

  • mget key [key...]

获取一个或多个键对应的字符串。

  • strlen key

返回key对应字符串的长度。

  • incr key

将key对应的字符串表示的数字加1。只有整数才可以。

  • incrby key increment

将key对应的值加上increment。要求key对应的值和increment都要是整数。

  • incrbyfloat key increment

将key对应的值加上increment。它是incrby的浮点数版本,key对应的值和increment既可以是整数,也可以是浮点数。

  • decr key

将key对应的整数减1。同样只能用于整数。

  • decrby key decrement

将key对应的值减去decrement。要求key对应的值和increment都要是整数。

  • append key value

value追加到key对应的字符串末尾,如果key不存在,这个命令和set没有区别。

List:Redis列表是一个字符串列表。左右都可以添加删除。如果键不存在会创建新链表,如果存在就添加到已有链表。如果链表的所有元素都被移除了,那么链表对应的键也会被删除。也就是说,Redis中是没有空列表的。链表的头尾操作效率是极高的,对中间元素的操作效率要低一些。

记住:一个不存在的key相当于一个空列表。

  • lpush key value [value ...]

头插法创建链表,此时相当于栈,后插入的元素反而在前面。

  • lpushx key value

仅当key对应的链表存在时,将value插入到链表头部。

  • rpush key value [value ...]

尾插法创建链表,相当于队列。在Redis中l(left)代表了链表头,r(right)代表了链表尾。

  • rpushx key value

仅当key对应的链表存在时,将value插入到链表尾部。

  • lpop key

移除并返回key对应链表的第一个元素。

  • rpop key

移除并返回key对应链表的最后一个元素。

  • lrem key count value

从key对应的链表中删除前count个值为value的元素,并返回成功删除的元素个数。如果count>0,就从前往后删;如果coutn<0,就从后往前删;如果count=0就删除所有值为value的元素。如果链表中值为value的元素不足count个,也能删除成功,结果是链表中所有值为value的元素都被删除了。

  • linsert key before|after pivot value

value插入到key对应的链表中从前往后第一个值为pivot的元素之前或之后。从这个命令开头的l就可以看出,它寻找插入位置的顺序是从前往后,并且,即使有多个pivot它也只会插入一次。pivot必须存在于链表中,否则会出错,这是理所当然的。

  • lset key index value

将key对应列表的下标为index的元素设置为value。假设链表长度为n,那么index必须满足-n<index<n-1,否则会出错。如果index是负数,代表的就是从后往前的索引,此时真正的下标是index+n

  • llen key

获取key对应的列表长度。

  • lindex key index

返回key对应链表中下标为index的元素。凡是下标都是同一套规则:假设链表长度为n,那么index必须满足-n<index<n-1,否则会出错。如果index是负数,代表的就是从后往前的索引,此时真正的下标是index+n

  • lrange key start stop

获取key对应链表中下标从startstop(包括stop)的元素。Redis中的起止下标也是同一套规则:如果start>stop,返回空;如果是负数,先加上一个链表长度,得到真正的下标,如果还是负数,就直接是等于0;如果大于链表长度,和等于链表长度是一样的。

  • ltrim key start stop

截取key对应链表中下标从startstop(包括stop)之间的子链表重新赋值给key。也就是将原链表截断了。如果start>stop,得到的是空链表,但是Redis中是没有空链表的,如果链表为空了,那么链表和对应的键都会被删除,所以这相当于是把这个key和对应的链表从数据库中删除了。

  • rpoplpush source destination

从键source对应的链表尾部删除一个元素并插入到键destination对应链表的头部。如果source不存在,那么什么也不会发生;如果destination不存在,则会创建一个新的链表。

  • blpop key [key ...] timeout

lpop命令的阻塞版本,timeout为超时时间。当所有key都不存在时,该命令就会阻塞直到有一个key对应的链表不为空或者超时。由于Redis没有空链表,所以key不存在和空链表是一个意思,换句话说,只要key存在,那它对应的链表就一定不是空。当timeout=0时,将永久阻塞,除非有个一链表不为空。

  • brpop key [key ...] timeout

rpop的阻塞版本。

  • brpoplpush source destination timeout

rpoplpush的阻塞版本。

Hash:一个键对应一个哈希表,哈希表的键和值都是字符串。每个哈希表最多可存储 2 32 − 1 2^{32}-1 2321对键值对。当哈希表中所有键值对都被删除时,对应的键也会被删除。也就是说Redis中没有空哈希表。Redis中似乎不会存在任何空的东西,毕竟一个key对应一个空值是没有任何意义的。

记住:一个不存在的key相当于一个空哈希表。

  • hset key field value

    向key对应的哈希表中添加一个键值对,如果key不存在会新建一个哈希表,如果field已存在,原值会被value覆盖。

  • hsetnx key field value

    当key或field不存在时才将键值对添加到数据库。当key不存在时,新建一个哈希表,当key存在但field不存在时,将键值对添加到key对应的哈希表中。当key和field都存在时,返回0,表示操作失败。

  • hget key field

    获取key对应哈希表中键field对应的值。如果key或field不存在则返回空。

  • hgetall key

    获取key对应哈希表中的所有键值对。

  • hmset key field value [field value ...]

    向key对应的哈希表中添加多个键值对。如果key不存在,会创建一个新哈希表。每个键值对都遵循有则替换,无则添加的规则。

  • hmget key field [field ...]

    从key对应的哈希表中获取所有键field对应的值。所有field都遵循有则返回,无则返回空的原则。

  • hlen key

    返回key对应的哈希表中的键值对数量。

  • hkeys key

    获取key对应哈希表的所有键。

  • hvals key

    获取key对应哈希表的所有值。

  • hexists key field

    如果key和field都存在返回1,只要有一个不存在就返回0。

  • hdel key field [field ...]

    删除key对应哈希表中所有键为field的键值对。返回成功删除的键值对的个数。

  • hincrby key field increment

    将key对应哈希表中field键对应的值加上increment,field对应的值和increment都必须是整数。

  • hincrbyfloat key field increment

    hincrby的浮点数版本,整数浮点数都可以。

  • hscan key cursor [match pattern] [count count]

    迭代key对应哈希表中的所有键值对。

Set:Redis的Set是String类型的无序集合,通过哈希实现,所以集合中不能有重复的数据。增、删、查的时间复杂度都是O(1)。集合的容量为 2 32 − 1 2^{32}-1 2321。同样空集合是不存在。

记住:一个不存在的key相当于一个空集合。

  • sadd key member [member ...]

    向key对应集合中添加一或多个成员,member已存在或重复会被丢弃。返回成功添加的成员个数。

  • srem key member [member ...]

    删除key对应集合中所有值为member的成员。返回成功删除的成员个数。

  • spop key [count]

    随机从集合中弹出count个元素并返回它们。因为集合是无序的,所以删除也是随机的。默认(不带count)随机移除一个成员。count=0时不移除任何成员,返回空;count<0时出错;count大于集合成员总数时,整个集合都会被删除。

  • srandmember key [count]

    从key对应集合中随机返回count元素。它和spop几乎一样,只是它不会把成员从集合中删除。

  • smove source destination member

    将source集合中的成员member移动到destination集合中。如果member不存在,返回0表示操作失败。将member从source集合中删除是一定会做的,但是如果destination集合中已存在member,则它不会被添加到destination集合中,因为集合不允许有重复的成员。如果destination不存在会创建一个新的集合。从结果上来说,最后source中一定没有member了,但是destination中有没有member取决于member是否真的存在。

  • smembers key

    返回key对应集合中的所有成员。

  • sismember key member

    返回member是否在key对应的集合中。如果存在返回1;不存在返回0。

  • scard key

    返回key对应集合中成员的数量。

  • sinter key [key ...]

    返回key对应集合的交集。如果只有一个key,则返回这个key对应的集合。

  • sinterstore destination key [key ...]

    将所有key对应集合的交集存储到destination对应的集合中。如果destination不存在则创建一个新的集合,如果存在会去掉重复的元素。

  • sdiff key [key ...]

    返回key对应集合的差集。注意它返回的是在第一个key对应集合中,但是不在后面的key对应集合中的元素,所以key的顺序是会影响结果的。如果只有一个key,则返回这个key对应的集合。

  • sdiffstore destination key [key ...]

    将所有key对饮集合的差集存储到destination对应的集合中。

  • sunion key [key ...]

    返回key对应集合的交集,并且会去掉重复的元素。

  • sunionstore destination key [key ...]

    将所有key对应集合的交集存储到destination对应的集合中。

  • sscan key cursor [match pattern] [count count]

    迭代集合中的元素。

zset:和集合一样也是string类型元素的集合,且不允许有重复的成员。不同的是每个成员都会关联一个double类型的分数,它们是一个整体。成员正是按分数从小到大排序的。有序集合的成员是唯一的,但分数是可以重复的。有序集合的有序有两层含义:一是成员按分数有序,二是分数相同的成员按字典有序。

记住:一个不存在的key相当于一个空有序集合。

  • zadd key [nx|xx] [ch] [incr] score member [score member ...]

    向key对应集合中添加元素。score必须是数字,如果member已存在,会更新已存在成员的分数,如果命令中有重复的member,那么最后那个的score才是有效的。可选项的含义如下:

    • nx:添加新成员,不会更新已有数据。
    • xx:更新已有成员,不会添加新数据。
    • ch:默认zadd命令的返回值是新添加的成员数量,该命令会将返回值变成被更新的成员的数量,包括新添加的和已存在但分数被更新的。
    • incr:该选项会将zadd命令变成zincrby命令。这意味着使用该选项时,zadd命令将只支持一个分数—成员对。如果member不存在就将成员和分数添加到key对应的有序集合中,如果member存在,则将member的分数加上score。因为key不存在和空集合是一个意思,因此key不存在时,我们说会创建一个新集合,也就是在往一个空集合添加数据,这两者是同一个意思。可以说世间万物都包含在Redis中,所谓的键不存在,只是键对应的值为空。换句话说,任何一个键都是存在于Redis中的,只是有些键对应着非空的值,而其它键都对应着空值。
  • zrem key member [member ...]

    从key对应有序集合中移除member以及成员对应的分数,因为它们是一个整体,成员不存在了,分数也就失去了意义,一荣俱荣,一损俱损。返回值是成功移除的成员的个数。

  • zremrangebyscore key min max

    删除key对应有序集合中分数在minmax之间的成员,包括minmax。在minmax前面加上(表示不包括分数为minmax的成员。

  • zremrangebylex key min max

    lex的含义是字典顺序。这个命令的功能和zremrangebyscore是一样的,不同的是,minmax代表的不是分数了,而是字符串。所有按字典顺序在minmax之间的成员都会被删除。注意minmax之前必须加上([,加(表示不包括minmax,加[表示包括minmax。为什么要有这个命令呢?因为有序集合中成员虽然不能重复,但是分数是可以重复的,当所有成员分数都一样时,所有和score有关的命令都形同虚设,但和lex有关的命令就很有用了。

  • zremrangebyrank key start stop

    不用说,这个命令和zremrangebyscore的功能也是一样的,它会删除有序集合中下标在startstop之间的成员,包括statstop。注意这里是不能加([的。下标从0开始,首先是按分数排,分数相同按字典顺序排。

  • zincrby key increment member

    将key对应集合中的成员member的分数加上score。如果member不存在会添加一个新成员到有序集合中,成员的分数就是score,如果key不存在会创建一个新的有序集合。返回值是member的新分数。

  • zrange key start stop [withscores]

    返回key对应有序集合中下标在startstop之间的成员,包括startstopwithscores选项要求同时返回成员的分数。也就是说,默认返回的成员是不带分数的。

  • zrangebylex key min max [limit offset count]

    返回key对应有序集合中按字典顺序在minmax之间的成员。minmax必须以([开头,(表示不包括minmax[表示包括minmaxlimit选项还可以进行二次过滤,参数含义如下:

    • offset表示偏移量,即从结果的第几个元素开始取值,前offset个元素会被丢弃。
    • count表示取值个数,即从结果中取几个元素作为最终的结果。
  • zrangebyscore key min max [withscores] [limit offset count]

    返回key对应集合中分数在minmax之间的成员,默认包括minmax,如果在minmax之前加上(表示不包括minmaxwithscores选项同时获取成员的分数,limit选项表示从结果的第offset个开始取count个元素作为结果。

  • zrevrange key start stop [withscores]

    逆序返回key对应有序集合中下标在startstop之间的成员,包括startstop。这里的startstop都是倒数的,也就是说,0代表的是最后一个成员。结果的顺序也是倒的,即从大到小,从倒数第start个成员到倒数第stop个成员。withscores选项会同时输出成员的分数。要求start≤stop,否则返回空。

  • zrevrangebyscore key max min [withscores] [limit offset count]

    按照分数递减的顺序返回有序集合中分数在maxmin之间的成员,默认包括max和min,在maxmin之前加上(表示不包括。它是zrangebyscore的逆序版本,除了maxmin的位置相反,其它都和zrangebyscore一样。

  • zrank key member

    获取key对应有序集合中member的下标。排序规则是先按分数排序,分数相同的按字典顺序排序。member不存在返回空。

  • zrevrank key member

    获取member的逆序下标,也就是它是倒数第几个。注意下标从0开始。假设有序集合中公有n个成员,有如下公式: z r e v r a n k = ( n − 1 ) − z r a n k zrevrank=(n-1)-zrank zrevrank=(n1)zrank

  • zscore key member

    获取key对应有序集合中成员member的分数。如果member不存在,返回空。

  • zcard key

    返回key对应有序集合中成员的数量。

  • zcount key min max

    返回key对应有序集合中分数在minmax之间的成员数量,默认包括minmax。在minmax之前加上(表示不包括minmax

  • zlexcount key min max

    返回key对应有序集合中按字典顺序在minmax之间的成员数量。minmax之前必须加上([(表示不包括minmax[表示包括minmax

  • zinterstore destination numkeys key [key ...] [weights weight [weight ...]] [aggregate sum|min|max]

    计算一或多个有序集合的交集并存储到有序集合destination中。计算交集的集合数量由numkeys指定,必须带入的有序集合数,也就是key的个数吻合。weights选项指定每个有序集合的分数权重,权重个数也必须和集合个数吻合。aggregate选项指定计算总分数的方式,具体来说是求和、取最小值还是取最大值。默认情况下所有集合的分数权重都是1,计算总分方式是求和。

  • zunionstore destination numkeys key [key ...] [weights weight [weight ...]] [aggregate sum|min|max]

    计算一或多个有序集合的并集并存储到有序集合destination中。其它和zinterstore命令一样。

  • zscan key cursor [match pattern] [count count]

    迭代有序集合中的元素,每个元素都是一个成员—分数对。

Redis的五种数据类型和相关命令就介绍完了。再总结一下规律,假设值的长度是n。

  • 下标规则:凡是看到index、start、stop都是下标。下标支持-inf+inf,表示负无穷和正无穷。index是比较严格的必须满足 − n ≤ i n d e x ≤ n − 1 -n≤index≤n-1 nindexn1,对于负数来说真正的下标是index+n,也就是说负数代表的从后往前数的。对于start和stop就宽松多了,正负无限制。超过n-1的都是n-1,小于-n的都是0。下标总是包括在内。
  • 范围规则:min和max表示范围。这种范围只出现在有序集合中,分为两种:一种是分数范围,一种是字符串范围,也称为字典范围。分数范围只支持(表示不包括范围边界。而字符串范围必须加上([(表示不包括边界,[表示包括边界。
  • 万物规则:Redis中是没有空值的,因为没有意义,一旦键对应的值为空了,那么键也会被删除。从另一个角度理解,键不存在和键对应的是一个空值是一个意思。我们可以认为Redis包含了世间万物,所有键都存在于Redis中,只不过绝大部分键都对应空值。这样理解的前提是Redis使用键时不用申请,而是直接拿来用,就好比这些键本来就存在一样。所以当我们对一个不存在的键去添加删除元素时,就是在对一个空字符串或空集合或空链表或空哈希表进行添加和删除操作,不会有任何问题。看起来就是我们不是创建了一个新的键值对,而是给一个已存在的键对应的空值添加了数据。

HyperLogLog:这也是Redis的一个结构,用来做基数统计。基数是一个数学概念,简单的说就是一个集合中不重复的元素的个数。

HyperLogLog就是用来做基数统计的算法,优点是即使输入元素的数量或体积非常大,计算基数所需的空间也是固定的,并且非常小。Redis中每个HyperLogLog键只需要12KB的内存就能计算 2 64 2^{64} 264个不同元素的基数。

需要注意的是,HyperLogLog并不会存储元素,只会根据输入来计算基数。也就是说它不具备存储功能,只具备计算功能。这也是没有把它和Redis5中数据类型归在一起的原因。

  • pfadd key element [element ...]

    将一个或多个元素添加到HyperLogLog结构中。

  • pfcount key [key ...]

    返回HyperLogLog结构中元素的基数估算值。

  • pfmerge destkey sourcekey [sourcekey ...]

    将一或多个HyperLogLog合并成一个。

发布订阅

Redis的发布订阅是一种进程间消息通信模式。发布者(pub)发送消息,订阅者(sub)接收消息。订阅者和发布者都是Redis客户端,并且是连接到同一个服务器的。

订阅者订阅的是频道(channel),发布者将消息发送到频道。就像电视上的体育频道一样,主播将体育新闻发送到体育频道,收看体育频道的人就能看到主播发回的资讯。你可以将频道想象成省委书记,其下辖的各地方官员就是订阅者,中央是发布者。中央向省委书记下达了一条指令,省委书记就会立刻传达到各地方官,落实中央的决定。注意中央也是一个大团体,所以频道和客户端总是多对多的关系。当某个客户端向某个频道发送了一条消息,那么频道就会立刻将这条消息发送给所有订阅了这个频道的客户端。

Reids整发布订阅目的是弄消息中间件,但是好像都不用它来做。

下图展示了三个客户端订阅频道1:
在这里插入图片描述

下图展示了向频道1发布消息:
在这里插入图片描述

命令

  • subscribe channel [channel ...]

    订阅一个或多个频道。channel是频道名,随便给个字符串就行。频道跟键一样,本身已是世间万象。

  • unsubscribe channel [channel ...]

    取消订阅一个或多个频道。

  • psubscribe pattern [pattern ...]

    订阅一个或多个频道。不同的时这个命令的频道支持通配符,比如ch*,只要是ch开头的频道就都算是订阅了。

  • punsubscribe pattern [pattern ...]

    取消订阅符合pattern模式的频道。

  • publish channel message

    向频道发布消息。消息是一个字符串,不能有空格,如果非要写空格就用引号引起来。双引号单引号都行。

  • pubsub subcommand [argument [argument ...]]

    查看订阅发布系统状态。subcommand有三种,所以它其实是三个命令:

    • pubsub channels [pattern]

      列出所有符合pattern模式的活跃的频道,活跃的含义是有一个或多个订阅者。如果不指定pattern模式,则列出所有频道。注意这里不包括通过psubscribe命令用通配符订阅的频道,因为一个*就可以表示无穷个频道,所以干脆就不列出来了。

    • pubsub numsub [channel ...]

      列出指定通道及其订阅者数量。如果指定channel,则返回空链表。这里也不包括那些以通配符形式订阅的通道。

    • pubsub numpat

      返回通过psubscribe命令以通配符订阅通道的的客户端的数量。也就是返回有多少个客户端执行了psubscribe命令。

我们来看看怎么玩发布和订阅。首先我们需要两个客户端,也就是打开两个cmd窗口,输入以下命令快速启动Redis客户端。

> redis-cli.exe
  • 1

然后选择一个客户端输入subscribe ch1 ch2订阅两个频道。
在这里插入图片描述

在另一个客户端中输入publish ch1 hello,在后面的客户端就能看到消息了。
在这里插入图片描述

事务

对数据库来说,事务是一组命令的队列,这一组命令要么一起执行成功,要么一起执行失败。同生共死,一荣俱荣,一损俱损。一个事务块中的所有命令都会序列化,按顺序串行化执行而不会被其他命令插入,不许加塞。

Redis事务的特征:

  • 事务是一个单独的隔离操作。事务中的所有命令都会被序列化,按顺序的执行。事务在执行过程中,不会被其他客户端发送来的命令请求打断。
  • 没有隔离级别的概念。事务队列中的命令在没有提交之前都不会被执行,因此也就不存在"事务内的查询要看到事务里的更新,在事务外查询不能看到"这个让人万分头疼的问题。
  • 不保证原子性。首先原子性是指事务中如果有一条命令执行失败,那么所有命令都取消执行,并且所有数据都恢复到命令执行之前的状态。但在Redis中同一个事务中如果有一条命令执行失败,其他命令依然会被执行,且没有回滚,也就是说Redis其实是部分支持事务。后面我们会看到例子。

Redis执行事务的过程:

  • 开始事务
  • 命令入队
  • 执行事务

这个过程就好比去超市购物,开始事务就是推个小推车;命令入队就是把商品放入购物车内;执行事务就是最后一起结账。

命令

  • multi

    标记一个事务块的开始。

  • exec

    执行事务块中的所有命令。

  • discard

    取消事务,放弃执行事务块内的所有命令。

  • watch key [key ...]

    监视一个或多个key,如果在事务执行之前这些key被其他命令改动,那么事务将被打断。

  • unwatch

    取消对所有key的监控。

在Redis中输入multi命令开始一个事务。输入multi回车后Redis总是返回OK响应,告诉你它准备好了。接下来就可以输入一系列命令了。输入命令之后你会得到QUEUED的回应,你输入的这些命令并没有立刻执行而是进入了一个队列。

执行事务块内的命令只需要输入exec命令即可。如果你输入的命令有错误,你依然可以继续往事务中添加命令,但是输入exec时会得到一个错误,且事务块内的所有命令都不会执行。但如果是很隐晦的错误,比如将incr命令应用到了非数字上,对于这类只有在执行时才能发现的错误,执行exec后,出错的命令会报错,但是其它命令会被执行,这也就是为什么说Redis只是部分支持事务的原因。

如果你想放弃此次事务,可以在输入exec之前输入discard命令来放弃这个事务,并退出事务模式。

为了保证数据的并发一致性,也就是有两个人同时修改同一条数据时不能冲突,需要给数据加上锁。watch就是用来保证并发安全的。

锁被分为两种:悲观锁和乐观锁。

悲观锁每次访问数据是都会悲观的以为别人也会修改这条数据。所以每次访问数据时都会加锁,这样别人想访问数据就会阻塞,直到它解锁。悲观锁属于杞人忧天有蛮横自私型的。行锁、表锁、读写锁都是悲观锁。当然包括Go的互斥锁和读写锁。

乐观锁每次访问数据都认为别人不会在这个节骨眼上修改数据,所以它不会加锁,而是在更新数据之前先检查别人是否改动过数据,如果数据被改动过,就放弃此次操作,如果数据未被改动才执行更新。适用于有大量读操作的应用场景,可以提高吞吐量。可以通过版本号机制实现:提交版本号必须大于数据的当前版本号才执行更新。watch就属于乐观锁。也包括Go的原子操作和CAS。

既然watch属于乐观锁,那么在开始事务之前,我们可以通过watch命令对需要更改的key进行监控。事务提交时,一旦发现数据被别的客户端改过了,那么整个事务都会失败,事务内的所有命令也都不会执行。

一旦执行unwatch命令或者exec命令之后,所有对可以的监控都会取消掉。也就是说,一旦输入exec执行了一个事务,下一个事务开始之前必须再次执行watch命令。

安全

可能你觉得奇怪,用了这么久的Redis都没输入过密码?那是因为Redis默认的配置禁用了密码,你可以看redis.conf文件的SECURITY模块,全部都被注释了。

# requirepass foobared
  • 1

你也可以在客户端中输入如下命令查看密码,得到的结果为"",表示并未开启密码。

127.0.0.1:6379> config get requirepass
1) "requirepass"
2) ""
  • 1
  • 2
  • 3

要想启用密码,需要执行以下命令:

127.0.0.1:6379> config set requirepass 123456
  • 1

现在如果想执行Redis命令,必须用auth命令验证密码,只需验证一次:

127.0.0.1:6379> auth 123456
OK
127.0.0.1:6379> set k1 v1
OK
  • 1
  • 2
  • 3
  • 4

再次查看密码,这次就不为空了:

127.0.0.1:6379> config get requirepass
1) "requirepass"
2) "123456"
  • 1
  • 2
  • 3

如果想重新禁用密码,只需要将密码设置为空即可:

127.0.0.1:6379> config set requirepass ""
  • 1

持久化

所谓持久化就是将内存中的数据存储到磁盘。Redis支持两种持久化:RDB和AOF。

RDBRedis DataBase

在指定时间间隔将内存中的数据集快照(Snapshot)写入磁盘。在Redis的安装目录下能看到一个名为dump.rdb的文件,这就是快照文件。它是一个二进制文件,特点是非常紧凑,在Redis启动时,会将dump.rdb文件加载到内存,恢复出快照中的数据。

Redis会单独复制(fork)一个子进程来进行持久化,子进程和父进程拥有相同的数据集。通过子进程将数据写入一个临时RDB文件,待子进程完成持久化过程之后,再用这个临时文件替换上次持久化的快照文件。整个过程中主进程不进行任何IO操作,全部由子进程完成。这保证了极高的性能,但同时由于fork的子进程将数据也克隆了一份,会有两倍的数据膨胀,如果数据本身比较大,那么这是很耗内存的。

快照配置:在redis.conf文件中找到SNAPSHOTTING模块,这里是对RDB的配置。

SNAPSHOTTING

  1. save <seconds> <changes>

    配置快照保存策略。表示如果在seconds秒内数据变更changes次及以上,就保存一次快照。可以同时配置多条save指令,它们之间是或的关系,即只要有一个条件满足了,就会执行一次快照。Redis的默认配置如下:

    • save 900 1:15分钟内修改1次
    • save 300 10:5分钟内修改10次
    • save 60 10000:1分钟内修改1万次

    以上三个条件只要有一个满足,就会触发快照。

    如果想禁用快照,配置save ""即可。

  2. stop-writes-on-bgsave-error yes

    后台数据备份出错时是否禁止前台数据写入,默认是yes。如果配置为no,就表示你不在乎数据一致性。

  3. rdbcompression yes

    是否对快照文件进行压缩存储。默认为yes,Redis采用LZF算法进行压缩。如果不想浪费CPU来压缩,可以配置no关闭此功能。

  4. rdbchecksum yes

    是否对存储的快照文件进行CRC64校验。默认为yes,这样会增加大约10%的性能消耗。如果想获得最大的性能提升,设置为no可以关闭校验。

  5. dbfilename dump.rdb

    快照文件的文件名,默认为dump.rdb。你在安装目录下看到的dump.rdb文件就是它配置的。

  6. dir ./

    快照文件的存储路径,默认为当前路径。这就是你能在安装目录下看到dump.rdb的原因。

手动触发快照

可以手动触发快照的命令有三个:

  • save

    同步保存,将内存的数据写入RDB文件,服务器会阻塞直到保存结束。

  • bgsave

    异步保存,该命令会立即返回OK,然后fork一个子进程保存快照,存储快照的同时也能响应客户端的请求。

  • flushall

    该命令的执行会清空所有数据库,同时也会触发快照,但此时保存的快照是空的,没有意义。

数据恢复

将dump.rdb文件拷贝到Redis安装目录,并启动服务即可。获取快照文件的保存路径可以用如下命令:

127.0.0.1:6379> config get dir
  • 1

如果dump.rdb文件损坏,Redis提供了redis-check-dump命令来修复RDB文件:

> redis-check-dump.exe --fix dump.rdb
  • 1

注意这不是Redis的命令,它和redis-cli.exe是同一级别的,是Redis提供的应用程序,直接在cmd窗口执行。

停止快照

动态停止所有RDB保存规则可以在客户端输入如下命令:

127.0.0.1:6379> config set save ""
  • 1

优点:

  1. RDB是一个非常紧凑的文件,适合用来做备份和灾难恢复,将dump.rdb拷贝到远程主机,当需要恢复数据时,拷贝回来就可以恢复了。
  2. RDB保存快照时,父进程唯一需要做的就是fork一个子进程,然后由子进程执行保存的工作。父进程不需要再做其他IO操作,所以RDB能最大限度提高Redis的性能。
  3. 恢复大数据集时,RDB要比AOF更快。

缺点:

  1. 数据丢失风险大,因为Redis是在一段时间内做一次备份,所以如果意外宕机,最后一次快照到宕机的这段时间内的数据修改会全部丢失。
  2. RDB需要经常fork子进程来保存数据到磁盘上,如果数据集比较大,fork的过程会非常耗时,可能导致服务器在毫秒级甚至秒级不能响应客户端的请求。

AOFAppend Only File

日志的形式记录所有写操作,保存为AOF文件。AOF文件只允许追加,不能改写。Redis启动时会将AOF文件中的命令全部执行一遍,以完成数据恢复。

Redis默认是关闭AOF的,我们可以通过下面的命令启用AOF:

127.0.0.1:6379> config set appendonly yes
  • 1

也可以修改配置文件启动AOF,AOF的配置在redis.conf文件的APPEND ONLY MODE模块。

APPEND ONLY MODE

  1. appendonly no

    是否开启AOF,默认关闭。

  2. appendfilename "appendonly.aof"

    AOF文件名,默认为appendonly.aof。

  3. appendfsync everysec

    AOF同步方式,有三种同步方式,默认为everysec,每秒同步一次。

    • always:每当数据更新时立即记录到磁盘,性能差但数据完整性最好。
    • everysec:每秒同步一次,默认的同步方式。速度快,最多丢失一到两秒的数据。官方推荐使用。
    • no:从不同步,最快但是数据完整性最差。
  4. no-appendfsync-on-rewrite no

    重写时是否允许运用appendfsync追加AOF文件,默认为no,不允许追加,保证数据完整性。

  5. auto-aof-rewrite-percentage 100

    当AOF文件大大小是上次重写后的AOF文件大小的百分之几时重写AOF文件。默认是100,即当新的AOF文件大小增加一倍时触发一次重写。

  6. auto-aof-rewrite-min-size 64mb

    当AOF文件大小超过多大时重写AOF文件,默认是64mb。

  7. aof-load-truncated yes

    当AOF文件损坏时是否能够启动Redis。默认为yes,表示允许启动Redis并尽可能多的恢复数据,同时报告错误。no表示报告错误并拒绝启动Redis。

重写:Rewrite

AOF文件采用追加的方式记录写命令,文件会越来越大。为了避免文件膨胀过快,Redis提供了重写机制,重写会将可以合并的命令进行合并。当AOF文件的大小超过设定的阈值时,Redis就会启动AOF文件重写,只保留可以恢复数据的最小等价指令集。要手动触发重写可以使用如下命令:

bgrewriteaof
  • 1

重写AOF文件只能异步执行。

重写过程:和快照很相似

AOF重写会fork一个新进程,将AOF文件写入一个临时文件。新进程直接根据内存中的数据重写AOF文件,而不会读取旧的AOF文件。主进程会将新的操作同时记录到缓存和旧的AOF文件,这样即使重写失败了,数据也不会丢失。当新进程重写AOF完成后,会向主进程发送一个消息,主进程接收到消息后将缓存中的命令追加到子进程重写的新AOF文件中。最后,Redis将临时文件重命名,完成重写过程。

数据恢复

AOF也是在Redis启动时自动加载的。如果AOF文件和RDB文件同时存在,那么Redis会优先加载AOF文件,因为AOF文件记录的数据更完整。如果AOF文件损坏,Redis会启动失败,通过下面的命令可以修复损坏的AOF文件:

> redis-check-aof --fix appendonly.aof
  • 1

优点:

  1. 配置灵活,可以选择不同的同策略。默认的每秒同步能在数据一致性比较高的的同时保证较好的性能。同步操作在后台异步完成,同时主线程依然可以执行写操作。最多也只有一秒的数据丢失。
  2. AOF以追加的方式将命令写入AOF文件,即使因为某些原因(网络问题或磁盘已满)命令写入出错或者不完整,也不会污染整个文件。你可以通过redis-check-aof --fix命令快速修复AOF文件。
  3. AOF可以在AOF文件变得过大时自动重写。重写时异步且安全的。异步的含义是重写进程是一个新的进程,不会阻塞原有进程。安全的含义是重写会创建一个新的AOF文件来写入当前数据所需的最小指令集,同时新的操作依然会追加到旧的AOF文件,万一重写失败,旧AOF文件还是可以用的。
  4. AOF文件以日志的格式有序的保存了对数据库的所有写操作,包括select选择数据库的命令。这些操作以Redis协议的格式保存,非常容易看懂,对文件进行分析也很轻松。你可以用文本编辑器打开AOF文件进行查看和修改。比如,你不小心执行了flashall命令干掉了所有数据,那么dump.rdb文件肯定是挂掉了的,这时只要没进行重写,你可以关掉数据库,打开AOF文件,删掉AOF文件的最后一条命令,重启服务器,所有的数据就都恢复了。

缺点:

  1. 对于相同的数据集来说,AOF文件通常比RDB文件大。
  2. 在特定的同步模式下,AOF可能比RDB慢。如果禁用同步,AOF可以和RDB一样快。

总结:用RDB还是AOF?

一般而言,如果对数据安全性要求很高,推荐同时使用RDB和AOF。

如果对数据一致性要求不那么高,也就是说可以忍受几分钟内的数据丢失,可以单独使用RDB。

不推荐单独使用AOF,因为它是可能出bug的。

主从复制

我们常说的主从复制或者叫Master/Slaver机制在Redis中叫Replication(复制)。主从复制常用来做读写分离和容灾恢复。

主从复制只需要配置从机,每个Redis服务器天生就是主机,将一个Redis服务器配置成服务器只需要输入如下命令:

slaveof ip port
  • 1

同时我们可以通过info replication命令查看服务器的主从信息。info命令则会输出所有信息。

主从复制有两种结构:一主多从和薪火相传。

一主多从

将多台Redis服务器中的一台作为主机,其它配置为从机。这种模式下,只有主机可以写入,从机不能写入数据,只能读取。主机负责写入,从机负责读取,所谓读写分离,就是这么回事。
在这里插入图片描述

当调用slaveof ip port命令将服务器变成从机后,主机上的所有数据都会复制到从机上,此后,主机上写入数据也会立刻同步到从机。

主机可以写入和读取,从机只能读取,写入直接报错。

当主机宕掉后,从机会原地待命,状态由up变成down。当主机重新上线后,一切自动恢复如初。

如果是从机与主机断开连接,重启后不会恢复从机的身份,也就是说你需要重新输入slaveof命令重新配置一遍。当然也可以写入配置文件,这样服务器启动时将自动作为从机。后面你会看到完整的配置介绍。

反客为主:

在一主多从的模式下,主机宕掉以后,从机会保持原地待命。但这样是不好的。这时我们可以选择一台从机,输入slaveof no one命令将这台服务器变回主机,它是slaveof ip port命令的逆命令。但此时其它从机依然在原地待命,我们还需要在其它主机上执行slave ip port命令,将它们变成新主机的从机,这样整个系统就有可以工作了。

一主多从中心化严重,主机写压力大。

薪火相传

这是一种串联模型,可以减轻主机写压力。
在这里插入图片描述

这种模式下依然只有一个主机,其他服务器的身份还是从机。只不过上一个从机可以作为下一个从机的主机。每个从机都可以接受其他从机的连接和同步请求,该从机作为链条中下一个从机的主机,可以有效减轻主机的写压力。

注意理解这里对减轻主机写压力的理解。并不是说这种模式下从机可以写入数据,事实上还是只有最顶层的那一台主机可以写入数据。这里减轻的是主机将数据同步复制到从机的压力。在一主二从的模式下,主机需要将数据复制到两台从机,而在薪火相传模式下,一台主机只需要将数据复制到一台从机。

其实主从复制模式下从机不是不能写入数据,只是配置文件默认关闭了这一功能。可以修改配置文件使从机可以写入数据,这部分的配置在后面会介绍。但即使是从机可以写入,在薪火相传的模式下,从机A写入的数据对从机B依然是不可见的。也就是说即使从机B和主机之间隔着从机A,从机B依然只复制主机的数据。

如果主机宕掉,那么从机A将变成主机。如果主机重新上线也无济于事。

如果从机中途变更主机,那么之前的数据会被清除,重新拷贝建立最新的数据。

哨兵模式

哨兵模式是一种智能的反客为主。在一主多从模式下,哨兵会监控主机状态,一旦主机宕掉,就立刻通过投票选举得票最多的一个从机作为新主机,其它从机都连接到新主机作为它的从机。

步骤如下:

  1. 新建sentinel.conf文件,名字一定不能错。
  2. sentinel.conf文件中输入sentinel monitor hostname ip port 1。其中hostname是主机名字,自己随便起;ip是主机IP地址;port是主机端口;最后的1表示得票是多于一票就可以成为新主机。如果得票相同,需要重新投票,直到选出新主机。
  3. 输入一下命令启动哨兵:redis-sentinel sentinel.conf

当主机宕掉以后,哨兵体系会通过投票在从机中选取一个从机作为新主机,其它从机自动挂到新主机上。当旧主机重新上线后,它将作为新主机的从机运行。

sentinel.conf文件中可以写多条sentinel monitor命令同时监控多个主机。

最后来看看Redis复制这部分的详细配置。这部分的配置在redis.conf文件的REPLICATION模块。

REPLICATION

  1. slaveof <masterip> <masterport>

    将服务器作为从机,masterip为主机IP地址,masterport为主机端口。

  2. masterauth <master-passworld>

    主机密码,如果主机设置了密码,那么就需要配置该项,否则主机会拒绝从机的请求。

  3. slave-serve-stale-data yes

    从机应答客户端的行为。默认为yes,作为从机的服务器依然可以应答客户端的请求,但是不包括写入数据的命令。如果设置为no,那么服务器将只能应答infoslaveof命令,对其它命令都会回复"SYNC with master in progress"(正在与主机同步)错误。

  4. slave-read-only yes

    设置从机是否为只读模式。默认为yes,从机将只能读数据,不能写数据,前面说的从机不能写就是在这里配置的。当然你也可以设置为no,让从机可以写入数据,但是非常不建议这么干。

  5. repl-diskless-sync no

    全盘同步方式。当新的从机连接到主机或者从机重新连接到主机时,会触发一次全盘同步,也就是主机将内存中的所有数据写入RDB文件,然后发送给从机。全盘同步有两种方式:

    • Disk-backed:基于磁盘的同步,主机会fork一个新新进程将数据写入RDB文件保存到磁盘,然后由主进程将磁盘上的RDB文件发送到从机。
    • Diskless:无磁盘化同步,这个翻译真是太渣了,也就是基于网络的同步。主机fork一个新进程直接将RDB文件通过网络传输给从机,而不是先保存到硬盘。

    默认是no,也就是基于磁盘同步。当网速快于磁盘读写速度时,Diskless的方式要更好。

  6. repl-diskless-sync-delay 5

    如果是在Diskless复制模式下,主机准备好RDB文件之后不会立刻发给从机,而是会先等一会,这很重要,因为当主机准备好RDB文件时,可能有的从机还没连上来。等一会能少同步几次,提高效率。同步等待时间单位是秒,默认等待5秒。设置为0会禁用等待,RDB文件会尽快发送到从机。

  7. repl-ping-slave-period 10

    检测连接时间间隔。从机会在预定的时间间隔内向主机发送PING命令以确定连接状态。默认是10秒。

  8. repl-timeout 60

    复制超时时间。该值必须大于repl-ping-slave-period的值。

  9. repl-disable-tcp-nodelay no

    同步之后禁用从机套接字的TCP_NODELAY。设置为yes的结果是Redis会使用更少的TCP包和更小的带宽将数据发送到从机,但是也会增加数据到达从机的延时。设置为no会减少延时,但增加了带宽。默认为no,Redis认为快更重要。

  10. repl-backlog-size 1mb

设置Backlog缓存的大小,Backlog指待办事项。Backlog是一个缓存,当从机掉线后,对主机的写入就会累记到Backlog缓存中。这样,当从机重新连接后,就不需要全盘重新同步,只需要部分重新同步即可。也就是说不必把主机的所有数据再往从机传一遍,只需要传从机掉线的这段时间错过的数据即可,也即是传Backlog缓存的内容就行了。很显然,Backlog缓存越大,允许从机掉线的时间就越长。当至少有一个从机时才会分配Backlog缓存。默认是1mb。

  1. repl-backlog-ttl 3600

    设置没有从机连接时,Backlog缓存的过期时间。默认是1小时。如果设置为0,表示不清除Backlog缓存。

  2. slave-priority 100

    从机优先级,这是一个整数。我们知道哨兵模式下,当主机宕掉以后,会通过投票选一个从机作为主机,投票就跟这个值有关。Redis会选择该值最小的从机作为新主机。如果设置为0,则该从机永远也不会被哨兵选择,也就是说它只能做从机。默认该值为100。

  3. min-slaves-to-write 3

  4. min-slaves-max-lag 10

    最后这两个配置项是配置的同一个东西,当从机数量太少或者延时太大时,主机为了保证数据一致性会拒绝写入。min-slaves-to-write设置的是最少的从机数量。从机每秒都会向主机发送ping命令,主机会记录最后一次收到每个从机发来ping的时间,由于网络原因,从机发送ping命令可能会滞后,min-slaves-max-lag配置的是最大的滞后时间。这两个配置项表达的含义就是:满足至少有min-slaves-to-write个从机,它们的滞后时间不大于min-slaves-max-lag秒时,主机才接受写入命令,否则,拒绝写入。


Golang使用Redis

前面讲了Redis的一些基础知识,下面看看在Golang中如何玩Redis。GitHub上有很多优秀的Go语言的Redis包,我在这里介绍两个,是我觉得还比较好用的。

monnand/goredis

安装:

go get github.com/monnand/goredis
  • 1

导入goredis包:

import "github.com/monnand/goredis"
  • 1

声明客户端:

var client goredis.Client
  • 1

这是一个开箱即用的库,声明一个客户端就可以直接使用了。Client其实是一个结构体,包含了Redis客户端的一些必要信息。

type Client struct {
    Addr     string        //服务器地址
    Db       int           //数据库编号
    Password string        //密码
    TimeOut  time.Duration //超时时间
    pool     chan net.Conn //连接池
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

通过Client结构体,我们可以指定服务器的IP地址和端口,使用哪个数据库,默认Redis有16个库,密码以及连接服务器的超时时间。最后还提供了一个连接池。

Redis服务器本质上是一个服务器,那么它就应该允许客户端通过TCP进行连接,实际上他们也是通过TCP连接的,客户端通过TCP将命令发送至服务器。goredis通过openConnection函数创建一个TCP连接。

func (client *Client) openConnection() (c net.Conn, err error) {

	var addr = defaultAddr
	if client.Addr != "" {
		addr = client.Addr
	}
	var timeOut = defaultTimeOut
	if client.TimeOut >= 1*time.Second {
		timeOut = client.TimeOut
	}
	c, err = net.DialTimeout("tcp", addr, timeOut)
	if err != nil {
		return
	}

	if client.Db != 0 {
		cmd := fmt.Sprintf("SELECT %d\r\n", client.Db)
		_, err = client.rawSend(c, []byte(cmd))
		if err != nil {
			return
		}
	}

	return
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

这段代码很简单,如果Client没有设置服务器的地址和连接超时时间,那么就会使用默认的值。defaultAddrdefaultTimeout的定义如下:

var defaultAddr = "127.0.0.1:6379"
var defaultTimeOut time.Duration = 3 * time.Second
  • 1
  • 2

现在你知道为什么Client可以开箱即用了吧。

然后调用go的net.DialTimeout方法创建一个TCP连接,这就是客户端和服务器通信的关键证据。最后如果设置了数据库编号,就向服务器发送一条SELECT命令,这正是Redis切换数据库的命令。

既然知道了Client是一个结构体,如果想定制客户端,可以使用下面两种方式:

var client = goredis.Client {
    Addr : "127.0.0.1:6379",
    Db   : 1,
}
  • 1
  • 2
  • 3
  • 4
var client goredis.Client
client.Addr = "127.0.0.1:6379"
client.Db = 1
  • 1
  • 2
  • 3

字符串基本操作

//set的值和Get的值都是字节切片
err := client.Set("a", []byte("hello"))
err = client.Get("a")
type, err := client.Type("a")
ok, err := client.Exists("a")
ok, err = client.Del("a")
var ms = map[string][]byte{
 "a": []byte("65"),
 "b": []byte("66"),
 "c": []byte("67"),
}
//Mset接受的是map类型
client.Mset(ms)
//Mget返回的是字节切片的切片
abc, _ := client.Mget("a", "b", "c")
for _, v := range abc {
 fmt.Println(string(v))
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

链表基本操作

vals := []string{"a", "b", "c", "d", "e"}
for _, v := range vals {
 //值是字节切片类型
 client.Rpush("l", []byte(v))
}
len, err := client.Llen("l")
for i := 0; i < len; i++ {
 val, err := client.Lindex("l", i)
}
//值是字节切片类型
err = client.Lset("l", 0, []byte("aa"))
ok, err := client.Del("l")
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

哈希基本操作

client.Hset("h", "hk", []byte("hv"))
hv, err := client.Hget("h", "hk")
var ms = map[string][]byte{
 "a": []byte("65"),
 "b": []byte("66"),
 "c": []byte("67"),
}
//map的键和值分别对应hash表的键和值
client.Hmset("hm", ms)
//可惜的是没有Hmget这个API
type tt struct {
 A, B, C, D, E string
}
t1 := tt{"aaaaa", "bbbbb", "ccccc", "ddddd", "eeeee"}
//结构体的字段名和值分别对应hash表的键和值
client.Hmset("ht", t1)
var t2 tt
//Hgetall的第二个参数其实是空接口类型,一般带入结构体或map
client.Hgetall("t1", &t2)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

集合基本操作

client.Sadd("sk", []byte("member1"))
var members [][]byte
//Smembers返回的是字节切片的切片
members, err := client.Smembers("sk")
ok, err := client.Sismember("sk", []byte("member1"))
ok, err = client.Srem("sk", []byte("member1"))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

有序集合基本操作

ok, err := client.Zadd("zk", []byte("a"), float64(65))
ok, err := client.Zadd("zk", []byte("b"), float64(66))
ok, err := client.Zadd("zk", []byte("c"), float64(67))
ok, err := client.Zadd("zk", []byte("d"), float64(68))
ok, err := client.Zadd("zk", []byte("e"), float64(69))
score, err := client.Zscore("zk", []byte("a"))
card, err := client.Zcard("zk")
var data [][]byte
data, _ = client.Zrange("zk", 0, 1)
data, _ = client.Zrangebyscore("zk", 0, float64(65))
client.Zincrby("zk", []byte("a"), 1)
ok, err = client.Zrem("zk", []byte("a"))
ok, err = client.Zremrangebyrank("zk", 0, 1)
ok, err = client.Zremrangebyscore("zs", 68, 69)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

发布订阅

发布:

client.Publish("ch1", []byte("hello"))
  • 1

订阅:

sub := make(chan string, 1)
sub <- "ch2"
messages := make(chan goredis.Message, 0)
defer close(sub)
defer close(messages)

go client.Subscribe(sub, nil, nil, nil, messages)
mag <- messages
fmt.println("Received from:", msg.Channel, " Message:", string(msg.Message))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

订阅需要两个通道,一个存储订阅的频道,一个接收消息。

基本上goredis提供的API和Redis的命令的名字是相同的。我只给了部分API的例子,如果熟悉Redis的命令,那么基本上一个Redis命令就有一个同名的API可以用。需要注意的是,通过API无论是写入还是获取,键是字符串类型,但值是字节切片或者字节切片的切片,需要手动转成字符串。因为要进行网络传输,所以要带入字节切片。

gosexy/redis

安装:

go get menteslibres.net/gosexy/redis
  • 1

目前这个网站是下不到这个包了,建议就用前面那个就行。

导入:

import "menteslibres.net/gosexy/redis"
  • 1

连接服务器:

var client *redis.Client
client = redis.New()
err := client.Connect("127.0.0.1", 6379)
  • 1
  • 2
  • 3

显然它不是开箱即用的,在redis包中,Client也是一个结构体,定义如下:

type Client struct {
    redis *conn
}
  • 1
  • 2
  • 3

本质上,客户端就是一个连接。New函数用来创建这样一个结构体,并返回指针。

func New() *Client {
    c := new(Client)
    return c
}
  • 1
  • 2
  • 3
  • 4

Connect函数的本质就是在客户端和服务器之间创建一个TCP连接。

func (c *Client) Connect(host string, port uint) (err error) {
	return c.dial(`tcp`, fmt.Sprintf(`%s:%d`, host, port))
}

func (c *Client) dial(network, address string) error {
	var err error
	if c.redis, err = dial(network, address); err != nil {
		return err
	}
	return nil
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

dial函数是经过简化的,它最核心的任务就是给Client结构体的redis字段赋值,它是一个连接。

/* conn.go */
func dial(network string, address string) (*conn, error) {
	var nc net.Conn
	var err error

	if nc, err = net.Dial(network, address); err != nil {
		return nil, err
	}

	return newConn(nc)
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

最终dial函数返回了一个TCP连接,客户端和服务器之间的连接正式建立起来。

redis包的API和Redis命令的名字也是基本相同的,就不再介绍了。需要注意的是,这个包封装的API为我们做了更多事,那就是它的API会在背后做一个字节切片的强制转换。所以我们可以以字符串作为值调用API,就像使用Redis命令一样,这一点是非常棒的,可惜已经下载不到了。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/511224
推荐阅读
相关标签
  

闽ICP备14008679号