Redis 基本概念

Redis是一个开源(BSD协议),内存数据结构存储,可以用作数据 库、缓存以及消息代理。

Redis是所谓的键值存储( key-value store),通常称为NoSQL数据库。

Redis不是普通的键值存储,它实际上是一个data structures server,支持不同类型的值。 这意味着,在传统的键值存储中,您将字符串键与字符串值相关联,在Redis中,值不仅限于简单的字符串,还可以包含更复杂的数据结构。 以下是Redis支持的所有数据结构的列表:

  • Binary-safe strings
  • Lists: 根据插入顺序排序的字符串元素的集合。本质上是链表。
  • Sets: 不同的,未排序的字符串元素的集合
  • Sorted sets: 和Sets类似,但每个字符串元素都有与浮点数值相关联的score。我们可以根据score的排序进行取值操作.(例如,您可能会问:给我前10个,或者下10个)
  • Hashes: 由与值相关联的字段组成的映射。字段和值都是字符串。这与Ruby或Python哈希非常相似。
  • Bit arrays (or simply bitmaps): 使用特殊命令可以像处理位数一样处理字符串值:您可以设置和清除各个位,将所有位设置为1,查找第一个或未设置位,等等。
  • HyperLogLogs: 这是一种概率数据结构,用于估计集合的基数(cardinality).
  • Streams : 追加一系列map-like的项,以提供抽象日志数据类型 仅附加的类似于地图的条目集合,它们提供抽象日志数据类型(append-only collections of map-like entries that provide an abstract log data type)。它们在Redis Streams简介中有详细介绍。

Redis具有内置复制(built-in replication),Lua脚本,LRU驱逐(LRU eviction),事务和不同级别的磁盘持久性,并通过Redis哨兵(Redis Sentinel)提供高可用性,通过Redis Cluster自动分区。

> SET  name  'cc'
OK
> GET  name
"cc"

存取示例

键值存储提供的其他常见操作是DEL删除给定键和关联值,SET-if-not-exists(在Redis上称为SETNX)仅在键不存在时设置键,并且INCR以原子方式递增 存储在给定键上的数字:

常用的其他操作举例
- DEL 可以删除给定的键和关键值
- SETNXSET-if-not-exists不存在的时候设置键
- INCR 以原子的形式进行递增

> GET name
"cc"
> SET connections 10
OK
#自增
> INCR connections
(integer) 11
> INCR connections
(integer) 12
> INCR connections
(integer) 13
#已经有值,因此SETNX不会生效
> SETNX connections 999
(integer) 0
> GET connections
"13"

INCR的操作是原子的,它很有存在的必要。如下列操作,多个客户端同时进行不能保证线程安全。

x = GET count
x = x + 1
SET count x

过期设置

可以通过key来告知缓存失效的确切时间。这是通过EXPIRETTL命令完成的。

>  SET resource:lock "Redis Demo"
OK
>  GET resource:lock
"Redis Demo"
#ttl值为-1 表示永不过期
> TTL resource:lock
(integer) -1
#设置8秒后过期
>  EXPIRE resource:lock 8
#查询剩余过期时间
>  TTL resource:lock
(integer) 8
#aftert 8s
>  GET resource:lock
(nil)
#过期后发现值为-2,表示不再存在
>  TTL resource:lock
(integer) -2

总结:

  • TTL <key>
    • 返回值为 -1 表示永不过期
    • 返回值为 -2 表示不存在。可能已经过期,也可能没存过
  • EXPIRE <key> <秒>
    • 设置过期时间

复杂的数据结构

Redis还支持几种更复杂的数据结构。

列表

我们要看的第一个是列表。列表是一系列有序值。

与列表交互的一些重要命令是

  • RPUSH 添加元素到数组后面(right)
  • LPUSH 添加元素到数组前面
  • LLEN 数组长度
  • LRANGE <A> <B> 相当于截取数组。取出[A,B]区间的数组。下标超过数组范围的不报错/不显示。 -1表示到数组最后一个。
  • LPOP 移除并返回第一个元素
  • RPOP 移除并返回最后一个元素

几个注意事项:

  • 通过Linked Lists来实现。这意味着即使列表中有数百万个元素,在列表的头部或尾部添加新元素的时间也是常量。使用LPUSH命令将新元素添加到具有十个元素的列表的头部的速度与将具有1000万个元素的元素添加到列表头部的速度相同。
  • 缺点:根据下标访没有优势。操作需要的工作量与所访问元素的索引成比例。
  • Redis列表使用Linked Lists列表实现,因为对于数据库系统,能够以非常快的方式将元素添加到很长的列表中是至关重要的。
  • Redis允许我们设置List的上限,只保留最新的n个数据。旧的数据通过LTRIM丢弃。
    • LTRIM命令类似于LRANGE,但它不是显示指定范围的元素,而是将此范围设置为新列表值。超出范围内的元素移除掉。

