下载地址 http://download.redis.io/releases/redis-5.0.7.tar.gz
安装步骤
1、下载获得 redis-5.0.7.tar.gz 后将它放到我们Linux的目录下 /opt ,也可以直接:wget http://download.redis.io/releases/redis-5.0.7.tar.gz
命令下载
2、/opt 目录下,解压命令 : tar -zxvf redis-5.0.7.tar.gz
3、解压完成后出现文件夹:redis-5.0.7
4、进入目录: cd redis-5.0.7
5、在 redis-5.0.7 目录下执行 make 命令
运行make命令时故意出现的错误解析: 1. 安装gcc (gcc是linux下的一个编译程序,是c程序的编译工具) 能上网: yum install gcc-c++ 版本测试: gcc -v 2. 二次make 3. 如果报:Jemalloc/jemalloc.h: 没有那个文件或目录 运行 make distclean 之后再make 4. Redis Test(可以不用执行)
6、如果make完成后继续执行 make install
7、make或者make test可能会报错:You need tcl 8.5 or newer in order to run the Redis test,运行 yum install tcl 命令安装即可
8、查看默认安装目录:usr/local/bin
/usr 这是一个非常重要的目录,类似于windows下的Program Files,存放用户的程序
8、拷贝配置文件(备用)
cd /opt/redis-5.0.7 ls -l # 在redis的解压目录下备份redis.conf mkdir myredis cp redis.conf myredis # 拷一个备份,养成良好的习惯,我们就修改这个文件 # 修改配置保证可以后台应用 vim redis.conf
A、redis.conf配置文件中daemonize守护线程,默认是NO。
B、daemonize是用来指定redis是否要用守护线程的方式启动。
daemonize 设置yes或者no区别
daemonize:yes
daemonize:no
9、启动测试一下!
# 【shell】启动redis服务 [root@192 bin]# cd /usr/local/bin [root@192 bin]# redis-server /opt/redis-5.0.7/redis.conf # redis客户端连接===> 观察地址的变化,如果连接ok,是直接连上的,redis默认端口号 6379 [root@192 bin]# redis-cli -p 6379 127.0.0.1:6379> ping PONG 127.0.0.1:6379> set k1 helloworld OK 127.0.0.1:6379> get k1 "helloworld" # 【shell】ps显示系统当前进程信息 [root@192 myredis]# ps -ef|grep redis root 16005 1 0 04:45 ? 00:00:00 redis-server 127.0.0.1:6379 root 16031 15692 0 04:47 pts/0 00:00:00 redis-cli -p 6379 root 16107 16076 0 04:51 pts/2 00:00:00 grep --color=auto redis # 【redis】关闭连接 127.0.0.1:6379> shutdown not connected> exit # 【shell】ps显示系统当前进程信息 [root@192 myredis]# ps -ef|grep redis root 16140 16076 0 04:53 pts/2 00:00:00 grep --color=auto redis
下载地址:https://github.com/dmajkic/redis/downloads
解压到自己电脑的环境目录即可
双击 redis-server.exe 启动即可
通过客户端去访问 redis-cli
# 基本的set设值 127.0.0.1:6379> set key kuangshen OK # 取出存储的值 127.0.0.1:6379> get key "kuangshen"
1)以前一直有个误区,以为:高性能服务器 一定是多线程来实现的 原因很简单因为误区二导致的:多线程 一定比 单线程 效率高,其实不然! 在说这个事前希望大家都能对 CPU 、 内存 、 硬盘的速度都有了解了!
2)redis 核心就是 如果我的数据全都在内存里,我单线程的去操作 就是效率最高的,为什么呢,因为 多线程的本质就是 CPU 模拟出来多个线程的情况,这种模拟出来的情况就有一个代价,就是上下文的切 换,对于一个内存的系统来说,它没有上下文的切换就是效率最高的。redis 用 单个CPU 绑定一块内存 的数据,然后针对这块内存的数据进行多次读写的时候,都是在一个CPU上完成的,所以它是单线程处 理这个事。在内存的情况下,这个方案就是最佳方案。
因为一次CPU上下文的切换大概在 1500ns 左右。从内存中读取 1MB 的连续数据,耗时大约为 250us, 假设1MB的数据由多个线程读取了1000次,那么就有1000次时间上下文的切换,那么就有1500ns * 1000 = 1500us ,我单线程的读完1MB数据才250us ,你光时间上下文的切换就用了1500us了,我还不 算你每次读一点数据 的时间。
常用命令说明:
get、set、del、append、strlen、incr、decr、incrby、decrby
getrange、setrange、setex(设置过期时间)、ttl(查看剩余时间)、
setnx(设置值,成功返回1,失败,返回0,value还是原来的值)
mset、mset、msetnx (都成功,返回 1 。有一个失败,就返回 0 ,原子操作)
getset(先get再set,返回的是原先的value,再次get查询的时候,才会显示最新的value)
# =================================================== # set、get、del、append、strlen # =================================================== 127.0.0.1:6379> set key1 value1 # 设置值 OK 127.0.0.1:6379> get key1 # 获得key "value1" 127.0.0.1:6379> del key1 # 删除key (integer) 1 127.0.0.1:6379> keys * # 查看全部的key (empty list or set) 127.0.0.1:6379> exists key1 # 确保 key1 不存在 (integer) 0 127.0.0.1:6379> append key1 "hello" # 对不存在的 key 进行 APPEND ,等同于 SET key1 "hello" (integer) 5 # 字符长度 127.0.0.1:6379> APPEND key1 "-2333" # 对已存在的字符串进行 APPEND (integer) 10 # 长度从 5 个字符增加到 10 个字符 127.0.0.1:6379> get key1 "hello-2333" 127.0.0.1:6379> STRLEN key1 # # 获取字符串的长度 (integer) 10 # =================================================== # incr、decr 一定要是数字才能进行加减,+1 和 -1。 # incrby、decrby 命令将 key 中储存的数字加上指定的增量值。 # =================================================== 127.0.0.1:6379> set views 0 # 设置浏览量为0 OK 127.0.0.1:6379> incr views # 浏览 + 1 (integer) 1 127.0.0.1:6379> incr views # 浏览 + 1 (integer) 2 127.0.0.1:6379> decr views # 浏览 - 1 (integer) 1 127.0.0.1:6379> incrby views 10 # +10 (integer) 11 127.0.0.1:6379> decrby views 10 # -10 (integer) 1 # =================================================== # range [范围] # getrange 获取指定区间范围内的值,类似between...and的关系,从零到负一表示全部 # =================================================== 127.0.0.1:6379> set key2 abcd123456 # 设置key2的值 OK 127.0.0.1:6379> getrange key2 0 -1 # 获得全部的值 "abcd123456" 127.0.0.1:6379> getrange key2 0 2 # 截取部分字符串 "abc" # =================================================== # setrange 设置指定区间范围内的值,格式是setrange key值 具体值 # =================================================== 127.0.0.1:6379> get key2 "abcd123456" 127.0.0.1:6379> SETRANGE key2 1 xx # 替换值 (integer) 10 127.0.0.1:6379> get key2 "axxd123456" # =================================================== # setex(set with expire)键秒值 # setnx(set if not exist) # =================================================== 127.0.0.1:6379> setex key3 60 expire # 设置过期时间 OK 127.0.0.1:6379> ttl key3 # 查看剩余的时间 (integer) 55 127.0.0.1:6379> setnx mykey "redis" # 如果不存在就设置,成功返回1 (integer) 1 127.0.0.1:6379> setnx mykey "mongodb" # 如果存在就设置,失败返回0 (integer) 0 127.0.0.1:6379> get mykey "redis" # =================================================== # mset 命令用于同时设置一个或多个 key-value 对。 # mget 命令返回所有(一个或多个)给定 key 的值。 # 如果给定的 key 里面,有某个 key 不存在,那么这个 key 返回特殊值 nil 。 # msetnx 当所有 key 都成功设置,返回 1 。 # 如果所有给定 key 都设置失败(至少有一个 key 已经存在),那么返回 0 。原子操作 # =================================================== 127.0.0.1:6379> mset k10 v10 k11 v11 k12 v12 OK 127.0.0.1:6379> keys * 1) "k12" 2) "k11" 3) "k10" 127.0.0.1:6379> mget k10 k11 k12 k13 1) "v10" 2) "v11" 3) "v12" 4) (nil) 127.0.0.1:6379> msetnx k10 v10 k15 v15 # 原子性操作! (integer) 0 127.0.0.1:6379> get key15 (nil) # 传统对象缓存 set user:1 value(json数据) # 可以用来缓存对象 mset user:1:name zhangsan user:1:age 2 mget user:1:name user:1:age # =================================================== # getset(先get再set) # =================================================== 127.0.0.1:6379> getset db mongodb # 没有旧值,返回 nil (nil) 127.0.0.1:6379> get db "mongodb" 127.0.0.1:6379> getset db redis # 返回旧值 mongodb "mongodb" 127.0.0.1:6379> get db "redis"
String数据结构是简单的key-value类型,value其实不仅可以是String,也可以是数字。
常规key-value缓存应用: 常规计数,微博数,粉丝数等
lpush(左边插入),rpush,lpop,rpop
lrange(范围查询),lindex(根据下标获取元素),llen(长度),lrem(移除)
Ltrim(对list进行范围裁剪),rpoplpush,lset,linsert(某一个元素前/后插入)
# =================================================== # Lpush:将一个或多个值插入到列表头部。(左) # rpush:将一个或多个值插入到列表尾部。(右) # lrange:返回列表中指定区间内的元素,区间以偏移量 START 和 END 指定。 # 其中 0 表示列表的第一个元素, 1 表示列表的第二个元素,以此类推。 # 你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。 # =================================================== 127.0.0.1:6379> LPUSH list "one" (integer) 1 127.0.0.1:6379> LPUSH list "two" (integer) 2 127.0.0.1:6379> RPUSH list "right" (integer) 3 127.0.0.1:6379> Lrange list 0 -1 1) "two" 2) "one" 3) "right" 127.0.0.1:6379> Lrange list 0 1 1) "two" 2) "one" # =================================================== # lpop 命令用于移除并返回列表的第一个元素。当列表 key 不存在时,返回 nil 。 # rpop 移除列表的最后一个元素,返回值为移除的元素。 # =================================================== 127.0.0.1:6379> Lpop list "two" 127.0.0.1:6379> Rpop list "right" 127.0.0.1:6379> Lrange list 0 -1 1) "one" # =================================================== # Lindex,按照索引下标获得元素(-1代表最后一个,0代表是第一个) # =================================================== 127.0.0.1:6379> Lindex list 1 (nil) 127.0.0.1:6379> Lindex list 0 "one" 127.0.0.1:6379> Lindex list -1 "one" # =================================================== # llen 用于返回列表的长度。 # =================================================== 127.0.0.1:6379> flushdb OK 127.0.0.1:6379> Lpush list "one" (integer) 1 127.0.0.1:6379> Lpush list "two" (integer) 2 127.0.0.1:6379> Lpush list "three" (integer) 3 127.0.0.1:6379> Llen list # 返回列表的长度 (integer) 3 # =================================================== # lrem key 根据参数 COUNT 的值,移除列表中与参数 VALUE 相等的元素。 # =================================================== 127.0.0.1:6379> lrem list 1 "two" (integer) 1 127.0.0.1:6379> Lrange list 0 -1 1) "three" 2) "one # =================================================== # Ltrim key 对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。 # =================================================== 127.0.0.1:6379> RPUSH mylist "hello" (integer) 1 127.0.0.1:6379> RPUSH mylist "hello" (integer) 2 127.0.0.1:6379> RPUSH mylist "hello2" (integer) 3 127.0.0.1:6379> RPUSH mylist "hello3" (integer) 4 127.0.0.1:6379> ltrim mylist 1 2 OK 127.0.0.1:6379> lrange mylist 0 -1 1) "hello" 2) "hello2" # =================================================== # rpoplpush 移除列表的最后一个元素,并将该元素添加到另一个列表并返回。 # =================================================== 127.0.0.1:6379> rpush mylist "hello" (integer) 1 127.0.0.1:6379> rpush mylist "foo" (integer) 2 127.0.0.1:6379> rpush mylist "bar" (integer) 3 127.0.0.1:6379> rpoplpush mylist myotherlist "bar" 127.0.0.1:6379> lrange mylist 0 -1 1) "hello" 2) "foo" 127.0.0.1:6379> lrange myotherlist 0 -1 1) "bar" # =================================================== # lset key index value 将列表 key 下标为 index 的元素的值设置为 value 。 # =================================================== 127.0.0.1:6379> exists list # 对空列表(key 不存在)进行 LSET (integer) 0 127.0.0.1:6379> lset list 0 item # 报错 (error) ERR no such key 127.0.0.1:6379> lpush list "value1" # 对非空列表进行 LSET (integer) 1 127.0.0.1:6379> lrange list 0 0 1) "value1" 127.0.0.1:6379> lset list 0 "new" # 更新值 OK 127.0.0.1:6379> lrange list 0 0 1) "new" 127.0.0.1:6379> lset list 1 "new" # index 超出范围报错 (error) ERR index out of range # =================================================== # linsert key before/after pivot value 用于在列表的元素前或者后插入元素。 # 将值 value 插入到列表 key 当中,位于值 pivot 之前或之后。 # =================================================== redis> RPUSH mylist "Hello" (integer) 1 redis> RPUSH mylist "World" (integer) 2 redis> LINSERT mylist BEFORE "World" "There" (integer) 3 redis> LRANGE mylist 0 -1 1) "Hello" 2) "There" 3) "World"
性能总结
它是一个字符串链表,left,right 都可以插入添加
如果键不存在,创建新的链表
如果键已存在,新增内容
如果值全移除,对应的键也就消失了
链表的操作无论是头和尾效率都极高,但假如是对中间元素进行操作,效率就很惨淡了。
list就是链表,略有数据结构知识的人都应该能理解其结构。使用Lists结构,我们可以轻松地实现最新消 息排行等功能。List的另一个应用就是消息队列,可以利用List的PUSH操作,将任务存在List中,然后工 作线程再用POP操作将任务取出进行执行。Redis还提供了操作List中某一段的api,你可以直接查询,删 除List中某一段的元素。
Redis的list是每个子元素都是String类型的双向链表,可以通过push和pop操作从列表的头部或者尾部 添加或者删除元素,这样List即可以作为栈,也可以作为队列。
sadd,smembers(返回所有成员),sismember,scard(获取元素个数),srandmember(返回随机元素)
srem(移除一个或者多个元素),spop(移除一个或者多个随机元素),smove(移动)
sdiff(差集),sinter(交集),sunion(并集)
# =================================================== # sadd 将一个或多个成员元素加入到集合中,不能重复 # smembers 返回集合中的所有的成员。 # sismember 命令判断成员元素是否是集合的成员。 # =================================================== 127.0.0.1:6379> sadd myset "hello" (integer) 1 127.0.0.1:6379> sadd myset "kuangshen" (integer) 1 127.0.0.1:6379> sadd myset "kuangshen" (integer) 0 127.0.0.1:6379> SMEMBERS myset 1) "kuangshen" 2) "hello" 127.0.0.1:6379> SISMEMBER myset "hello" (integer) 1 127.0.0.1:6379> SISMEMBER myset "world" (integer) 0 # =================================================== # scard,获取集合里面的元素个数 # =================================================== 127.0.0.1:6379> scard myset (integer) 2 # =================================================== # srem key value 用于移除集合中的一个或多个成员元素 # =================================================== 127.0.0.1:6379> srem myset "kuangshen" (integer) 1 127.0.0.1:6379> SMEMBERS myset 1) "hello" # =================================================== # srandmember key 命令用于返回集合中的一个随机元素。 # =================================================== 127.0.0.1:6379> SMEMBERS myset 1) "kuangshen" 2) "world" 3) "hello" 127.0.0.1:6379> SRANDMEMBER myset "hello" 127.0.0.1:6379> SRANDMEMBER myset 2 1) "world" 2) "kuangshen" 127.0.0.1:6379> SRANDMEMBER myset 2 1) "kuangshen" 2) "hello" # =================================================== # spop key 用于移除集合中的指定 key 的一个或多个随机元素 # =================================================== 127.0.0.1:6379> SMEMBERS myset 1) "kuangshen" 2) "world" 3) "hello" 127.0.0.1:6379> spop myset "world" 127.0.0.1:6379> spop myset "kuangshen" 127.0.0.1:6379> spop myset "hello" # =================================================== # smove SOURCE DESTINATION MEMBER # 将指定成员 member 元素从 source 集合移动到 destination 集合。 # =================================================== 127.0.0.1:6379> sadd myset "hello" (integer) 1 127.0.0.1:6379> sadd myset "world" (integer) 1 127.0.0.1:6379> sadd myset "kuangshen" (integer) 1 127.0.0.1:6379> sadd myset2 "set2" (integer) 1 127.0.0.1:6379> smove myset myset2 "kuangshen" (integer) 1 127.0.0.1:6379> SMEMBERS myset 1) "world" 2) "hello" 127.0.0.1:6379> SMEMBERS myset2 1) "kuangshen" 2) "set2" # =================================================== # - 数字集合类 # - 差集: sdiff # - 交集: sinter # - 并集: sunion # =================================================== 127.0.0.1:6379> sadd key1 "a" (integer) 1 127.0.0.1:6379> sadd key1 "b" (integer) 1 127.0.0.1:6379> sadd key1 "c" (integer) 1 127.0.0.1:6379> sadd key2 "c" (integer) 1 127.0.0.1:6379> sadd key2 "d" (integer) 1 127.0.0.1:6379> sadd key2 "e" (integer) 1 127.0.0.1:6379> SDIFF key1 key2 # 差集 1) "a" 2) "b" 127.0.0.1:6379> SINTER key1 key2 # 交集 1) "c" 127.0.0.1:6379> SUNION key1 key2 # 并集 1) "a" 2) "b" 3) "c" 4) "e" 5) "d"
在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis还为 集合提供了求交集、并集、差集等操作,可以非常方便的实现如共同关注、共同喜好、二度好友等功 能,对上面的所有集合操作,你还可以使用不同的命令选择将结果返回给客户端还是存集到一个新的集 合中。
kv模式不变,但V是一个键值对
hset,hget,hmset,hmget,hgetall,hdel,hlen,hexists
hkeys(获取指定hash的所有key),hvals(获取所有value),hincrby,hsetnx
# =================================================== # hset、hget 命令用于为哈希表中的字段赋值 。 # hmset、hmget 同时将多个field-value对设置到哈希表中。会覆盖哈希表中已存在的字段。 # hgetall 用于返回哈希表中,所有的字段和值。 # hdel 用于删除哈希表 key 中的一个或多个指定字段 # =================================================== 127.0.0.1:6379> hset myhash field1 "kuangshen" (integer) 1 127.0.0.1:6379> hget myhash field1 "kuangshen" 127.0.0.1:6379> HMSET myhash field1 "Hello" field2 "World" OK 127.0.0.1:6379> HGET myhash field1 "Hello" 127.0.0.1:6379> HGET myhash field2 "World" 127.0.0.1:6379> hgetall myhash 1) "field1" 2) "Hello" 3) "field2" 4) "World" 127.0.0.1:6379> HDEL myhash field1 (integer) 1 127.0.0.1:6379> hgetall myhash 1) "field2" 2) "World" # =================================================== # hlen 获取哈希表中字段的数量。 # =================================================== 127.0.0.1:6379> hlen myhash (integer) 1 127.0.0.1:6379> HMSET myhash field1 "Hello" field2 "World" OK 127.0.0.1:6379> hlen myhash (integer) 2 # =================================================== # hexists 查看哈希表的指定字段是否存在。 # =================================================== 127.0.0.1:6379> hexists myhash field1 (integer) 1 127.0.0.1:6379> hexists myhash field3 (integer) 0 # =================================================== # hkeys 获取哈希表中的所有域(field)。 # hvals 返回哈希表所有域(field)的值。 # =================================================== 127.0.0.1:6379> HKEYS myhash 1) "field2" 2) "field1" 127.0.0.1:6379> HVALS myhash 1) "World" 2) "Hello" # =================================================== # hincrby 为哈希表中的字段值加上指定增量值。 # =================================================== 127.0.0.1:6379> hset myhash field 5 (integer) 1 127.0.0.1:6379> HINCRBY myhash field 1 (integer) 6 127.0.0.1:6379> HINCRBY myhash field -1 (integer) 5 127.0.0.1:6379> HINCRBY myhash field -10 (integer) -5 # =================================================== # hsetnx 为哈希表中不存在的的字段赋值 。 # =================================================== 127.0.0.1:6379> HSETNX myhash field1 "hello" (integer) 1 # 设置成功,返回 1 。 127.0.0.1:6379> HSETNX myhash field1 "world" (integer) 0 # 如果给定字段已经存在,返回 0 。 127.0.0.1:6379> HGET myhash field1 "hello"
Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。 存储部分变更的数据,如用户信息等。
zadd,zrange,ZRANGE,ZRANGEBYSCORE(递增排列),ZREVRANGE(递减排列),zrem
zcard(显示个数),zcount(计算指定区间的数量),zrank(显示排名,从小到大排),zrevrank(排名)
# =================================================== # zadd 将一个或多个成员元素及其分数值加入到有序集当中。 # zrange 返回有序集中,指定区间内的成员 # =================================================== 127.0.0.1:6379> zadd myset 1 "one" (integer) 1 127.0.0.1:6379> zadd myset 2 "two" 3 "three" (integer) 2 127.0.0.1:6379> zrange myset 0 -1 1) "one" 2) "two" 3) "three" # =================================================== # zrangebyscore 返回有序集合中指定分数区间的成员列表。有序集成员按分数值递增(从小到大)次序排列。 # =================================================== 127.0.0.1:6379> zadd salary 2500 xiaoming (integer) 1 127.0.0.1:6379> zadd salary 5000 xiaohong (integer) 1 127.0.0.1:6379> zadd salary 500 kuangshen (integer) 1 # Inf无穷大量+∞,同样地,-∞可以表示为-Inf。 127.0.0.1:6379> ZRANGEBYSCORE salary -inf +inf # 显示整个有序集 1) "kuangshen" 2) "xiaoming" 3) "xiaohong" 127.0.0.1:6379> ZRANGEBYSCORE salary -inf +inf withscores # 递增排列 1) "kuangshen" 2) "500" 3) "xiaoming" 4) "2500" 5) "xiaohong" 6) "5000" 127.0.0.1:6379> ZREVRANGE salary 0 -1 WITHSCORES # 递减排列 1) "xiaohong" 2) "5000" 3) "xiaoming" 4) "2500" 5) "kuangshen" 6) "500" 127.0.0.1:6379> ZRANGEBYSCORE salary -inf 2500 WITHSCORES # 显示工资 <=2500的所有成员 1) "kuangshen" 2) "500" 3) "xiaoming 4) "2500" # =================================================== # zrem 移除有序集中的一个或多个成员 # =================================================== 127.0.0.1:6379> ZRANGE salary 0 -1 1) "kuangshen" 2) "xiaoming" 3) "xiaohong" 127.0.0.1:6379> zrem salary kuangshen (integer) 1 127.0.0.1:6379> ZRANGE salary 0 -1 1) "xiaoming" 2) "xiaohong" # =================================================== # zcard 命令用于计算集合中元素的数量。 # =================================================== 127.0.0.1:6379> zcard salary (integer) 2 OK # =================================================== # zcount 计算有序集合中指定分数区间的成员数量。 # =================================================== 127.0.0.1:6379> zadd myset 1 "hello" (integer) 1 127.0.0.1:6379> zadd myset 2 "world" 3 "kuangshen" (integer) 2 127.0.0.1:6379> ZCOUNT myset 1 3 (integer) 3 127.0.0.1:6379> ZCOUNT myset 1 2 (integer) 2 # =================================================== # zrank 返回有序集中指定成员的排名。其中有序集成员按分数值递增(从小到大)顺序排列。 # =================================================== 127.0.0.1:6379> zadd salary 2500 xiaoming (integer) 1 127.0.0.1:6379> zadd salary 5000 xiaohong (integer) 1 127.0.0.1:6379> zadd salary 500 kuangshen (integer) 1 127.0.0.1:6379> ZRANGE salary 0 -1 WITHSCORES # 显示所有成员及其 score 值 1) "kuangshen" 2) "500" 3) "xiaoming" 4) "2500" 5) "xiaohong" 6) "5000" 127.0.0.1:6379> zrank salary kuangshen # 显示 kuangshen 的薪水排名,最少 (integer) 0 127.0.0.1:6379> zrank salary xiaohong # 显示 xiaohong 的薪水排名,第三 (integer) 2 # =================================================== # zrevrank 返回有序集中成员的排名。其中有序集成员按分数值递减(从大到小)排序。 # =================================================== 127.0.0.1:6379> ZREVRANK salary kuangshen # 狂神第三 (integer) 2 127.0.0.1:6379> ZREVRANK salary xiaohong # 小红第一 (integer) 0
和set相比,sorted set增加了一个权重参数score,使得集合中的元素能够按score进行有序排列,比如 一个存储全班同学成绩的sorted set,其集合value可以是同学的学号,而score就可以是其考试得分, 这样在数据插入集合的时候,就已经进行了天然的排序。可以用sorted set来做带权重的队列,比如普 通消息的score为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取工作任务。让 重要的任务优先执行。
排行榜应用,取TOP N操作 !
简介
Redis 的 GEO 特性在 Redis 3.2 版本中推出, 这个功能可以将用户给定的地理位置信息储存起来, 并对 这些信息进行操作。来实现诸如附近位置、摇一摇这类依赖于地理位置信息的功能。geo的数据类型为 zset。
GEO 的数据结构总共有六个常用命令:geoadd、geopos、geodist、georadius、 georadiusbymember、gethash
官方文档:https://www.redis.net.cn/order/3685.html
简介
Redis 在 2.8.9 版本添加了 HyperLogLog 结构。
Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。
在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基 数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
HyperLogLog则是一种算法,它提供了不精确的去重计数方案。
举个栗子:假如我要统计网页的UV(浏览用户数量,一天内同一个用户多次访问只能算一次),传统的 解决方案是使用Set来保存用户id,然后统计Set中的元素数量来获取页面UV。但这种方案只能承载少量 用户,一旦用户数量大起来就需要消耗大量的空间来存储用户id。我的目的是统计用户数量而不是保存 用户,这简直是个吃力不讨好的方案!而使用Redis的HyperLogLog最多需要12k就可以统计大量的用户 数,尽管它大概有0.81%的错误率,但对于统计UV这种不需要很精确的数据是可以忽略不计的。
简介
在开发中,可能会遇到这种情况:需要统计用户的某些信息,如活跃或不活跃,登录或者不登录;又如需要记录用户一年的打卡情况,打卡了是1, 没有打卡是0,如果使用普通的 key/value存储,则要记录 365条记录,如果用户量很大,需要的空间也会很大,所以 Redis 提供了 Bitmap 位图这中数据结构, Bitmap 就是通过操作二进制位来进行记录,即为 0 和 1;如果要记录 365 天的打卡情况,使用 Bitmap 表示的形式大概如下:0101000111000111…,这样有什么好处呢?当然就是节约内存了,365 天相当于 365 bit,又 1 字节 = 8 bit , 所以相当于使用 46 个字节即可。
BitMap 就是通过一个 bit 位来表示某个元素对应的值或者状态, 其中的 key 就是对应元素本身,实际上底层也是通过对字符串的操作来实现。Redis 从 2.2 版本之后新增了setbit, getbit, bitcount 等几个 bitmap 相关命令。
位置
Redis 的配置文件位于 Redis 安装目录下,文件名为 redis.conf
config get * # 获取全部的配置
配置文件的地址:
我们一般情况下,会单独拷贝出来一份进行操作。来保证初始文件的安全。
Units 单位
1、配置大小单位,开头定义了一些基本的度量单位,只支持bytes,不支持bit
2、对大小写不敏感
INCLUDES 包含
NETWORK 网络配置
bind 127.0.0.1 # 绑定的ip protected-mode yes # 保护模式 port 6379 # 默认端口
GENERAL 通用
daemonize yes # 默认情况下,Redis不作为守护进程运行。需要开启的话,改为 yes supervised no # 可通过upstart和systemd管理Redis守护进程 pidfile /var/run/redis_6379.pid # 以后台进程方式运行redis,则需要指定pid 文件 loglevel notice # 日志级别。可选项有: # debug(记录大量日志信息,适用于开发、测试阶段); # verbose(较多日志信息); # notice(适量日志信息,使用于生产环境); # warning(仅有部分重要、关键信息才会被记录)。 logfile "" # 日志文件的位置,当指定为空字符串时,为标准输出 databases 16 # 设置数据库的数目。默认的数据库是DB 0 always-show-logo yes # 是否总是显示logo
SNAPSHOPTING 快照
# 900秒(15分钟)内至少1个key值改变(则进行数据库保存--持久化) save 900 1 # 300秒(5分钟)内至少10个key值改变(则进行数据库保存--持久化) save 300 10 # 60秒(1分钟)内至少10000个key值改变(则进行数据库保存--持久化) save 60 10000 stop-writes-on-bgsave-error yes # 持久化出现错误后,是否依然进行继续进行工作 rdbcompression yes # 使用压缩rdb文件 yes:压缩,但是需要一些cpu的消耗。no:不压缩,需要更多的磁盘空间 rdbchecksum yes # 是否校验rdb文件,更有利于文件的容错性,但是在保存rdb文件的时候,会有大概10%的性能损耗 dbfilename dump.rdb # dbfilenamerdb文件名称 dir ./ # dir 数据目录,数据库的写入会在这个目录。rdb、aof文件也会写在这个目录
SECURITY安全
访问密码的查看,设置和取消
# 启动redis # 连接客户端 # 获得和设置密码 config get requirepass config set requirepass "123456" #测试ping,发现需要验证 127.0.0.1:6379> ping NOAUTH Authentication required. # 验证 127.0.0.1:6379> auth 123456 OK 127.0.0.1:6379> ping PONG
限制
maxclients 10000 # 设置能连上redis的最大客户端连接数量 maxmemory <bytes> # redis配置的最大内存容量 maxmemory-policy noeviction # maxmemory-policy 内存达到上限的处理策略 # volatile-lru:利用LRU算法移除设置过过期时间的key。 # volatile-random:随机移除设置过过期时间的key。 # volatile-ttl:移除即将过期的key,根据最近过期时间来删除(辅以TTL) # allkeys-lru:利用LRU算法移除任何key。 # allkeys-random:随机移除任何key。 # noeviction:不移除任何key,只是返回一个写错误。
append only模式
appendonly no # 是否以append only模式作为持久化方式,默认使用的是rdb方式持久化,这种方式在许多应用中已经足够用了 appendfilename "appendonly.aof" # appendfilename AOF 文件名称 appendfsync everysec # appendfsync aof持久化策略的配置 # no表示不执行fsync,由操作系统保证数据同步到磁盘,速度最快。 # always表示每次写入都执行fsync,以保证数据同步到磁盘。 # everysec表示每秒执行一次fsync,可能会导致丢失这1s数据。
1、Redis默认不是以守护进程的方式运行,可以通过该配置项修改,使用yes启用守护进程
daemonize no
2、当Redis以守护进程方式运行时,Redis默认会把pid写入/var/run/redis.pid文件,可以通过pidfile指定
pidfile /var/run/redis.pid
3、指定Redis监听端口,默认端口为6379,作者在自己的一篇博文中解释了为什么选用6379作为默端口,因为6379在手机按键上MERZ对应的号码,而MERZ取自意大利歌女Alessia Merz的名字
port 6379
4、绑定的主机地址
bind 127.0.0.1
5、当 客户端闲置多长时间后关闭连接,如果指定为0,表示关闭该功能
timeout 300
6、指定日志记录级别,Redis总共支持四个级别:debug、verbose、notice、warning,默认为verbose
loglevel verbose
7、日志记录方式,默认为标准输出,如果配置Redis为守护进程方式运行,而这里又配置为日志记录方式为标准输出,则日志将会发送给/dev/null
logfile stdout
8、设置数据库的数量,默认数据库为0,可以使用SELECT 命令在连接上指定数据库id
databases 16
9、指定在多长时间内,有多少次更新操作,就将数据同步到数据文件,可以多个条件配合
save
Redis默认配置文件中提供了三个条件:
save 900 1
save 300 10
save 60 10000
分别表示900秒(15分钟)内有1个更改,300秒(5分钟)内有10个更改以及60秒内有10000个更改。
10、指定存储至本地数据库时是否压缩数据,默认为yes,Redis采用LZF压缩,如果为了节省CPU时间,可以关闭该选项,但会导致数据库文件变的巨大
rdbcompression yes
11、指定本地数据库文件名,默认值为dump.rdb
dbfilename dump.rdb
12、指定本地数据库存放目录
dir ./
13、设置当本机为slav服务时,设置master服务的IP地址及端口,在Redis启动时,它会自动从master进行数据同步
slaveof
14、当master服务设置了密码保护时,slav服务连接master的密码
masterauth
15、设置Redis连接密码,如果配置了连接密码,客户端在连接Redis时需要通过AUTH 命令提供密码,默认关闭
requirepass foobared
16、设置同一时间最大客户端连接数,默认无限制,Redis可以同时打开的客户端连接数为Redis进程可以打开的最大文件描述符数,如果设置 maxclients 0,表示不作限制。当客户端连接数到达限制时,Redis会关闭新的连接并向客户端返回max number of clients reached错误信息
maxclients 128
17、指定Redis最大内存限制,Redis在启动时会把数据加载到内存中,达到最大内存后,Redis会先尝试清除已到期或即将到期的Key,当此方法处理 后,仍然到达最大内存设置,将无法再进行写入操作,但仍然可以进行读取操作。Redis新的vm机制,会把Key存放内存,Value会存放在swap区
maxmemory
18、指定是否在每次更新操作后进行日志记录,Redis在默认情况下是异步的把数据写入磁盘,如果不开启,可能会在断电时导致一段时间内的数据丢失。因为 redis本身同步数据文件是按上面save条件来同步的,所以有的数据会在一段时间内只存在于内存中。默认为no
appendonly no
19、指定更新日志文件名,默认为appendonly.aof
appendfilename appendonly.aof
20、指定更新日志条件,共有3个可选值:
no:表示等操作系统进行数据缓存同步到磁盘(快)
always:表示每次更新操作后手动调用fsync()将数据写到磁盘(慢,安全)
everysec:表示每秒同步一次(折衷,默认值)
appendfsync everysec
21、指定是否启用虚拟内存机制,默认值为no,简单的介绍一下,VM机制将数据分页存放,由Redis将访问量较少的页即冷数据swap到磁盘上,访问多的页面由磁盘自动换出到内存中(在后面的文章我会仔细分析Redis的VM机制)
vm-enabled no
22、虚拟内存文件路径,默认值为/tmp/redis.swap,不可多个Redis实例共享
vm-swap-file /tmp/redis.swap
23、将所有大于vm-max-memory的数据存入虚拟内存,无论vm-max-memory设置多小,所有索引数据都是内存存储的(Redis的索引数据 就是keys),也就是说,当vm-max-memory设置为0的时候,其实是所有value都存在于磁盘。默认值为0
vm-max-memory 0
24、Redis swap文件分成了很多的page,一个对象可以保存在多个page上面,但一个page上不能被多个对象共享,vm-page-size是要根据存储的 数据大小来设定的,作者建议如果存储很多小对象,page大小最好设置为32或64bytes;如果存储很大大对象,则可以使用更大的page,如果不 确定,就使用默认值
vm-page-size 32
25、设置swap文件中的page数量,由于页表(一种表示页面空闲或使用的bitmap)是在放在内存中的,在磁盘上每8个pages将消耗1byte的内存。
vm-pages 134217728
26、设置访问swap文件的线程数,最好不要超过机器的核数,如果设置为0,那么所有对swap文件的操作都是串行的,可能会造成比较长时间的延迟。默认值为4
vm-max-threads 4
27、设置在向客户端应答时,是否把较小的包合并为一个包发送,默认为开启
glueoutputbuf yes
28、指定在超过一定的数量或者最大的元素超过某一临界值时,采用一种特殊的哈希算法
hash-max-zipmap-entries 64
hash-max-zipmap-value 512
29、指定是否激活重置哈希,默认为开启(后面在介绍Redis的哈希算法时具体介绍)
activerehashing yes
30、指定包含其它的配置文件,可以在同一主机上多个Redis实例之间使用同一份配置文件,而同时各个实例又拥有自己的特定配置文件
include /path/to/local.conf
Redis 是内存数据库,如果不将内存中的数据库状态保存到磁盘,那么一旦服务器进程退出,服务器中的数据库状态也会消失。所以 Redis 提供了持久化功能!
什么是RDB
在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里。
Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的。这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。
Fork
Fork的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量,环境变量,程序计数器等)数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程。
Rdb 保存的是 dump.rdb 文件
配置位置及SNAPSHOTTING解析
这里的触发条件机制,我们可以修改测试一下:
save 120 10 # 120秒内修改10次则触发RDB
RDB 是整合内存的压缩过的Snapshot,RDB 的数据结构,可以配置复合的快照触发条件。默认:
如果想禁用RDB持久化的策略,只要不设置任何save指令,或者给save传入一个空字符串参数也可以。若要修改完毕需要立马生效,可以手动使用 save 命令!立马生效 !
其余命令解析
Stop-writes-on-bgsave-error:如果配置为no,表示你不在乎数据不一致或者有其他的手段发现和控制,默认为yes。
rbdcompression:对于存储到磁盘中的快照,可以设置是否进行压缩存储。如果是的话,redis会采用LZF算法进行压缩,如果你不想消耗CPU来进行压缩的话,可以设置为关闭此功能。
rdbchecksum:在存储快照后,还可以让redis使用CRC64算法来进行数据校验,但是这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能。默认为yes。
如何触发RDB快照
1、配置文件中默认的快照配置,建议多用一台机子作为备份,复制一份 dump.rdb
2、命令save或者是bgsave
3、执行flushall命令,也会产生 dump.rdb 文件,但里面是空的,无意义 !
4、退出的时候也会产生 dump.rdb 文件!
如何恢复
1、将备份文件(dump.rdb)移动到redis安装目录并启动服务即可
2、CONFIG GET dir 获取目录
127.0.0.1:6379> config get dir dir /usr/local/bin
优点和缺点
优点:
1、适合大规模的数据恢复
2、对数据完整性和一致性要求不高
缺点:
1、在一定间隔时间做一次备份,所以如果redis意外down掉的话,就会丢失最后一次快照后的所有修改
2、Fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑。
小结
是什么
以日志的形式来记录每个写操作,将Redis执行过的所有指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作
Aof保存的是 appendonly.aof 文件
配置
appendonly no # 是否以append only模式作为持久化方式,默认使用的是rdb方式持久化,这种方式在许多应用中已经足够用了 appendfilename "appendonly.aof" # appendfilename AOF 文件名称 appendfsync everysec # appendfsync aof持久化策略的配置 # no表示不执行fsync,由操作系统保证数据同步到磁盘,速度最快。 # always表示每次写入都执行fsync,以保证数据同步到磁盘。 # everysec表示每秒执行一次fsync,可能会导致丢失这1s数据。 No-appendfsync-on-rewrite #重写时是否可以运用Appendfsync,用默认no即可,保证数据安 全性 Auto-aof-rewrite-min-size # 设置重写的基准值 Auto-aof-rewrite-percentage #设置重写的基准值
AOF 启动/修复/恢复
正常恢复:
异常恢复:
redis-check-aof --fix appendonly.aof
进行修复Rewrite
是什么:
AOF 采用文件追加方式,文件会越来越大,为避免出现此种情况,新增了重写机制,当AOF文件的大小超过所设定的阈值时,Redis 就会启动AOF 文件的内容压缩,只保留可以恢复数据的最小指令集,可以使用命令 bgrewriteaof !
重写原理:
AOF 文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写临时文件最后再rename),遍历新进程的内存中数据,每条记录有一条的Set语句。重写aof文件的操作,并没有读取旧的aof文件,这点和快照有点类似!
触发机制:
Redis会记录上次重写时的AOF大小,默认配置是当AOF文件大小是上次rewrite后大小的已被且文件大于64M的触发。
优点和缺点
优点:
1、每修改同步:appendfsync always 同步持久化,每次发生数据变更会被立即记录到磁盘,性能较差但数据完整性比较好
2、每秒同步: appendfsync everysec 异步操作,每秒记录 ,如果一秒内宕机,有数据丢失
3、不同步: appendfsync no 从不同步
缺点:
1、相同数据集的数据而言,aof 文件要远大于 rdb文件,恢复速度慢于 rdb
2、Aof 运行效率要慢于 rdb,每秒同步策略效率较好,不同步效率和rdb相同。
小总结
1、RDB 持久化方式能够在指定的时间间隔内对你的数据进行快照存储
2、AOF 持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以Redis 协议追加保存每次写的操作到文件末尾,Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大。
3、只做缓存,如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化
4、同时开启两种持久化方式
5、性能建议
save 900 1
这条规则。Redis事务的概念:
Redis 事务的本质是一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。
总结说:redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。
Redis事务没有隔离级别的概念:
批量操作在发送 EXEC 命令前被放入队列缓存,并不会被实际执行!
Redis不保证原子性:
Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。
Redis事务的三个阶段:
Redis事务相关命令:
watch key1 key2 ... #监视一或多个key,如果在事务执行之前,被监视的key被其他命令改动,则事务被打断 ( 类似乐观锁 ) multi # 标记一个事务块的开始( queued ) exec # 执行所有事务块的命令 ( 一旦执行exec后,之前加的监控锁都会被取消掉 ) discard # 取消事务,放弃事务块中的所有命令 unwatch # 取消watch对所有key的监控
正常执行
放弃事务
若在事务队列中存在命令性错误(类似于java编译性错误),则执行EXEC命令时,所有命令都不会执行
若在事务队列中存在语法性错误(类似于java的1/0的运行时异常),则执行EXEC命令时,其他正确命令会被执行,错误命令抛出异常。
Watch 监控
悲观锁:
悲观锁(Pessimistic Lock),顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿到这个数据就会block直到它拿到锁。传统的关系型数据库里面就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在操作之前先上锁。
乐观锁:
乐观锁(Optimistic Lock),顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁。但是在更新的时候会判断一下再此期间别人有没有去更新这个数据,可以使用版本号等机制,乐观锁适用于多读的应用类型,这样可以提高吞吐量,乐观锁策略:提交版本必须大于记录当前版本才能执行更新
测试:
1、初始化信用卡可用余额和欠额
127.0.0.1:6379> set balance 100 OK 127.0.0.1:6379> set debt 0 OK
2、使用watch检测balance,事务期间balance数据未变动,事务执行成功
127.0.0.1:6379> watch balance OK 127.0.0.1:6379> MULTI OK 127.0.0.1:6379> decrby balance 20 QUEUED 127.0.0.1:6379> incrby debt 20 QUEUED 127.0.0.1:6379> exec 1) (integer) 80 2) (integer) 20
3、使用watch检测balance,事务期间balance数据变动,事务执行失败!
# 窗口一 127.0.0.1:6379> watch balance OK 127.0.0.1:6379> MULTI # 执行完毕后,执行窗口二代码测试 OK 127.0.0.1:6379> decrby balance 20 QUEUED 127.0.0.1:6379> incrby debt 20 QUEUED 127.0.0.1:6379> exec # 修改失败! (nil) # 窗口二 127.0.0.1:6379> get balance "80" 127.0.0.1:6379> set balance 200 OK # 窗口一:出现问题后放弃监视,然后重来! 127.0.0.1:6379> UNWATCH # 放弃监视 OK 127.0.0.1:6379> watch balance OK 127.0.0.1:6379> MULTI OK 127.0.0.1:6379> decrby balance 20 QUEUED 127.0.0.1:6379> incrby debt 20 QUEUED 127.0.0.1:6379> exec # 成功! 1) (integer) 180 2) (integer) 40
说明:
一但执行 EXEC 开启事务的执行后,无论事务使用执行成功, WARCH 对变量的监控都将被取消。故当事务执行失败后,需重新执行WATCH命令对变量进行监控,并开启新的事务进行操作。
watch指令类似于乐观锁,在事务提交时,如果watch监控的多个KEY中任何KEY的值已经被其他客户端更改,则使用EXEC执行事务时,事务队列将不会被执行,同时返回Nullmulti-bulk应答以通知调用者事务执行失败
是什么
Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。Redis 客户端可以订阅任意数量的频道。
订阅/发布消息图:
下图展示了频道 channel1 , 以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 之间的关系:
当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端:
命令
这些命令被广泛用于构建即时通信应用,比如网络聊天室(chatroom)和实时广播、实时提醒等。
测试
以下实例演示了发布订阅是如何工作的。在我们实例中我们创建了订阅频道名为 redisChat:
redis 127.0.0.1:6379> SUBSCRIBE redisChat Reading messages... (press Ctrl-C to quit) 1) "subscribe" 2) "redisChat" 3) (integer) 1
现在,我们先重新开启个 redis 客户端,然后在同一个频道 redisChat 发布两次消息,订阅者就能接收到消息。
redis 127.0.0.1:6379> PUBLISH redisChat "Hello,Redis" (integer) 1 redis 127.0.0.1:6379> PUBLISH redisChat "Hello,Kuangshen" (integer) 1 # 订阅者的客户端会显示如下消息 1) "message" 2) "redisChat" 3) "Hello,Redis" 1) "message" 2) "redisChat" 3) "Hello,Kuangshen"
原理
Redis是使用C实现的,通过分析 Redis 源码里的 pubsub.c 文件,了解发布和订阅机制的底层实现,籍此加深对 Redis 的理解。
Redis 通过 PUBLISH 、SUBSCRIBE 和 PSUBSCRIBE 等命令实现发布和订阅功能。通过 SUBSCRIBE 命令订阅某频道后,redis-server 里维护了一个字典,字典的键就是一个个channel
,而字典的值则是一个链表,链表中保存了所有订阅这个 channel 的客户端。SUBSCRIBE 命令的关键,就是将客户端添加到给定 channel 的订阅链表中。
通过 PUBLISH 命令向订阅者发送消息,redis-server 会使用给定的频道作为键,在它所维护的 channel字典中查找记录了订阅这个频道的所有客户端的链表,遍历这个链表,将消息发布给所有订阅者。
Pub/Sub 从字面上理解就是发布(Publish)与订阅(Subscribe),在Redis中,你可以设定对某一个key值进行消息发布及消息订阅,当一个key值上进行了消息发布后,所有订阅它的客户端都会收到相应的消息。这一功能最明显的用法就是用作实时消息系统,比如普通的即时聊天,群聊等功能。
使用场景
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master/leader),后者称为从节点(slave/follower);数据的复制是单向的,只能由主节点到从节点。Master以写为主,Slave 以读为主。
默认情况下,每台Redis服务器都是主节点;且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。
主从复制的作用主要包括:
1、数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
2、故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
3、负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
4、高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。
一般来说,要将Redis运用于工程项目中,只使用一台Redis是万万不能的,原因如下:
电商网站上的商品,一般都是一次上传,无数次浏览的,说专业点也就是"多读少写"。对于这种场景,我们可以使如下这种架构:
基本配置
配从库不配主库,从库配置:
slaveof 主库ip 主库端口 # 配置主从 Info replication # 查看信息
每次与 master 断开之后,都需要重新连接,除非你配置进 redis.conf 文件!
修改配置文件!
准备工作:我们配置主从复制,至少需要三个,一主二从!配置三个客户端!
1、拷贝多个redis.conf 文件
2、指定端口 6379,依次类推
3、开启daemonize yes
4、Pid文件名字 pidfile /var/run/redis_6379.pid
, 依次类推
5、Log文件名字 logfile "6379.log"
, 依次类推
6、Dump.rdb 名字 dbfilename dump6379.rdb
, 依次类推
上面都配置完毕后,3个服务通过3个不同的配置文件开启,我们的准备环境就OK 了!
一主二仆
1、环境初始化
默认三个都是Master 主节点
2、配置为一个Master 两个Slave
3、在主机设置值,在从机都可以取到!从机不能写值!
测试一:主机挂了,查看从机信息,主机恢复,再次查看信息
测试二:从机挂了,查看主机信息,从机恢复,查看从机信息
层层链路
上一个Slave 可以是下一个slave 和 Master,Slave 同样可以接收其他 slaves 的连接和同步请求,那么该 slave 作为了链条中下一个的master,可以有效减轻 master 的写压力!
测试:6379 设置值以后 6380 和 6381 都可以获取到!OK!
谋朝篡位
一主二从的情况下,如果主机断了,从机可以使用命令 SLAVEOF NO ONE
将自己改为主机!这个时候其余的从机链接到这个节点。对一个从属服务器执行命令 SLAVEOF NO ONE 将使得这个从属服务器关闭复制功能,并从从属服务器转变回主服务器,原来同步所得的数据集不会被丢弃。
主机再回来,也只是一个光杆司令了,从机为了正常使用跑到了新的主机上!
复制原理
Slave 启动成功连接到 master 后会发送一个sync命令
Master 接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,master将传送整个数据文件到slave,并完成一次完全同步。
全量复制:而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。
增量复制:Master 继续将新的所有收集到的修改命令依次传给slave,完成同步
但是只要是重新连接master,一次完全同步(全量复制)将被自动执行
概述
主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。Redis从2.8开始正式提供了Sentinel(哨兵) 架构来解决这个问题。
谋朝篡位的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库。
哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。
这里的哨兵有两个作用
然而一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。
假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover[故障转移]操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。
配置测试
1、调整结构,6379带着80、81
2、自定义的 /myredis 目录下新建 sentinel.conf 文件,名字千万不要错
3、配置哨兵,填写内容
sentinel monitor 被监控主机名字 127.0.0.1 6379 1
4、启动哨兵
5、正常主从演示
6、原有的Master 挂了
7、投票新选
8、重新主从继续开工,info replication 查查看
9、问题:如果之前的master 重启回来,会不会双master 冲突? 之前的回来只能做小弟了
哨兵模式的优缺点
优点
缺点
哨兵配置说明
# Example sentinel.conf # 哨兵sentinel实例运行的端口 默认26379 port 26379 # 哨兵sentinel的工作目录 dir /tmp # 哨兵sentinel监控的redis主节点的 ip port # master-name 可以自己命名的主节点名字 只能由字母A-z、数字0-9 、这三个字符".-_"组成。 # quorum 配置多少个sentinel哨兵统一认为master主节点失联 那么这时客观上认为主节点失联了 # sentinel monitor <master-name> <ip> <redis-port> <quorum> sentinel monitor mymaster 127.0.0.1 6379 2 # 当在Redis实例中开启了requirepass foobared 授权密码 这样所有连接Redis实例的客户端都要提供密码 # 设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码 # sentinel auth-pass <master-name> <password> sentinel auth-pass mymaster MySUPER--secret-0123passw0rd # 指定多少毫秒之后 主节点没有应答哨兵sentinel 此时 哨兵主观上认为主节点下线 默认30秒 # sentinel down-after-milliseconds <master-name> <milliseconds> sentinel down-after-milliseconds mymaster 30000 # 这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行 同步,这个数字越小,完成failover所需的时间就越长,但是如果这个数字越大,就意味着越 多的slave因为replication而不可用。可以通过将这个值设为 1 来保证每次只有一个slave 处于不能处理命令请求的状态。 # sentinel parallel-syncs <master-name> <numslaves> sentinel parallel-syncs mymaster 1 # 故障转移的超时时间 failover-timeout 可以用在以下这些方面: #1. 同一个sentinel对同一个master两次failover之间的间隔时间。 #2. 当一个slave从一个错误的master那里同步数据开始计算时间。直到slave被纠正为向正确的master那里同步数据时。 #3.当想要取消一个正在进行的failover所需要的时间。 #4.当进行failover时,配置所有slaves指向新的master所需的最大时间。不过,即使过了这个超时,slaves依然会被正确配置为指向master,但是就不按parallel-syncs所配置的规则来了 # 默认三分钟 # sentinel failover-timeout <master-name> <milliseconds> sentinel failover-timeout mymaster 180000 # SCRIPTS EXECUTION #配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知相关人员。 #对于脚本的运行结果有以下规则: #若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10 #若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行。 #如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。 #一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个SIGKILL信号终止,之后重新执行。 #通知型脚本:当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等),将会去调用这个脚本,这时这个脚本应该通过邮件,SMS等方式去通知系统管理员关于系统不正常运行的信息。调用该脚本时,将传给脚本两个参数,一个是事件的类型,一个是事件的描述。如果sentinel.conf配置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则sentinel无法正常启动成功。 #通知脚本 # sentinel notification-script <master-name> <script-path> sentinel notification-script mymaster /var/redis/notify.sh # 客户端重新配置主节点参数脚本 # 当一个master由于failover而发生改变时,这个脚本将会被调用,通知相关的客户端关于master地址已经发生改变的信息。 # 以下参数将会在调用脚本时传给脚本: # <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port> # 目前<state>总是“failover”, # <role>是“leader”或者“observer”中的一个。 # 参数 from-ip, from-port, to-ip, to-port是用来和旧的master和新的master(即旧的slave)通信的 # 这个脚本应该是通用的,能被多次调用,不是针对性的。 # sentinel client-reconfig-script <master-name> <script-path> sentinel client-reconfig-script mymaster /var/redis/reconfig.sh
Redis缓存的使用,极大的提升了应用程序的性能和效率,特别是数据查询方面。但同时,它也带来了一
些问题。其中,最要害的问题,就是数据的一致性问题,从严格意义上讲,这个问题无解。如果对数据
的一致性要求很高,那么就不能使用缓存。
另外的一些典型问题就是,缓存穿透、缓存雪崩和缓存击穿。目前,业界也都有比较流行的解决方案。
概念
缓存穿透的概念很简单,用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于是向持久层数据库查询。发现也没有,于是本次查询失败。当用户很多的时候,缓存都没有命中,于是都去请求了持久层数据库。这会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透。
解决方案
布隆过滤器
布隆过滤器是一种数据结构,对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃,从而避免了对底层存储系统的查询压力;
缓存空对象
当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源;
但是这种方法会存在两个问题:
1、如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键;
2、即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。
概述
这里需要注意和缓存击穿的区别,缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
当某个key在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来查询最新数据,并且回写缓存,会导使数据库瞬间压力过大。
解决方案
设置热点数据永不过期
从缓存层面来看,没有设置过期时间,所以不会出现热点 key 过期后产生的问题。
加互斥锁
分布式锁:使用分布式锁,保证对于每个key同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可。这种方式将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大。
概念
缓存雪崩,是指在某一个时间段,缓存集中过期失效。
产生雪崩的原因之一,比如在写本文的时候,马上就要到双十二零点,很快就会迎来一波抢购,这波商品时间比较集中的放入了缓存,假设缓存一个小时。那么到了凌晨一点钟的时候,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。
其实集中过期,倒不是非常致命,比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网。因为自然形成的缓存雪崩,一定是在某个时间段集中创建缓存,这个时候,数据库也是可以顶住压力的。无非就是对数据库产生周期性的压力而已。而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知的,很有可能瞬间就把数据库压垮。
解决方案
redis高可用
这个思想的含义是,既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。
限流降级
这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
数据预热
数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
Jedis是Redis官方推荐的Java连接开发工具。要在Java开发中使用好Redis中间件,必须对Jedis熟悉才能写成漂亮的代码(spring2.X之后,底层原来使用的jedis被替换为了lettuce,详见SpringBoot:Redis整合)
1、新建一个普通的Maven项目
2、导入redis的依赖!
<!-- https://mvnrepository.com/artifact/redis.clients/jedis --> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>3.2.0</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.58</version> </dependency>
3、编写测试代码
package com.kuang.ping; import redis.clients.jedis.Jedis; public class Ping { public static void main(String[] args) { Jedis jedis = new Jedis("127.0.0.1",6379); System.out.println("连接成功"); //查看服务是否运行 System.out.println("服务正在运行: "+jedis.ping()); } }
4、启动redis服务
5、启动测试,结果
连接成功 服务正在运行: PONG
基本操作
public class TestPassword { public static void main(String[] args) { Jedis jedis = new Jedis("127.0.0.1", 6379); //验证密码,如果没有设置密码这段代码省略 // jedis.auth("password"); jedis.connect(); //连接 jedis.disconnect(); //断开连接 jedis.flushAll(); //清空所有的key } }
对key操作的命令
public class TestKey { public static void main(String[] args) { Jedis jedis = new Jedis("127.0.0.1", 6379); System.out.println("清空数据:"+jedis.flushDB()); System.out.println("判断某个键是否存在:"+jedis.exists("username")); System.out.println("新增<'username','kuangshen'>的键值对:"+jedis.set("username", "kuangshen")); System.out.println("新增<'password','password'>的键值对:"+jedis.set("password", "password")); System.out.print("系统中所有的键如下:"); Set<String> keys = jedis.keys("*"); System.out.println(keys); System.out.println("删除键password:"+jedis.del("password")); System.out.println("判断键password是否存在:"+jedis.exists("password")); System.out.println("查看键username所存储的值的类型:"+jedis.type("username")); System.out.println("随机返回key空间的一个:"+jedis.randomKey()); System.out.println("重命名key:"+jedis.rename("username","name")); System.out.println("取出改后的name:"+jedis.get("name")); System.out.println("按索引查询:"+jedis.select(0)); System.out.println("删除当前选择数据库中的所有key:"+jedis.flushDB()); System.out.println("返回当前数据库中key的数目:"+jedis.dbSize()); System.out.println("删除所有数据库中的所有key:"+jedis.flushAll()); } }
对String操作的命令
public class TestString { public static void main(String[] args) { Jedis jedis = new Jedis("127.0.0.1", 6379); jedis.flushDB(); System.out.println("===========增加数据==========="); System.out.println(jedis.set("key1","value1")); System.out.println(jedis.set("key2","value2")); System.out.println(jedis.set("key3", "value3")); System.out.println("删除键key2:"+jedis.del("key2")); System.out.println("获取键key2:"+jedis.get("key2")); System.out.println("修改key1:"+jedis.set("key1", "value1Changed")); System.out.println("获取key1的值:"+jedis.get("key1")); System.out.println("在key3后面加入值:"+jedis.append("key3", "End")); System.out.println("key3的值:"+jedis.get("key3")); System.out.println("增加多个键值对:"+jedis.mset("key01","value01","key02","value02","key03","value03")); System.out.println("获取多个键值对:"+jedis.mget("key01","key02","key03")); System.out.println("获取多个键值对:"+jedis.mget("key01","key02","key03","key04")); System.out.println("删除多个键值对:"+jedis.del("key01","key02")); System.out.println("获取多个键值对:"+jedis.mget("key01","key02","key03")); jedis.flushDB(); System.out.println("===========新增键值对防止覆盖原先值=============="); System.out.println(jedis.setnx("key1", "value1")); System.out.println(jedis.setnx("key2", "value2")); System.out.println(jedis.setnx("key2", "value2-new")); System.out.println(jedis.get("key1")); System.out.println(jedis.get("key2")); System.out.println("===========新增键值对并设置有效时间============="); System.out.println(jedis.setex("key3", 2, "value3")); System.out.println(jedis.get("key3")); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(jedis.get("key3")); System.out.println("===========获取原值,更新为新值=========="); System.out.println(jedis.getSet("key2", "key2GetSet")); System.out.println(jedis.get("key2")); System.out.println("获得key2的值的字串:"+jedis.getrange("key2", 2, 4)); } }
对List操作命令
public class TestList { public static void main(String[] args) { Jedis jedis = new Jedis("127.0.0.1", 6379); jedis.flushDB(); System.out.println("===========添加一个list==========="); jedis.lpush("collections", "ArrayList", "Vector", "Stack", "HashMap", "WeakHashMap", "LinkedHashMap"); jedis.lpush("collections", "HashSet"); jedis.lpush("collections", "TreeSet"); jedis.lpush("collections", "TreeMap"); System.out.println("collections的内容:"+jedis.lrange("collections", 0, -1));//-1代表倒数第一个元素,-2代表倒数第二个元素,end为-1表示查询全部 System.out.println("collections区间0-3的元素:"+jedis.lrange("collections",0,3)); System.out.println("==============================="); // 删除列表指定的值 ,第二个参数为删除的个数(有重复时),后add进去的值先被删,类似于出栈 System.out.println("删除指定元素个数:"+jedis.lrem("collections", 2, "HashMap")); System.out.println("collections的内容:"+jedis.lrange("collections", 0, -1)); System.out.println("删除下表0-3区间之外的元素:"+jedis.ltrim("collections", 0, 3)); System.out.println("collections的内容:"+jedis.lrange("collections", 0, -1)); System.out.println("collections列表出栈(左端):"+jedis.lpop("collections")); System.out.println("collections的内容:"+jedis.lrange("collections", 0, -1)); System.out.println("collections添加元素,从列表右端,与lpush相对应:"+jedis.rpush("collections", "EnumMap")); System.out.println("collections的内容:"+jedis.lrange("collections", 0, -1)); System.out.println("collections列表出栈(右端):"+jedis.rpop("collections")); System.out.println("collections的内容:"+jedis.lrange("collections", 0, -1)); System.out.println("修改collections指定下标1的内容:"+jedis.lset("collections", 1, "LinkedArrayList")); System.out.println("collections的内容:"+jedis.lrange("collections", 0, -1)); System.out.println("==============================="); System.out.println("collections的长度:"+jedis.llen("collections")); System.out.println("获取collections下标为2的元素:"+jedis.lindex("collections", 2)); System.out.println("==============================="); jedis.lpush("sortedList", "3","6","2","0","7","4"); System.out.println("sortedList排序前:"+jedis.lrange("sortedList", 0, -1)); System.out.println(jedis.sort("sortedList")); System.out.println("sortedList排序后:"+jedis.lrange("sortedList", 0, -1)); } }
对Hash的操作命令
public class TestHash { public static void main(String[] args) { Jedis jedis = new Jedis("127.0.0.1", 6379); jedis.flushDB(); Map<String,String> map = new HashMap<>(); map.put("key1","value1"); map.put("key2","value2"); map.put("key3","value3"); map.put("key4","value4"); //添加名称为hash(key)的hash元素 jedis.hmset("hash",map); //向名称为hash的hash中添加key为key5,value为value5元素 jedis.hset("hash", "key5", "value5"); System.out.println("散列hash的所有键值对为:"+jedis.hgetAll("hash"));//return Map<String,String> System.out.println("散列hash的所有键为:"+jedis.hkeys("hash"));//return Set<String> System.out.println("散列hash的所有值为:"+jedis.hvals("hash"));//return List<String> System.out.println("将key6保存的值加上一个整数,如果key6不存在则添加key6:"+jedis.hincrBy("hash", "key6", 6)); System.out.println("散列hash的所有键值对为:"+jedis.hgetAll("hash")); System.out.println("将key6保存的值加上一个整数,如果key6不存在则添加key6:"+jedis.hincrBy("hash", "key6", 3)); System.out.println("散列hash的所有键值对为:"+jedis.hgetAll("hash")); System.out.println("删除一个或者多个键值对:"+jedis.hdel("hash", "key2")); System.out.println("散列hash的所有键值对为:"+jedis.hgetAll("hash")); System.out.println("散列hash中键值对的个数:"+jedis.hlen("hash")); System.out.println("判断hash中是否存在key2:"+jedis.hexists("hash","key2")); System.out.println("判断hash中是否存在key3:"+jedis.hexists("hash","key3")); System.out.println("获取hash中的值:"+jedis.hmget("hash","key3")); System.out.println("获取hash中的值:"+jedis.hmget("hash","key3","key4")); } }
基本操作
package com.kuang.multi; import com.alibaba.fastjson.JSONObject; import redis.clients.jedis.Jedis; import redis.clients.jedis.Transaction; public class TestMulti { public static void main(String[] args) { //创建客户端连接服务端,redis服务端需要被开启 Jedis jedis = new Jedis("127.0.0.1", 6379); jedis.flushDB(); JSONObject jsonObject = new JSONObject(); jsonObject.put("hello", "world"); jsonObject.put("name", "java"); //开启事务 Transaction multi = jedis.multi(); String result = jsonObject.toJSONString(); try{ //向redis存入一条数据 multi.set("json", result); //再存入一条数据 multi.set("json2", result); //这里引发了异常,用0作为被除数 int i = 100/0; //如果没有引发异常,执行进入队列的命令 multi.exec(); }catch(Exception e){ e.printStackTrace(); //如果出现异常,回滚 multi.discard(); }finally{ System.out.println(jedis.get("json")); System.out.println(jedis.get("json2")); //最终关闭客户端 jedis.close(); } } }
SpringBoot:Redis整合01.md