1. 安装与配置
1.1 安装
-
解压
Redis-x64-3.2.100_2.zip
,存放到某个文件目录下,重命名为redis文件夹 -
cd
命令进入redis文件夹下,我的位置是C:\softWare\redis
-
测试是否成功:
- 在命令行中输入
redis-server.exe redis.windows.conf
,启动redis服务端,出现下面则成功
- 在命令行中输入
- 启动客户端,存入数据,新开cmd窗口,进入到redis文件下,记住上面的窗口不要关闭。命令:
redis-cli.exe -h 127.0.0.1 -p 6379
;
3. 存入数值和取出数值查看
#存入值
set k01 v01
#取出值
get k01
-
将redis服务的启动,设置为window的服务。redis的服务端窗口必须保持一致打开,可以设置为服务,用于后台启动。
#安装为服务,必须进入redis目录 redis-server --service-install redis.windows.conf #启动服务 redis-server --service-start
常用的服务命令
卸载服务:redis-server --service-uninstall
开启服务:redis-server --service-start
停止服务:redis-server --service-stop
1.2 配置
Redis 的配置文件位于 Redis 安装目录下,文件名为 redis.conf(Windows 名为 redis.windows.conf)。
你可以通过 CONFIG 命令查看或设置配置项。
语法
redis 127.0.0.1:6379> CONFIG GET CONFIG_SETTING_NAME
实例
redis 127.0.0.1:6379> CONFIG GET loglevel
1) "loglevel"
2) "notice"
使用 ***** 号获取所有配置项:
redis 127.0.0.1:6379> CONFIG GET *
1) "dbfilename"
2) "dump.rdb"
3) "requirepass"
4) ""
5) "masterauth"
6) ""
7) "unixsocket"
8) ""
9) "logfile"
10) ""
11) "pidfile"
12) "/var/run/redis.pid"
13) "maxmemory"
14) "0"
15) "maxmemory-samples"
16) "3"
17) "timeout"
18) "0"
19) "tcp-keepalive"
20) "0"
21) "auto-aof-rewrite-percentage"
22) "100"
23) "auto-aof-rewrite-min-size"
24) "67108864"
25) "hash-max-ziplist-entries"
26) "512"
27) "hash-max-ziplist-value"
28) "64"
29) "list-max-ziplist-entries"
30) "512"
31) "list-max-ziplist-value"
32) "64"
33) "set-max-intset-entries"
34) "512"
35) "zset-max-ziplist-entries"
36) "128"
37) "zset-max-ziplist-value"
38) "64"
39) "hll-sparse-max-bytes"
40) "3000"
41) "lua-time-limit"
42) "5000"
43) "slowlog-log-slower-than"
44) "10000"
45) "latency-monitor-threshold"
46) "0"
47) "slowlog-max-len"
48) "128"
49) "port"
50) "6379"
51) "tcp-backlog"
52) "511"
53) "databases"
54) "16"
55) "repl-ping-slave-period"
56) "10"
57) "repl-timeout"
58) "60"
59) "repl-backlog-size"
60) "1048576"
61) "repl-backlog-ttl"
62) "3600"
63) "maxclients"
64) "4064"
65) "watchdog-period"
66) "0"
67) "slave-priority"
68) "100"
69) "min-slaves-to-write"
70) "0"
71) "min-slaves-max-lag"
72) "10"
73) "hz"
74) "10"
75) "no-appendfsync-on-rewrite"
76) "no"
77) "slave-serve-stale-data"
78) "yes"
79) "slave-read-only"
80) "yes"
81) "stop-writes-on-bgsave-error"
82) "yes"
83) "daemonize"
84) "no"
85) "rdbcompression"
86) "yes"
87) "rdbchecksum"
88) "yes"
89) "activerehashing"
90) "yes"
91) "repl-disable-tcp-nodelay"
92) "no"
93) "aof-rewrite-incremental-fsync"
94) "yes"
95) "appendonly"
96) "no"
97) "dir"
98) "/home/deepak/Downloads/redis-2.8.13/src"
99) "maxmemory-policy"
100) "volatile-lru"
101) "appendfsync"
102) "everysec"
103) "save"
104) "3600 1 300 100 60 10000"
105) "loglevel"
106) "notice"
107) "client-output-buffer-limit"
108) "normal 0 0 0 slave 268435456 67108864 60 pubsub 33554432 8388608 60"
109) "unixsocketperm"
110) "0"
111) "slaveof"
112) ""
113) "notify-keyspace-events"
114) ""
115) "bind"
116) ""
redis.conf 配置项说明如下:
序号 | 配置项 | 说明 |
---|---|---|
1 | daemonize no | Redis 默认不是以守护进程的方式运行,可以通过该配置项修改,使用 yes 启用守护进程(Windows 不支持守护线程的配置为 no ) |
2 | pidfile /var/run/redis.pid | 当 Redis 以守护进程方式运行时,Redis 默认会把 pid 写入 /var/run/redis.pid 文件,可以通过 pidfile 指定 |
3 | port 6379 | 指定 Redis 监听端口,默认端口为 6379,作者在自己的一篇博文中解释了为什么选用 6379 作为默认端口,因为 6379 在手机按键上 MERZ 对应的号码,而 MERZ 取自意大利歌女 Alessia Merz 的名字 |
4 | bind 127.0.0.1 | 绑定的主机地址 |
5 | timeout 300 | 当客户端闲置多长时间后关闭连接,如果指定为 0,表示关闭该功能 |
6 | loglevel notice | 指定日志记录级别,Redis 总共支持四个级别:debug、verbose、notice、warning,默认为 notice |
7 | logfile stdout | 日志记录方式,默认为标准输出,如果配置 Redis 为守护进程方式运行,而这里又配置为日志记录方式为标准输出,则日志将会发送给 /dev/null |
8 | databases 16 | 设置数据库的数量,默认数据库为0,可以使用SELECT 命令在连接上指定数据库id |
9 | save <seconds> <changes> Redis 默认配置文件中提供了三个条件:save 900 1save 300 10save 60 10000分别表示 900 秒(15 分钟)内有 1 个更改,300 秒(5 分钟)内有 10 个更改以及 60 秒内有 10000 个更改。 | 指定在多长时间内,有多少次更新操作,就将数据同步到数据文件,可以多个条件配合 |
10 | rdbcompression yes | 指定存储至本地数据库时是否压缩数据,默认为 yes,Redis 采用 LZF 压缩,如果为了节省 CPU 时间,可以关闭该选项,但会导致数据库文件变的巨大 |
11 | dbfilename dump.rdb | 指定本地数据库文件名,默认值为 dump.rdb |
12 | dir ./ | 指定本地数据库存放目录 |
13 | slaveof <masterip> <masterport> | 设置当本机为 slav 服务时,设置 master 服务的 IP 地址及端口,在 Redis 启动时,它会自动从 master 进行数据同步 |
14 | masterauth <master-password> | 当 master 服务设置了密码保护时,slav 服务连接 master 的密码 |
15 | requirepass foobared | 设置 Redis 连接密码,如果配置了连接密码,客户端在连接 Redis 时需要通过 AUTH 命令提供密码,默认关闭 |
16 | maxclients 128 | 设置同一时间最大客户端连接数,默认无限制,Redis 可以同时打开的客户端连接数为 Redis 进程可以打开的最大文件描述符数,如果设置 maxclients 0,表示不作限制。当客户端连接数到达限制时,Redis 会关闭新的连接并向客户端返回 max number of clients reached 错误信息 |
17 | maxmemory <bytes> | 指定 Redis 最大内存限制,Redis 在启动时会把数据加载到内存中,达到最大内存后,Redis 会先尝试清除已到期或即将到期的 Key,当此方法处理 后,仍然到达最大内存设置,将无法再进行写入操作,但仍然可以进行读取操作。Redis 新的 vm 机制,会把 Key 存放内存,Value 会存放在 swap 区 |
18 | appendonly no | 指定是否在每次更新操作后进行日志记录,Redis 在默认情况下是异步的把数据写入磁盘,如果不开启,可能会在断电时导致一段时间内的数据丢失。因为 redis 本身同步数据文件是按上面 save 条件来同步的,所以有的数据会在一段时间内只存在于内存中。默认为 no |
19 | appendfilename appendonly.aof | 指定更新日志文件名,默认为 appendonly.aof |
20 | appendfsync everysec | 指定更新日志条件,共有 3 个可选值:no:表示等操作系统进行数据缓存同步到磁盘(快)always:表示每次更新操作后手动调用 fsync() 将数据写到磁盘(慢,安全)everysec:表示每秒同步一次(折中,默认值) |
21 | vm-enabled no | 指定是否启用虚拟内存机制,默认值为 no,简单的介绍一下,VM 机制将数据分页存放,由 Redis 将访问量较少的页即冷数据 swap 到磁盘上,访问多的页面由磁盘自动换出到内存中(在后面的文章我会仔细分析 Redis 的 VM 机制) |
22 | vm-swap-file /tmp/redis.swap | 虚拟内存文件路径,默认值为 /tmp/redis.swap,不可多个 Redis 实例共享 |
23 | vm-max-memory 0 | 将所有大于 vm-max-memory 的数据存入虚拟内存,无论 vm-max-memory 设置多小,所有索引数据都是内存存储的(Redis 的索引数据 就是 keys),也就是说,当 vm-max-memory 设置为 0 的时候,其实是所有 value 都存在于磁盘。默认值为 0 |
24 | vm-page-size 32 | Redis swap 文件分成了很多的 page,一个对象可以保存在多个 page 上面,但一个 page 上不能被多个对象共享,vm-page-size 是要根据存储的 数据大小来设定的,作者建议如果存储很多小对象,page 大小最好设置为 32 或者 64bytes;如果存储很大大对象,则可以使用更大的 page,如果不确定,就使用默认值 |
25 | vm-pages 134217728 | 设置 swap 文件中的 page 数量,由于页表(一种表示页面空闲或使用的 bitmap)是在放在内存中的,,在磁盘上每 8 个 pages 将消耗 1byte 的内存。 |
26 | vm-max-threads 4 | 设置访问swap文件的线程数,最好不要超过机器的核数,如果设置为0,那么所有对swap文件的操作都是串行的,可能会造成比较长时间的延迟。默认值为4 |
27 | glueoutputbuf yes | 设置在向客户端应答时,是否把较小的包合并为一个包发送,默认为开启 |
28 | hash-max-zipmap-entries 64 hash-max-zipmap-value 512 | 指定在超过一定的数量或者最大的元素超过某一临界值时,采用一种特殊的哈希算法 |
29 | activerehashing yes | 指定是否激活重置哈希,默认为开启(后面在介绍 Redis 的哈希算法时具体介绍) |
30 | include /path/to/local.conf | 指定包含其它的配置文件,可以在同一主机上多个Redis实例之间使用同一份配置文件,而同时各个实例又拥有自己的特定配置文件 |
配置允许远程连接:
-
打开
redis.windows.conf
文件,关闭bind 127.0.0.1
,让服务器允许所有的ip请求。#bind 127.0.0.1
-
设置连接密码,当客户端连接时,需要输入密码
requirepass 123456
-
登录 auth + 密码 // -a 密码
C:\Users\maho>redis-cli -h 127.0.0.1 -p 6379 127.0.0.1:6379> keys * (error) NOAUTH Authentication required. 127.0.0.1:6379> auth 123456 OK 127.0.0.1:6379> keys * (empty list or set) 127.0.0.1:6379> 或者 C:\Users\maho>redis-cli -h 127.0.0.1 -p 6379 -a 123456
2. redis 数据类型
Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)。
String(字符串)
string 是 redis 最基本的类型,你可以理解成与 Memcached 一模一样的类型,一个 key 对应一个 value。
string 类型是二进制安全的。意思是 redis 的 string 可以包含任何数据。比如jpg图片或者序列化的对象。
string 类型是 Redis 最基本的数据类型,string 类型的值最大能存储 512MB。
实例
#set和get是存储和获取的关键字
127.0.0.1:6379> set k1 "v1"
OK
127.0.0.1:6379> set k2 456
OK
127.0.0.1:6379> get k1
"v1"
127.0.0.1:6379> get k2
"456"
127.0.0.1:6379> keys *
1) "k2"
2) "k1"
Hash(哈希)
Redis hash 是一个键值(key=>value)对集合。
Redis hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象。
#hmset和hmget是用于存取hash的数据结构,用于存储对象
127.0.0.1:6379> hmset user1 name "zs" age 18
OK
127.0.0.1:6379> hmget user1 name
1) "zs"
实例中我们使用了 Redis HMSET, HGET 命令,HMSET 设置了两个 field=>value 对, HGET 获取对应 field 对应的 value。
每个 hash 可以存储 23^2 -1 键值对(40多亿)
List(列表)
Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)
redis 127.0.0.1:6379> DEL runoob
redis 127.0.0.1:6379> lpush runoob redis
(integer) 1
redis 127.0.0.1:6379> lpush runoob mongodb
(integer) 2
redis 127.0.0.1:6379> lpush runoob rabitmq
(integer) 3
redis 127.0.0.1:6379> lrange runoob 0 10
1) "rabitmq"
2) "mongodb"
3) "redis"
redis 127.0.0.1:6379>
列表最多可存储 23^2 - 1 元素 (4294967295, 每个列表可存储40多亿)。
Set(集合)
Redis 的 Set 是 string 类型的无序集合。
集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。
redis 127.0.0.1:6379> DEL runoob
redis 127.0.0.1:6379> sadd runoob redis
(integer) 1
redis 127.0.0.1:6379> sadd runoob mongodb
(integer) 1
redis 127.0.0.1:6379> sadd runoob rabitmq
(integer) 1
redis 127.0.0.1:6379> sadd runoob rabitmq
(integer) 0
redis 127.0.0.1:6379> smembers runoob
1) "redis"
2) "rabitmq"
3) "mongodb"
#####################################
127.0.0.1:6379> sadd k4 1
(integer) 1
127.0.0.1:6379> sadd k4 3
(integer) 1
127.0.0.1:6379> sadd k4 2
(integer) 1
127.0.0.1:6379> smembers k4
1) "1"
2) "2"
3) "3"
**注意:**以上实例中 rabitmq 添加了两次,但根据集合内元素的唯一性,第二次插入的元素将被忽略。
集合中最大的成员数为 2^32 - 1(4294967295, 每个集合可存储40多亿个成员)。
zset (sorted set) 有序集合
Redis zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。
zset的成员是唯一的,但分数(score)却可以重复。
redis 127.0.0.1:6379> DEL runoob
redis 127.0.0.1:6379> zadd runoob 0 redis
(integer) 1
redis 127.0.0.1:6379> zadd runoob 0 mongodb
(integer) 1
redis 127.0.0.1:6379> zadd runoob 0 rabitmq
(integer) 1
redis 127.0.0.1:6379> zadd runoob 0 rabitmq
(integer) 0
redis 127.0.0.1:6379> > ZRANGEBYSCORE runoob 0 1000
1) "mongodb"
2) "rabitmq"
3) "redis"
3. Redis键 (key)
Redis key命令
序号 | 命令及描述 | |
---|---|---|
1 | DEL key 该命令用于在 key 存在时删除 key。 | 删除 |
2 | DUMP key 序列化给定 key ,并返回被序列化的值。 | |
3 | EXISTS key 检查给定 key 是否存在。 | |
4 | EXPIRE key seconds 为给定 key 设置过期时间,以秒计。 | |
5 | EXPIREAT key timestamp EXPIREAT 的作用和 EXPIRE 类似,都用于为 key 设置过期时间。 不同在于 EXPIREAT 命令接受的时间参数是 UNIX 时间戳(unix timestamp)。 | |
6 | PEXPIRE key milliseconds 设置 key 的过期时间以毫秒计。 | |
7 | PEXPIREAT key milliseconds-timestamp 设置 key 过期时间的时间戳(unix timestamp) 以毫秒计 | |
8 | KEYS pattern 查找所有符合给定模式( pattern)的 key 。 | |
9 | MOVE key db 将当前数据库的 key 移动到给定的数据库 db 当中。 |
#1. 删除key对应的元素
127.0.0.1:6379> del k1
(integer) 1
127.0.0.1:6379> del k1 k2
(integer) 1
127.0.0.1:6379> del k
(integer) 0
#2. 序列化key,返回序列化的值
127.0.0.1:6379> dump k4
"\x0b\x0e\x02\x00\x00\x00\x03\x00\x00\x00\x01\x00\x02\x00\x03\x00\a\x00Z0\x89\x11e"
#3. exists 检查key是否存在,返回存在的数量
127.0.0.1:6379> exists k4 k5 k1
(integer) 2
#4. expire key s 给key设置过期时间,秒计算
127.0.0.1:6379> expire k4 10
(integer) 1
127.0.0.1:6379> ttl k4
(integer) 5
127.0.0.1:6379> ttl k4 #查看key的剩余时间
(integer) 0
127.0.0.1:6379> keys *
1) "k5"
2) "user1"
3) "k3"
#5. 移除key的过期时间,将永远保存,persist
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> expire k1 25
(integer) 1
127.0.0.1:6379> persist k1
(integer) 1
127.0.0.1:6379> ttl k1
(integer) -1
#6. keys */?
*表示匹配多个字符
?表示一个字符,表示一个字符这个位置的key是未知的。
key过期时间场景: expire
- 手机验证码
- 限制网站的访问评率
- 网站数据缓存,比如排行榜的过期
- 限时的的优惠活动
4. String
############存值
#1.set
set ORDER:0001 "oeder[name=apple,price=15]"
#2.setnx key value 指定key不存在,才能赋值成功 (set if Not eXists)
setnx ORDER:0001 "oeder[name=apple,price=15]" //赋值失败,存在相同的key
setnx ORDER:0002 "oeder[name=pear,price=20]" //1
##################取值
#1.get
get ORDER:0001
#2.截取取值 getRange key start end
getRange ORDER:001 1 2 //截取的是开始和结尾的下标
127.0.0.1:6379> getRange ORDER:0001 2 4
"der"
#3.取值在赋值 getset key value
127.0.0.1:6379> getset ORDER:0002 order[name=haha,price=250]
"oeder[name=pear,price=20]" //返回了旧值
127.0.0.1:6379> get ORDER:0002
"order[name=haha,price=250]" //存入了新值
###############自增/自减
#1. 自增 incr key 将存的数值值增加1,如果不存在,会先初始化为0,在增加
incr NUM:1
127.0.0.1:6379> incr NUM:1//返回的是增长后的结果值。
(integer) 1
127.0.0.1:6379> get NUM:1
"1"
#1.incrBy key num //自己确定自增变量
incrby NUM:1 100
127.0.0.1:6379> incrby NUM:1 100
(integer) 101
#2. 自减 ,不存在,初始化为0,然后-1. 同样存在自减增量 decrBy key num
127.0.0.1:6379> decr NUM:1
(integer) 1
应用场景:
- String通常用于保存字符串或者json串,作为缓存使用
- **因为String是二进制安全的,**所以可以把一个图片的内容当做字符串来存贮。
- 计数器,(常规key-value缓存应用。常规计数:微博数,粉丝数,用incr 和 decr来记录,然后写会数据库)
INCR等指令本身具有原子性,所以我们可以利用,redis的incr, incrby,decr,decrby等指令完成原子的计数操作。比如某场景有3个客户端同时读取了mynum的值为2,然后对其同时加1操作,那么最后mynum是5.
5. Hash
Redis hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象。
Redis 中每个 hash 可以存储 232 - 1 键值对(40多亿)。对于hash,每一个hash数据,就是一张hash
表,hash表中用于存储键值对(对象属性)。
#########存值
#1. hset key f1 v1 f2 v2
hset USER:1 sex man //赋值单个属性
hmset USER:1 id 1 name mahao age 18 //赋值多个属性
###########取值
hget USER:1 name //取值单个属性
hmget USER:1 id name //取出多个属性
hgetall USER:1 //取出key的全部属性
127.0.0.1:6379> hgetall USER:1
1) "id"
2) "1"
3) "name"
4) "mahao"
5) "age"
6) "18"
7) "sex"
8) "man"
#2. 获取hash表中所有的键值对的key
hkeys key
#3. 获取每个hash表中多少个key
hlen key
127.0.0.1:6379> hkeys USER:1
1) "id"
2) "name"
3) "age"
4) "sex"
127.0.0.1:6379> hlen USER:1
(integer) 4
###############删除
#1. 删除hash表
del key
# 2. 删除hash表中的属性
hdel key field
#################其他语法
#1. hsetnx 字段field不存在再去赋值
hsetnx USER:1 name zs
#2.hincrby key field num 为hash表中的字段增加增量
127.0.0.1:6379> hincrby USER:2 id 10 //不存在,则会创建后增加
(integer) 10
127.0.0.1:6379> hincrby USER:1 age 3 //存在直接增加
(integer) 21
127.0.0.1:6379>
#3. 为浮点数增加num
hincrbyfloat key field num
#4.hexists USER:1 name //判断name属性是否存在
应用场景:
-
常用于存储一个对象
-
为什么不用String存储一个对象?
hash是最接近关系型数据库结构的数据类型,可以将数据库的一条记录或者程序中的一个对象转换为hashmap存放在redis中。
用户id为查找的key,存储的value用户对象包括姓名,年龄生日等信息,如果用普通的key/value形式存储数据,有2中方式:
第一种方式将用户的id作为查找的key,吧其他信息封装成一个对象,以序列化的方式存储或者转为json串,这种缺点是增加了序列化与反序列化的开销,当修改其中的某一个属性时,需要将整个对象反序列化,并且修改操作需要对其进行并发保护,有cas等问题。
第二种方式是将用户信息多少属性,封装成多少个key/value形式,用户的id+属性名作为key,解决了序列化问题,但是id重复浪费空间。
总结:
redis提供的hash和好的解决了这个问题,redis的hash的实际存储是value为一个HashMap,并提供了直接存取这个map成员的接口
6. Java连接redis
依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
package chapter1;
import com.sun.tracing.dtrace.ArgsAttributes;
import org.junit.Test;
import redis.clients.jedis.Jedis;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
/**
* Jedis操作redis
*
* @author: mahao
* @date: 2019/10/7
*/
public class Jedis1 {
final static String host = "127.0.0.1";
final static int port = 6379;
/**
* 操作string数据
*
* @param args
*/
public static void main(String[] args) {
Jedis jedis = new Jedis(host, port);
String auth = jedis.auth("123456");
jedis.set("ORDER:03", "order[订单3]");
String s = jedis.get("ORDER:03");
System.out.println(s);
//setnx命令,不存在key,则赋值
Long num = jedis.setnx("ORDER:03", "新订单3");
System.out.println(num);//返回更改的记录数
//getset获取旧的,并设置新的
String getset1 = jedis.getSet("ORDER:05", "新订单3");//不存在旧值,返回null,并设置新的
String getset2 = jedis.getSet("ORDER:03", "新订单3");
System.out.println(getset1);
System.out.println(getset2);
Long incr = jedis.incr("k1");//自增1,返回增加后的结果
Long by = jedis.incrBy("k1", 10);//自增10
Boolean exists = jedis.exists("k1");//存在指定key
jedis.persist("k1"); //让key持续保存
jedis.close();
}
/**
* 测试hash类型数据
*/
@Test
public void testHash() {
Jedis jedis = new Jedis(host, port);
String auth = jedis.auth("123456");
//hset设置单个值
Long hset = jedis.hset("USER:01", "name", "mahao");
//向hash表中存数据
Long hset2 = jedis.hset("USER:01", "age", "18");
//hmset 设置多个值
jedis.hmset("USER:02", new HashMap<>());
//获取单个值
String name = jedis.hget("USER:01", "name");
//获取多个值
List<String> hmget = jedis.hmget("USER:01", "name", "age");
//获取hashmap的keys
Set<String> hkeys = jedis.hkeys("USER:01");
/
//删除某个属性,或多个
Long hdel = jedis.hdel("USER:01", "name", "gae");
其余操作
//为某个属性自增操作
Long hincrBy = jedis.hincrBy("USER:01", "age", 18);
}
}
7. List
Redis列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)(LinkedList)
一个列表最多可以包含 232 - 1 个元素 (4294967295, 每个列表超过40亿个元素)。
list基本:
###########存值,就是双向队列
#1. 左右存值
lpush key v2 v1
rpush key v3 v4
127.0.0.1:6379> lpush l1 v2 v1
(integer) 2
127.0.0.1:6379> lrange l1 0 -1
1) "v1"
2) "v2"
127.0.0.1:6379> rpush l1 v3 v4
(integer) 4
127.0.0.1:6379> lrange l1 0 -1
1) "v1"
2) "v2"
3) "v3"
4) "v4"
#2. 左右存值,如果不存在key,则不存
lpushx l1 v0
rpushx l2 v1
####################取值语法
#1. 范围取值
lrange key start end //start end 为索引值
当 end为-1时,表示是最后一个值,start end 都可以为负数,表示倒数第几个
127.0.0.1:6379> lrange l1 -3 -1
1) "v2"
2) "v3"
3) "v4"
#2. 其他
llen key //列表长度
lindex key index //索引位置元素
127.0.0.1:6379> llen l1
(integer) 5
127.0.0.1:6379> llen l2
(integer) 0
127.0.0.1:6379> lindex l1 2
"v2"
127.0.0.1:6379>
###########删除元素
#1. 删除key
del key
#2. 删除元素,带返回元素
lpop key 移除列表第一个元素
rpop key 移除列表的最后一个元素
127.0.0.1:6379> lpop l1
"v0"
127.0.0.1:6379> rpop l1
"v4"
127.0.0.1:6379> lrange l1 0 -1
1) "v1"
2) "v2"
3) "v3"
#3. 阻塞移除,可做阻塞队列,当元素为null,一直等待,直到元素添加进来。
blpop key1 [key2...] timeout
案例;
开辟两个客户端窗口,操作l1 列表,窗口2取出数据,当取完后,会阻塞,
如果窗口1添加元素,则窗口2会得到元素。
#4. 修剪,tirm只需要指定索引的元素
ltrim key start end
#######################修改
#1. 修改索引位置元素为v,超出索引报错
lset key index v
#2. 在指定元素的前/后插入元素
linsert key before/after node newnode
应用场景:
项目中常用与: 1. 对数据量大的集合数据删减 2.任务队列
-
对数据量大的集合数据删减
列表数据显示,关注列表,粉丝列表,留言评价等…分页,热点新闻(top5)等。
利用lrange还可以很方便的的实现分页功能,在博客系统中,每片博文的评论也可以存入一个单独的list中。
-
任务队列
#1. rpoplpush list1 list2 //将list的右边,添加到list的左边
127.0.0.1:6379> lrange list1 0 -1
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
127.0.0.1:6379> lrange list2 0 -1
1) "a"
2) "b"
3) "c"
4) "d"
5) "e"
127.0.0.1:6379> rpoplpush list1 list2 //list1队列出队列,插入到list头部
"5"
127.0.0.1:6379> lrange list1 0 -1
1) "1"
2) "2"
3) "3"
4) "4"
127.0.0.1:6379> lrange list2 0 -1
1) "5"
2) "a"
3) "b"
4) "c"
5) "d"
6) "e"
127.0.0.1:6379>
##############当出队列和移除队列的是同一个元素时,则是循环了
rpoplpush list1 list1
127.0.0.1:6379> rpoplpush list1 list1
"3"
127.0.0.1:6379> lrange list1 0 -1
1) "3"
2) "1"
3) "2"
##################可以阻塞队列交互
Brpoplpush list1 list2 timeOut //当list1队列尾部出时,没有元素,则会阻塞等待,直到有元素移除为止。
(list通常来实现一个消息队列,而且可以确保先后顺序,不想mysql那样需要通过order by进行排序)。
任务队列介绍(生产者和消费者模式):
在处理web客户端发送的命令请求时,某些操作的执行 时间可能会比我们预期的更长一些,通过将待执行任务的相关信息放在队列里面,并在之后对队列进行处理,这种任务存放队列做法成为任务队列。
RpopLpush list1 list2,这种做法是移除队列尾部,并插入到另一个队列的头部。
案例:订单系统的下单流程,用户系统的登录注册短信等
import bean.Util;
import redis.clients.jedis.Jedis;
import java.util.List;
/**
* 使用list数据结构实现,任务队列,
* 模拟快递的运输流程,将已经完成的任务插入到已经完成的list中。
* 这里使用redis是将它作为任务队列使用。
*
* @author: mahao
* @date: 2019/10/8
*/
public class TaskQueue {
public static final String ORDER = "ORDER:";
static Jedis jedis = Util.getJedis();
public static void main(String[] args) throws InterruptedException {
final String orderId = "25004";
TaskQueue task = new TaskQueue();
task.createTask(orderId);
List<String> noTask = null;
List<String> succTask = null;
while ((noTask = task.listTask(orderId)) != null && noTask.size() > 0) {
String s = task.touchTask(orderId);
System.out.println("触发的任务是: " + s);
succTask = task.listSucc(orderId);
System.out.println("尚未完成的任务: " + noTask);
System.out.println("已经完成的任务: " + succTask);
System.out.println("===========================================");
Thread.sleep(2000);
}
}
//1.创建一个新的任务队列
public void createTask(String orderId) {
/*
流程:
1.发货
2.到北京
3.到商丘
4.签收
*/
Long rpush = jedis.lpush(ORDER + orderId, "1.发货", " 2.到北京", " 3.到商丘", " 4.签收");
}
//2.触发任务
public String touchTask(String orderId) {
String rpoplpush = jedis.rpoplpush(ORDER + orderId, ORDER + orderId + ":succ");
return rpoplpush;//完成的操作
}
//已经完成的任务
public List<String> listSucc(String orderId) {
return jedis.lrange(ORDER + orderId + ":succ", 0, -1);
}
//未完成的任务
public List<String> listTask(String orderId) {
return jedis.lrange(ORDER + orderId, 0, -1);
}
}
8. Set
Redis 的 Set 是 String 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。
Redis 中集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。
集合中最大的成员数为 232 - 1 (4294967295, 每个集合可存储40多亿个成员)。
set数据类型是通过hash表来实现的,所以,类似HashSet,底层实现也是通过hashmap实现,将value作为key,
所以set的存取速度快,复杂度是O(1).
#############赋值
#1. sadd key v1 v2
################# 取值
#1. set大小
scard key
#2. set成员
smembers key
#3. 判断set中是否存在元素
sismember key member
#4. 随机返回值
srandmember key count
127.0.0.1:6379> srandmember set1 2
1) "v2"
2) "v3"
#####################删除值
#1.移除set中1个多个值
srem key v1 v2
#2.随机移除并返回元素
spop key num
127.0.0.1:6379> spop set1 2
1) "v2"
2) "v1"
127.0.0.1:6379> smembers set1
1) "v3"
#3. 移除元素从一个set到另一个set
smove source destination v1
127.0.0.1:6379> smove set1 set2 v3
(integer) 1
127.0.0.1:6379> smove set1 set2 v1
(integer) 0
####差集,交集,并集#####
#1. 差集 ,给定所有集合的差集(左set有的,右set没有)
sdiff key1 key2
sdiffstore key3 key1 key2 //将key1和key2的差集,仿造key3中
#2. 交集
sinter k1 k2 //显示交集
sinterstore k3 k1 k2
#3. 并集
sunion k1 k2
应用场景:
常用于:对两个集合间的数据进行交集,并集,和差集运算。
- 非常方便的实现共同关注,共同喜好,二度好友等功能。
- 利用唯一性,实现访问网站的所有独立ip。
9. zset
Redis 有序集合和集合一样也是string类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。
有序集合的成员是唯一的,但分数(score)却可以重复。
集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。 集合中最大的成员数为 232 - 1 (4294967295, 每个集合可存储40多亿个成员)。
###################存贮
# 1. 添加时,需要为元素指定s1,作为大小
zset key s1 v1 s2 v2
######################取值
#1. 获取集合的成员数
zcard key
#2. 计算在有序集合中指定区间分数的成员数
zcount key min max
#4. 返回索引区间有序集合,由低到高排序
zrange key start end
zrevrange key start end //高到低排序
127.0.0.1:6379> zrange z1 0 -1
1) "1"
2) "a"
3) "2"
4) "22"
5) "b"
6) "3"
7) "c"
8) "4"
#3. 返回有序集合中指定成员的索引,是有序排列的。
zrank key a
127.0.0.1:6379> zrank z1 a
(integer) 1
127.0.0.1:6379> zrank z1 2
(integer) 2
######################## 删除
#1. 删除元素
zrem key v1
#2.移除有序集合中给定的排名区间的所有成员,根据索引
zremrangerank key start end
127.0.0.1:6379> zrange z1 0 -1
1) "2"
2) "22"
3) "3"
4) "c"
5) "4"
127.0.0.1:6379> zremrangebyrank z1 0 2 //删除下标从0到2的元素
(integer) 3
127.0.0.1:6379> zrange z1 0 -1
1) "c"
2) "4"
#3. 删除分数区间
zremrangebyscore key min max
应用场景:
排行榜
- 发布文章,以发布时间作为score,文章作为值,这样就是排序好的
- 比如一个存储全班同学的成绩的sorted set,集合value作为学号,score作为分数,数据在插入时候,祭祀排好序的。
- 做带权重的队列,比如普通消息score是1,重要的是2,然后工作线程选择score来获取工作的任务。
10 发布订阅
Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。
Redis 客户端可以订阅任意数量的频道。
下图展示了频道 channel1 , 以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 之间的关系:
当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端:
#A窗口和B窗口开启订阅消息:
redis 127.0.0.1:6379> SUBSCRIBE redisChat
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "redisChat"
3) (integer) 1
#c窗口发布消息
redis 127.0.0.1:6379> PUBLISH redisChat "Redis is a great caching technique"
(integer) 1
redis 127.0.0.1:6379> PUBLISH redisChat "Learn redis by runoob.com"
(integer) 1
# 订阅者的客户端会显示如下消息
1) "message"
2) "redisChat"
3) "Redis is a great caching technique"
1) "message"
2) "redisChat"
3) "Learn redis by runoob.com"
下表列出了 redis 发布订阅常用命令:
序号 | 命令及描述 |
---|---|
1 | [PSUBSCRIBE pattern pattern …] 订阅一个或多个符合给定模式的频道。 |
2 | [PUBSUB subcommand argument [argument …]] 查看订阅与发布系统状态。 |
3 | PUBLISH channel message 将信息发送到指定的频道。 |
4 | [PUNSUBSCRIBE pattern [pattern …]] 退订所有给定模式的频道。 |
5 | [SUBSCRIBE channel channel …] 订阅给定的一个或多个频道的信息。 |
6 | [UNSUBSCRIBE channel [channel …]] 指退订给定的频道。 |
场景:
构建实时消息系统,比如聊天功能:
- 发布新文章,推送给关注的粉丝,微信公众号的模式。
11. 多数据库
redis有16个数据库 ,从0-15命名,通过select 数据库名
来切换数据库:
select 1 #切换数据库为1的库
数据局清空:
flushdb
flushall 清空所有库
12. redis 事务
Redis事务可以一次执行多个命令,并且带有以下3个重要的保证:
- 批量操作在发送exec命令前被放入到队列缓存
- 收到exec命令后进入事务执行,事务中任意命令失败,其余命令都可以被执行。
- 在事务执行的过程中,其他客户端提交的命令不会插入到事务执行的命令序列中。
multi开始一个事务,然后多个命令入队到事务中,最后有exec命令触发,一并执行事务中的所有命令:
127.0.0.1:6379[1]> multi #开启事务
OK
127.0.0.1:6379[1]> decrby s1 20 #s1减去20元
QUEUED
127.0.0.1:6379[1]> get s1 #显示金额
QUEUED
127.0.0.1:6379[1]> incrby s2 20 #s2增加20元
QUEUED
127.0.0.1:6379[1]> get s2 #显示金额
QUEUED
127.0.0.1:6379[1]> exec #执行命令 discard 撤回执行
1) (integer) 80
2) "80"
3) (integer) 100
4) "100"
单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增加任何维持原子性的机制,所以 Redis 事务的执行并不是原子性的。
事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。
比如:
127.0.0.1:6379[1]> multi
OK
127.0.0.1:6379[1]> set s1 25
QUEUED
127.0.0.1:6379[1]> get s1
QUEUED
127.0.0.1:6379[1]> hset s2 name mahao #这是两个错误的
QUEUED
127.0.0.1:6379[1]> hget s2 name #错误
QUEUED
127.0.0.1:6379[1]> get s1
QUEUED
127.0.0.1:6379[1]> exec
1) OK
2) "25"
3) (error) WRONGTYPE Operation against a key holding the wrong kind of value
4) (error) WRONGTYPE Operation against a key holding the wrong kind of value
5) "25"
看到执行错误,单是任然不会回滚,这是因为,redis事务认为,发生错误是逻辑错误,只保证命令执行不被其他客户端插入,其余不负责。
13. Redis持久化
1. RDB
RDB相当于照快照,保存的是一种状态,10G的数据几kb的数据。
是默认的持久化方式,这种方式是将内存中的数据以快照的方式下入到二进制文件中,默认的文件名是dump.rdb
优点:
快照保存数据极快,还原数据极快
适用于灾难备份
**缺点:**小内存机器不适合,RDB机制符合要求就会照快照。
快照条件:
-
服务器正常关闭时
redis-cli shutdown
-
key满足一定的条件,会进行快照。配置文件中有描述:
2. AOF
由于快照的方式是在一定间隔时间做的,所以如果发生意外,就会丢失最后一次快照后的所有的修改。如果数据要求严格的话,可以采用aof持久化方式。
Append-only file :aof比快照方式有更好的持久性,由于在使用aof持久化时,redis会将每一个收到的写命令都通过write函数追加到文件中(appendonly.aof)。当redis重启时,会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。
问题:
持久化文件会变得越来越大,比如调用incr 命令100次,直接结果是100,但需要进行100次的调用。
14 redis做缓存问题
们使用缓存时,我们的业务系统大概的调用流程如下图:
当我们查询一条数据时,先去查询缓存,如果缓存有就直接返回,如果没有就去查询数据库,然后返回。这种情况下就可能会出现一些现象。
1. 缓存穿透
1.什么是缓存穿透:
正常情况下,我们去查询数据库的数据都是存在的。
但是如果请求的数据是根本不存在的,也就是缓存和数据库都查询不到这条数据,但是每次请求都会打到数据库上面去。
这种查询不存在的数据的现象称为缓存穿透
2.带来的问题:
如果大量请求,哪一个不存在的id去查询数据,会产生大量的请求到数据库查询。可能会导致你的数据库由于压力过大而down掉。
3.解决方法
-
缓存空值
之所以会发生穿透,就是因为缓存中没有存储这些空数据的key。从而导致每次查询都需要到数据库中查询。那么就可以给这些找不到数据的key,设置成null,存储到缓存中。后面在查询这个key时,就直接返回null,这样就不会访问数据库了,别忘记设置过期时间。
-
BloomFilter
BloomFilter 类似于一个hbase set 用来判断某个元素(key)是否存在于某个集合中。
这种方式在大数据场景应用比较多,比如 Hbase 中使用它去判断数据是否在磁盘上。还有在爬虫场景判断url 是否已经被爬取过。
这种方案可以加在第一种方案中,在缓存之前在加一层 BloomFilter ,在查询的时候先去 BloomFilter 去查询 key 是否存在,如果不存在就直接返回,存在再走查缓存 -> 查 DB。
4. 如何选择
对于恶意攻击,攻击带过来的大量不存在的key,那么采用第一种方式就会缓存大量不存在的key的数据。则会造成缓存的太多,这时候采用第一种就不合适了。
我们可以使用第二种方案,进行过滤掉这些key。这对这些key异常多,请求重复的率比较低的数据,没必要缓存,使用第二种方案直接过滤掉。
而低于空数据比较有限的,重复率比较高的,我们可以使用缓存空值为value的方式。
2. 缓存击穿
1.什么是击穿
在平常高并发的系统中,大量的请求同时查询一个 key 时,此时这个key正好失效了,就会导致大量的请求都打到数据库上面去。这种现象我们称为缓存击穿。
2.带来的问题
会造成某一个时候数据库请求量巨大,压力剧增。
3.如何解决
上面的现象是多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询的请求上使用一个互斥锁来锁住他。
其线程走到这一步拿不到锁,只能等着,等第一个线程查询到了数据,然后做缓存。后面的线程进来发现已经有缓存了,就直接走缓存。
package chapter3;
import bean.Util;
import redis.clients.jedis.Jedis;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
/**
* 加互斥锁解决缓存击穿:
* 在平常高并发的系统中,大量的请求同时查询一个 key 时,此时这个key正好失效了,就会导致大量的请求都打到数据库上面去。
* 这种现象我们称为**缓存击穿**。
* <p>
* **2.带来的问题**
* 会造成某一个时候数据库请求量巨大,压力剧增。
* <p>
* **3.如何解决**
* 上面的现象是多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询的请求上使用一个互斥锁来锁住他。
* <p>
* 其线程走到这一步拿不到锁,只能等着,等第一个线程查询到了数据,然后做缓存。后面的线程进来发现已经有缓存了,就直接走缓存。
* <p>
* <p>
* 注意Jedis的坑,因为有递归,如果关闭了两次上,会报错。
*
* @author: mahao
* @date: 2019/10/8
*/
public class CacheLock {
final ReentrantLock lock = new ReentrantLock();
static AtomicInteger redisCount = new AtomicInteger(0);
static AtomicInteger sqlCount = new AtomicInteger(0);
/**
* 模拟1000个请求,每个请求100次数据
*
* @param args
*/
public static void main(String[] args) throws InterruptedException {
CacheLock instance = new CacheLock();
final String key = "CACHE:01";
ExecutorService executor = Executors.newCachedThreadPool();
for (int i = 0; i < 1000; i++) {//1000个任务
executor.execute(() -> {
Jedis jedis = Util.getJedis();
for (int j = 0; j < 20; j++) {//100次请求数据
try {
instance.getData(key, jedis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
jedis.close();
});
}
executor.shutdown();
executor.awaitTermination(10, TimeUnit.MINUTES);
System.out.println(redisCount.get());
System.out.println(sqlCount.get());
}
/**
* 从缓存中获取数据,获取区间会发生缓存击穿的问题
*/
public String getData(String key, Jedis jedis) throws InterruptedException {
String data = getDataRedis(key, jedis);
if (data == null) {//缓存为null,加锁从数据库中查询
if (lock.tryLock()) {//尝试获取锁,成功了去查询数据库,并设置缓存
data = getDataSQL(key);
setCache(key, data, jedis);
lock.unlock();
} else {//获取锁,失败,修整一会,递归在此获取数据
Thread.sleep(100);
//这里的递归操作,则会再去执行从redis查询并获取的操作。
return getData(key, jedis);
}
}
return data;
}
public static String getDataRedis(String key, Jedis jedis) {
int i = redisCount.incrementAndGet();
System.out.println("redis--" + i);
if (jedis.exists(key))
return jedis.get(key);
return null;
}
//模拟数据库查询
public static String getDataSQL(String key) {
int i = sqlCount.incrementAndGet();
System.out.println("sql--" + i);
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "mysql" + key;
}
//5秒失效
public static void setCache(String key, String value, Jedis jedis) {
jedis.set(key, value);
jedis.expire(key, 30);
}
}
3. 缓存雪崩
1.什么是缓存雪崩
就是大面积的缓存击穿,某一时刻大规模的缓存失效,比如缓存服务器宕机了,会有大量的请求打到数据库上,DB挂了。
2. 解决方法:
-
事前
使用集群缓存,保证缓存服务的高可用。
这种方案就是在发生雪崩前对缓存集群实现高可用,如果是使用 Redis,可以使用 主从+哨兵 ,Redis Cluster 来避免 Redis 全盘崩溃的情况。
-
事中
ehcache本地缓存 + Hystrix限流&降级,避免mysql被打死使用 ehcache 本地缓存的目的也是考虑在 Redis Cluster 完全不可用的时候,ehcache 本地缓存还能够支撑一阵。
使用 Hystrix进行限流 & 降级 ,比如一秒来了5000个请求,我们可以设置假设只能有一秒 2000个请求能通过这个组件,那么其他剩余的 3000 请求就会走限流逻辑。
然后去调用我们自己开发的降级组件(降级),比如设置的一些默认值呀之类的。以此来保护最后的 MySQL 不会被大量的请求给打死。
-
事后
开启Redis持久化机制,尽快恢复缓存集群一旦重启,就能从磁盘上自动加载数据恢复内存中的数据。
防止雪崩方案:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xtAjI6qK-1570588128510)(…/…/…/image/20191008201141.jpg)]
4. 解决热点数据集中失效问题
我们在设置缓存的时候,一般会给缓存设置一个失效时间,过了这个时间,缓存就失效了。
对于一些热点的数据来说,当缓存失效以后会存在大量的请求过来,然后打到数据库去,从而可能导致数据库崩溃的情况。
解决方法
-
1. 设置不同的失效时间
为了避免这些热点的数据集中失效,那么我们在设置缓存过期时间的时候,我们让他们失效的时间错开。
比如在一个基础的时间上加上或者减去一个范围内的随机值。
-
2. 互斥锁
结合上面的击穿的情况,在第一个请求去查询数据库的时候对他加一个互斥锁,其余的查询请求都会被阻塞住,直到锁被释放,从而保护数据库。
但是也是由于它会阻塞其他的线程,此时系统吞吐量会下降。需要结合实际的业务去考虑是否要这么做。