您可以立即开始使用key存储列表,只要这个key现在没用于存储其他类型的数据。

> RPush friends 'alice'
(integer) 1
> RPush friends 'bill'
(integer) 2
> GET RPush
(nil)
> GET friends\
(nil)
> GET friends
(error) WRONGTYPE Operation against a key holding the wrong kind of value
> LLEN friends
(integer) 2
> LPUSH friends "Sam"
> LRange friends 0 2
1) "Sam"
2) "alice"
3) "bill"
> LRange friends 0 999
1) "Sam"
2) "alice"
3) "bill"
> LRange friends 0 -1
1) "Sam"
2) "alice"
3) "bill"
#移除元素
>  LLEN friends
(integer) 3
>  RPOP friends
"bill"
>  LPOP friends
"Sam"
>  LLEN friends
(integer) 1
>  LLEN friends
(integer) 3
>  RPOP friends
"bill"
>  LPOP friends
"Sam"
>  LLEN friends
(integer) 1
> rpush mylist 1 2 3 4 5
(integer) 5
> ltrim mylist 0 2
OK
# ltrim
> lrange mylist 0 -1
1) "1"
2) "2"
3) "3"

集合

集合类似于列表,但是没有特定的顺序,每个元素只能出现一次。 使用集合的一些重要命令是

  • SADD <k> <v1> 添加值
  • SREM <k> <v1> 移除特定的值
  • SISMEMBER <k> <v1>是否包含特定值
  • SUNION 合并两个或多个集合并返回所有元素的列表
  • SMEMBERS 返回此集合中所有成员的列表
> SADD superpowers "flight"
(integer) 1
>  SADD superpowers "x-ray vision"
(integer) 1
> SADD superpowers "reflexes"
(integer) 1
> SREM superpowers "reflexes"
(integer) 1
> SISMEMBER superpowers "flight"
(integer) 1
> SISMEMBER superpowers "reflexes"
(integer) 0
> SMEMBERS superpowers
1) "x-ray vision"
2) "flight"

sorted sets

集合是一种非常方便的数据类型,但由于它们未排序,因此无法解决许多问题。 这就是Redis 1.2引入Sorted Sets的原因。

有序集类似于常规集,但现在每个值都有一个关联的分数。此分数用于对集合中的元素进行排序。

>  ZADD hackers 1940 "Alan Kay"
(integer) 1
>  ZADD hackers 1940 "Alan Kay"
(integer) 0
> ZADD hackers 1906 "Grace Hopper"
> ZRANGE hackers 2 4
(empty list or set)
>  ZRANGE hackers 1 0
(empty list or set)
#取值, 根据score排序
>  ZRANGE hackers 0 -1
1) "Grace Hopper"
2) "Alan Kay"

Hashes

哈希是字符串字段和字符串值之间的映射,因此它们是表示对象的完美数据类型(例如:具有多个字段的用户,如姓名,姓氏,年龄等).

  • HSET 设置单个值
  • HMSET 设置多个值
  • HGETALL 获取全部hash

数字类型的值的插入方法与字符串相同,数字类型的值提供了原子性的添加方法:HINCRBY

#`user:1000`中存储用户名、email、密码等数据
> HSET user:1000 name "John Smith"
(integer) 1
> HSET user:1000 email "john.smith@example.com"
(integer) 1
> HSET user:1000 password "s3cret"
(integer) 1
> HGETALL user:1000
1) "name"
2) "John Smith"
3) "email"
4) "john.smith@example.com"
5) "password"
6) "s3cret"
#可以一次性赋值
>  HMSET user:1001 name "Mary Jones" password "hidden" email "mjones@example.com"
OK
> HGET user:1001 name
"Mary Jones"
#存储数值类型
#添加访问次数10
> HSET user:1000 visits 10
(integer) 1
#(原子)访问次数增加
> HINCRBY user:1000 visits 1
(integer) 11
>  HGETALL user:1000
1) "name"
2) "John Smith"
3) "email"
4) "john.smith@example.com"
5) "password"
6) "s3cret"
7) "visits"
8) "11"
#删除
#清除访问次数
> HDEL user:1000 visits
(integer) 1
> HGETALL user:1000
1) "name"
2) "John Smith"
3) "email"
4) "john.smith@example.com"
5) "password"
6) "s3cret"
#(原子)访问次数增加
>  HINCRBY user:1000 visits 1
(integer) 1
> HGETALL user:1000
1) "name"
2) "John Smith"
3) "email"
4) "john.smith@example.com"
5) "password"
6) "s3cret"
7) "visits"
8) "1" #访问次数为1

其他知识

Redis keys

Redis keys是二进制安全的,这意味着不管是字符串还是JPEG图片都可以作为key。

关于key的其他规则

  • 不建议过长。过长更占用内存,而且根据key取值时做字符串比较也更耗时。即使当前的任务是匹配大值的存在(Even when the task at hand is to match the existence of a large value),散列它(例如使用SHA1)也是一个更好的主意,特别是从内存和带宽的角度来看。
  • 不建议过端。例如user:1000:followersu1000flw后者不利于可读性。虽然短key消耗内存更少,但您的工作就是找到合适的平衡点。
  • 坚持使用schema。例如,object-type:id是一个好主意,如user:1000.-通常用于多字词字段,如comment:123:reply.tocomment:123:reply-to
  • 允许最大的key为512 MB

Redis Strings

Redis String类型是可以与Redis键关联的最简单的值类型。它是Memcached中唯一的数据类型,因此新手在Redis中使用它也很自然。

> SET mykey 123
OK
> GET mykey
"123"
> SET mykey 'zz'
OK
> GET mykey
"zz"

正如您所看到的,使用SET和GET命令是我们设置和检索字符串值的方式。 请注意,即使key与非字符串值相关联,SET也将替换已存在于key中的任何现有值(如果key已存在)。所以SET执行一项分配。

  • SET值可以是各种类型的string(包括二进制数据)
  • SET值不能大于512 MB
  • SET后面可以加参数nx xx
    • nx : Only set the key if it does not already exist.
    • xx : Only set the key if it already exist.
  • incr原子方式递增
    • 将字符串值解析为整数,将其递增1,最后将获取的值设置为新值。
    • 其他类似的命令,如INCRBY,DECR和DECRBY。在内部,它始终是相同的命令,以稍微不同的方式起作用。

Strings相关的其他命令:

  • GETSET 设置旧值返回新值
  • MSETMGET 设置/取多个值
## nx | xx
> set mykey newval nx
(nil)
> set mykey newval xx
OK
#incr原子方式递增
> set counter 100
OK
> incr counter
(integer) 101
> incr counter
(integer) 102
> incrby counter 50
(integer) 152

> set v1 100
OK
#getset返回旧值、设置新值
> getset v1 200
"100"
> getset v11 200
(nil)
> getset v11 2001
"200"

更改和查询key space

  • EXISTS 返回0或1
  • DEL 返回0或1
> set mykey hello
OK
#查看类型
> type mykey
string
> exists mykey
(integer) 1
> del mykey
(integer) 1
> exists mykey
(integer) 0

比较重要的概念

master/slave

主从(master/slave)

参考 https://lanjingling.github.io/2015/11/17/redis-mast-slaveof/

Redis中,用户可以使用slaveof命令或者slaveof配置项,让一个服务器去复制另一个服务器。进行复制中的主从服务器双方的数据库将保存相同的数据(一致性)。

  • 主数据库可以进行读写操作,当写操作导致数据变化时会自动将数据同步给从数据库
  • 数据库一般是只读的,并接受主数据库同步过来的数据
  • 一个主数据库可以拥有多个从数据库,而一个从数据库只能拥有一个主数据库
  • 命令:
    • slaveof <主redis服务器>
    • 使用slaveof no one进行清空

使用主从的两种方式(不同ip的访问请确保配好远程访问):

1)通过命令slaveof <主redis服务器>

127.0.0.1:6380> slaveof 127.0.0.2 6379

上面的命令表示:端口为6380的服务器 开始复制 端口为6379的服务器。 并且后续数据会同步到从节点。 从节点(slave)是可以只读的,不可以手动set值。

2)通过配置文件: 主服务器不用做任何修改,在从服务器的配置文件中作如下修改:

slaveof 192.168.0.100 6379 (映射到主服务器上)

如果master设置了验证密码,还需配置masterauth。楼主的master设置了验证密码为admin,所以配置masterauth admin。配置完之后启动slave的Redis服务,OK,主从配置完成。

原理
当从数据库启动时,会向主数据库发送sync命令,主数据库接收到sync后开始在后台报错快照rdb,在保存快照期间受到的命名缓存起来,当快照完成时,主数据库会将快照和缓存的命令一块发送给从。复制初始化结束。
之后,主每受到1个命令就同步发送给从。
当出现断开重连后,2.8之后的版本会将断线期间的命令传给重数据库。增量复制
主从复制是乐观复制,当客户端发送写执行给主,主执行完立即将结果返回客户端,并异步的把命令发送给从,从而不影响性能。也可以设置至少同步给多少个从主才可写。
无硬盘复制:如果硬盘效率低将会影响复制性能,2.8之后可以设置无硬盘复制,repl-diskless-sync yes

哨兵

哨兵(sentinel)用于自动化的系统监控和故障恢复功能。

sentinel是redis高可用的解决方案,sentinel系统可以监视一个或者多个redis master服务,以及这些master服务的所有从服务;当某个master服务下线时,自动将该master下的某个从服务升级为master服务替代已下线的master服务继续处理请求。一般建议sentinel采取奇数台,防止某一台sentinel无法连接到master导致误切换。

  • 建议使用redis2.8以上
  • Redis-sentinel本身也是一个独立运行的进程,它能监控多个master-slave集群,发现master宕机后能进行自动切换

SUB/PUB

https://redis.io/topics/pubsub

订阅及发布

  • 订阅
    • 开启订阅,支持一个或多个: SUBSCRIBE <channel1> <channel2>
    • 模糊匹配: PSUBSCRIBE f*
  • 发布
    • 发布信息到频道 PUBLISH <channel> <message>

使用两个终端调试

#终端1  - 订阅
➜  ~ docker exec -it redis redis-cli
127.0.0.1:6379> SUBSCRIBE channel1
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "channel1"
3) (integer) 1
1) "message"
2) "channel1"
3) "hello"
1) "message" 
2) "channel1"
3) "worl"
#终端2  - 发布
➜  ~ docker exec -it redis redis-cli
127.0.0.1:6379> PUBLISH channel1 'hello'
(integer) 1
127.0.0.1:6379> PUBLISH channel1 'worl'
(integer) 1

其他

数据持久化、cluster稍后进行补充

问题

远程访问

#其他机器访问10.10.38.44 redis
$ docker exec -it redis redis-cli -h 10.10.38.44 -p 6379
10.10.38.44:6379> set key 11
Error: Server closed the connection

(不限ip访问的设置)开启远程访问访问,修改配置文件redis.conf :

  1. 注释掉 # bind 127.0.0.1
  2. Redis默认不是以守护进程的方式运行daemonize no,这块不需要修改
  3. 保护模式关掉 protected-mode no

可以根据实际情况灵活配置。生产环境配置请慎重。

参考

连接docker redis超时问题

连接本地redis-server不报错,jedis连接docker返回报错。但是命令行是没问题的。先记录此问题。

2019-06-06 11:33:24.356 ERROR 37771 --- [           main] o.s.boot.SpringApplication               : Application run failed

java.lang.IllegalStateException: Failed to execute ApplicationRunner
	at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:807) ~[spring-boot-2.1.5.RELEASE.jar:2.1.5.RELEASE]
	at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:794) ~[spring-boot-2.1.5.RELEASE.jar:2.1.5.RELEASE]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:324) ~[spring-boot-2.1.5.RELEASE.jar:2.1.5.RELEASE]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1260) ~[spring-boot-2.1.5.RELEASE.jar:2.1.5.RELEASE]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1248) ~[spring-boot-2.1.5.RELEASE.jar:2.1.5.RELEASE]
	at com.example.jedisdemo.JedisDemoApplication.main(JedisDemoApplication.java:17) ~[classes/:na]
Caused by: redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: Read timed out
	at redis.clients.util.RedisInputStream.ensureFill(RedisInputStream.java:202) ~[jedis-2.9.3.jar:na]
	at redis.clients.util.RedisInputStream.readByte(RedisInputStream.java:40) ~[jedis-2.9.3.jar:na]
	at redis.clients.jedis.Protocol.process(Protocol.java:153) ~[jedis-2.9.3.jar:na]
	at redis.clients.jedis.Protocol.read(Protocol.java:218) ~[jedis-2.9.3.jar:na]
	at redis.clients.jedis.Connection.readProtocolWithCheckingBroken(Connection.java:341) ~[jedis-2.9.3.jar:na]
	at redis.clients.jedis.Connection.getStatusCodeReply(Connection.java:240) ~[jedis-2.9.3.jar:na]
	at redis.clients.jedis.Jedis.set(Jedis.java:122) ~[jedis-2.9.3.jar:na]
	at com.example.jedisdemo.JedisDemoApplication.useJedis(JedisDemoApplication.java:63) ~[classes/:na]
	at com.example.jedisdemo.JedisDemoApplication.run(JedisDemoApplication.java:22) ~[classes/:na]
	at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:804) ~[spring-boot-2.1.5.RELEASE.jar:2.1.5.RELEASE]
	... 5 common frames omitted
Caused by: java.net.SocketTimeoutException: Read timed out
	at java.base/java.net.SocketInputStream.socketRead0(Native Method) ~[na:na]
	at java.base/java.net.SocketInputStream.socketRead(SocketInputStream.java:115) ~[na:na]
	at java.base/java.net.SocketInputStream.read(SocketInputStream.java:168) ~[na:na]
	at java.base/java.net.SocketInputStream.read(SocketInputStream.java:140) ~[na:na]
	at java.base/java.net.SocketInputStream.read(SocketInputStream.java:126) ~[na:na]
	at redis.clients.util.RedisInputStream.ensureFill(RedisInputStream.java:196) ~[jedis-2.9.3.jar:na]
	... 14 common frames omitted

参考