Redis入门
Redis(Remote directory server,远程字典服务器)是完全开源免费的,用C语言编写的,遵守BSD协议,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。
Redis是单进程
Redis作者antirez,也是dump1090的作者
Redis写每秒8w,读每秒11w
Redis能读的速度是110000次/s,写的速度是81000次/s
相比memcached,Redis支持更丰富的数据结构,例如hashes, lists, sets等,同时支持数据持久化。除此之外,Redis还提供一些类数据库的特性,比如事务,HA,主从库。可以说Redis兼具了缓存系统和数据库的一些特性,因此有着丰富的应用场景。
redis指令不区分大小写,但是出于规范考虑,应该使用大写
redis中存放的键是区分大小写的
redis的值是否区分大小写?????
Redis 与其他 key - value 缓存产品有以下三个特点
- Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用
- Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储(memecache只支持KV的数据类型)
- Redis支持数据的备份,即master-slave模式的数据备份(虽然数据放在内存中, 但是写到硬盘)
统一密码管理,16个数据库都使用一个密码
默认端口是6379(数字键盘的merz,而MERZ取自意大利歌女Alessia Merz的名字)
安装&执行
使用gcc执行make
make install
安装在了/usr/local/bin
redis-server /opt/redis-4.0.9/redis.conf
redis-cli -p 6379
127.0.0.1:6379> set k1 hello
OK
127.0.0.1:6379> get k1
"hello"
# 关闭
127.0.0.1:6379> SHUTDOWN
杂的命令
SELECT index 切换到指定的数据库
默认16个数据库,在业务上,某些服务找一号库,某些服务找二号库,这样1.有分担,2.逻辑上清晰
select 0
select 15
select 16 // 错误
PING 查看服务是否运行
QUIT 关闭当前连接
# 查看value的类型
127.0.0.1:6379> type k1
string
dbsize,当前数据库中key的数量
keys *,列出所有的key
127.0.0.1:6379> dbsize
(integer) 2
127.0.0.1:6379> keys *
1) "k1"
2) "k2"
FLUSHALL 清除所有库的数据(一定不要用这个命令!!!)
FLUSHDB 清除当前库的数据
127.0.0.1:6379> info
# Server
redis_version:3.2.100
redis_git_sha1:00000000
redis_git_dirty:0
redis_build_id:dd26f1f93c5130ee
redis_mode:standalone
os:Windows
arch_bits:64
multiplexing_api:WinSock_IOCP
process_id:1528
run_id:885af51c70425c50f51ecfff882006b0dfbe8349
tcp_port:6379
uptime_in_seconds:5168
uptime_in_days:0
hz:10
lru_clock:14086418
executable:C:\Program Files\Redis\redis-server.exe
config_file:C:\Program Files\Redis\redis.windows-service.conf
# Clients
connected_clients:1
client_longest_output_list:0
client_biggest_input_buf:0
blocked_clients:0
# Memory
used_memory:689664
used_memory_human:673.50K
used_memory_rss:651776
used_memory_rss_human:636.50K
used_memory_peak:1657216
used_memory_peak_human:1.58M
total_system_memory:0
total_system_memory_human:0B
used_memory_lua:37888
used_memory_lua_human:37.00K
maxmemory:0
maxmemory_human:0B
maxmemory_policy:noeviction
mem_fragmentation_ratio:0.95
mem_allocator:jemalloc-3.6.0
# Persistence
loading:0
rdb_changes_since_last_save:0
rdb_bgsave_in_progress:0
rdb_last_save_time:1524031052
rdb_last_bgsave_status:ok
rdb_last_bgsave_time_sec:0
rdb_current_bgsave_time_sec:-1
aof_enabled:1
aof_rewrite_in_progress:0
aof_rewrite_scheduled:0
aof_last_rewrite_time_sec:-1
aof_current_rewrite_time_sec:-1
aof_last_bgrewrite_status:ok
aof_last_write_status:ok
aof_current_size:58101543
aof_base_size:58101204
aof_pending_rewrite:0
aof_buffer_length:0
aof_rewrite_buffer_length:0
aof_pending_bio_fsync:0
aof_delayed_fsync:0
# Stats
total_connections_received:1
total_commands_processed:22
instantaneous_ops_per_sec:0
total_net_input_bytes:688
total_net_output_bytes:5891669
instantaneous_input_kbps:0.00
instantaneous_output_kbps:0.00
rejected_connections:0
sync_full:0
sync_partial_ok:0
sync_partial_err:0
expired_keys:0
evicted_keys:0
keyspace_hits:8
keyspace_misses:0
pubsub_channels:0
pubsub_patterns:0
latest_fork_usec:52006
migrate_cached_sockets:0
# Replication
role:master
connected_slaves:0
master_repl_offset:0
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
# CPU
used_cpu_sys:0.95
used_cpu_user:5.55
used_cpu_sys_children:0.00
used_cpu_user_children:0.00
# Cluster
cluster_enabled:0
# Keyspace
db0:keys=3,expires=0,avg_ttl=0
Redis数据类型 五+一
Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。这就是为什么redis干掉了memcache的原因
string(字符串)
hash(哈希)
list(列表)
set(集合)
zset(sorted set:有序集合)
Key 键 KEYS EXISTS MOVE EXPIRE TTL
keys *
# 判断一个key是否存在
127.0.0.1:6379> EXISTS k1
(integer) 1
127.0.0.1:6379> set k1 hello
OK
127.0.0.1:6379> get k1
"hello"
# 把k1移动到1号库
127.0.0.1:6379> MOVE k1 1
(integer) 0
# EXPIRE给指定的key设置过期时间;
# TTL 查看key还有多久过期(-1永不过期,-2已经过期)
# 比如某个促销活动就持续三天,就可以设置key的过期时间
127.0.0.1:6379> TTL k1
(integer) -1
# 设置k2在10s后消失,过期后,k2会自动被移出数据库
127.0.0.1:6379> expire k2 10
(integer) 1
127.0.0.1:6379> ttl k2
(integer) 2
127.0.0.1:6379> ttl k2
(integer) 0
127.0.0.1:6379> ttl k2
(integer) -2 # 过期了
# 再次查看,k2已经是空值了
127.0.0.1:6379> get k2
(nil)
# 查看keys *,已经没有k2了
127.0.0.1:6379> keys *
1) "k1"
String 字符串 SET GET APPEND STRLEN INCR DECR INCBY DECRBY
string类型是二进制安全的。意思是redis的string可以包含任何数据。比如jpg图片或者序列化的对象 。
String是单值单value
127.0.0.1:6379> get k1
"v1"
127.0.0.1:6379> set k1 v11
OK
127.0.0.1:6379> get k1
"v11" # 被覆盖
# append和STRLEN
127.0.0.1:6379> get k1
"v11"
127.0.0.1:6379> APPEND k1 hello
(integer) 8
127.0.0.1:6379> get k1
"v11hello"
127.0.0.1:6379> STRLEN k1
(integer) 8
# INCR,DECR,INCRBY,DECBY
# 注意:value必须是数字!!!!
127.0.0.1:6379> set k2 1
OK
127.0.0.1:6379> INCR k2
(integer) 2
127.0.0.1:6379> INCR k2
(integer) 3
127.0.0.1:6379> INCR k2
(integer) 4
127.0.0.1:6379> DECR k2
(integer) 3
127.0.0.1:6379> DECR k2
(integer) 2
127.0.0.1:6379> DECR k2
(integer) 1
127.0.0.1:6379> DECR k2
(integer) 0
127.0.0.1:6379> DECR k2
(integer) -1
127.0.0.1:6379> DECR k2
(integer) -2
# INCRBY DECRBY
127.0.0.1:6379> INCRBY k2 3 // 每次递增3
(integer) 1
127.0.0.1:6379> INCRBY k2 3
(integer) 4
127.0.0.1:6379> DECRBY k2 2
(integer) 2
127.0.0.1:6379> DECRBY k2 2
(integer) 0
# getrange setrange (范围内取值,范围内设置,类似Java的substring)
127.0.0.1:6379> set k3 helloworld
OK
127.0.0.1:6379> get k3
"helloworld"
127.0.0.1:6379> getrange k3 0 4
"hello"
127.0.0.1:6379> setrange k3 5 xxxxx
(integer) 10
127.0.0.1:6379> get k3
"helloxxxxx"
# setex(set with expire)
127.0.0.1:6379> setex k4 10 v4
OK
127.0.0.1:6379> ttl k4
(integer) 6
127.0.0.1:6379> ttl k4
(integer) 4
127.0.0.1:6379> ttl k4
(integer) 0
127.0.0.1:6379> ttl k4
(integer) -2 // k4死掉了
# setnx(set if not exist)只有key不存在的时候才能set
127.0.0.1:6379> get k1
"v11hello"
127.0.0.1:6379> setnx k1 v1
(integer) 0
127.0.0.1:6379> get k1
"v11hello"
# mset mget msetnx 连续多个的设置
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3
OK
127.0.0.1:6379> mget k1 k2 k3 k4
1) "v1"
2) "v2"
3) "v3"
4) (nil)
msetnx,如果部分已经存在了,那所有的都不成功
List 列表 LPUSH RPUSH LPOP RPOP LRANGE
Redis的List是类似Java中的ArrayList还是LinkedList?
答案:它底层是一个链表
正因为是链表,因此可以在list的头部和尾部添加一个元素
因此List在头部或者尾部插入元素的效率都很高,但是在中间插入元素,效率就很低
# lpush rpush lrange
# lpush是一个栈,后进的先出
127.0.0.1:6379> lpush list1 1 2 3 4 5
(integer) 5
127.0.0.1:6379> lrange list1 0 -1
1) "5"
2) "4"
3) "3"
4) "2"
5) "1"
# rpush是怎么进来的怎么出去,是一个队列
127.0.0.1:6379> rpush list2 1 2 3 4 5
(integer) 5
127.0.0.1:6379> lrange list2 0 -1
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
# lpop和rpop
127.0.0.1:6379> lrange list1 0 -1
1) "5"
2) "4"
3) "3"
4) "2"
5) "1"
127.0.0.1:6379> lpop list1
"5"
127.0.0.1:6379> rpop list1
"1"
# lindex 按照索引下标获取元素
127.0.0.1:6379> lrange list1 0 -1
1) "4"
2) "3"
3) "2"
127.0.0.1:6379> lindex list1 0
"4"
127.0.0.1:6379> lindex list1 1
"3"
127.0.0.1:6379> lindex list1 2
"2"
127.0.0.1:6379> lindex list1 3
(nil)
# lrem 删除n个value
127.0.0.1:6379> lrange list4 0 -1
1) "3"
2) "3"
3) "3"
4) "2"
5) "2"
6) "2"
7) "1"
8) "1"
9) "1"
127.0.0.1:6379> lrem list4 2 1 // 删除两个1
(integer) 2
127.0.0.1:6379> lrange list4 0 -1
1) "3"
2) "3"
3) "3"
4) "2"
5) "2"
6) "2"
7) "1"
# ltrim key 开始index 结束index
# rpoplpush 从一个list取数据,然后添加到另一个数据中
127.0.0.1:6379> lpush list01 1 2 3
(integer) 3
127.0.0.1:6379> lrange list01 0 -1
1) "3"
2) "2"
3) "1"
127.0.0.1:6379> lpush list02 4 5 6
(integer) 3
127.0.0.1:6379> lrange list02 0 -1
1) "6"
2) "5"
3) "4"
127.0.0.1:6379> rpoplpush list01 list02
"1"
127.0.0.1:6379> lrange list02 0 -1
1) "1"
2) "6"
3) "5"
4) "4"
# lset 更新指定索引的元素
127.0.0.1:6379> lrange list01 0 -1
1) "3"
2) "2"
127.0.0.1:6379> lset list01 0 1
OK
127.0.0.1:6379> lrange list01 0 -1
1) "1"
2) "2"
# linsert 在某个元素之前之后插入
127.0.0.1:6379> lrange list01 0 -1
1) "1"
2) "2"
127.0.0.1:6379> linsert list01 before 1 0
(integer) 3
127.0.0.1:6379> lrange list01 0 -1
1) "0"
2) "1"
3) "2"
Set 集合 SADD SMEMBERS SISMEMBER
# sadd 添加,smembers 查看set中所有元素,sismember 查看一个值是不是set的元素
127.0.0.1:6379> sadd set01 1 1 2 2 3 3
(integer) 3
127.0.0.1:6379> smembers set01
1) "1"
2) "2"
3) "3"
127.0.0.1:6379> sismember set01 1
(integer) 1
127.0.0.1:6379> sismember set01 5
(integer) 0
# scard 查看有多少个元素
127.0.0.1:6379> smembers set01
1) "1"
2) "2"
3) "3"
127.0.0.1:6379> scard set01
(integer) 3
# srem 移出某个元素
# srandmember key N (从某个set中随机的取出N个元素,比如一个砸金蛋的业务,从几万个人中随机的选出N个人)
# spop key 随机一个元素出栈
127.0.0.1:6379> sadd set01 1 2 3 4 5 6 7 8 9
(integer) 7
127.0.0.1:6379> SMEMBERS set01
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "6"
7) "7"
8) "8"
9) "9"
127.0.0.1:6379> SRANDMEMBER set01 3
1) "6"
2) "3"
3) "4"
127.0.0.1:6379> SRANDMEMBER set01 3
1) "1"
2) "3"
3) "2"
127.0.0.1:6379>
127.0.0.1:6379> spop set01
"4"
127.0.0.1:6379> spop set01
"7"
数学的差集 sdiff 交集sinter 并集 sunion
127.0.0.1:6379> SMEMBERS set01
1) "1"
2) "2"
3) "3"
4) "5"
5) "6"
6) "8"
7) "9"
127.0.0.1:6379> SMEMBERS set02
1) "9"
2) "10"
3) "11"
4) "12"
127.0.0.1:6379> sdiff set01 set02 //在set01中,但是不再set02中
1) "1"
2) "2"
3) "3"
4) "5"
5) "6"
6) "8"
127.0.0.1:6379> sdiff set02 set01 // //在set02中,但是不再set01
1) "10"
2) "11"
3) "12"
127.0.0.1:6379> sinter set01 set02
1) "9"
127.0.0.1:6379> sunion set01 set02
1) "1"
2) "2"
3) "3"
4) "5"
5) "6"
6) "8"
7) "9"
8) "10"
9) "11"
10) "12"
Hash 哈希 重中之重
hash是一个键值对的集合,类似Java中的Map<String, Object>
hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。
假设有两个java类,一个user,一个school
class User {
private String name;
private Integer id;
}
class School {
private String name;
private String city;
private Integer level;
}
# hset hget hmset hmget
127.0.0.1:6379> hset user01 id 1
(integer) 1
127.0.0.1:6379> hset user01 name jack
(integer) 1
127.0.0.1:6379> hmget user01 id name
1) "1"
2) "jack"
127.0.0.1:6379> hmset user02 id 2 name zhichao
OK
127.0.0.1:6379> hmget user02 id name
1) "2"
2) "zhichao"
#
127.0.0.1:6379> hgetall user01
1) "id"
2) "1"
3) "name"
4) "jack"
# hdel hgetall
127.0.0.1:6379> hdel user01 name
(integer) 1
127.0.0.1:6379> hgetall user01
1) "id"
2) "1"
# hkeys hvals
127.0.0.1:6379> hkeys user02
1) "name"
2) "id"
127.0.0.1:6379> hvals user02
1) "zhichao"
2) "2"
Zset 有序集合
相对于Set,Zset的每一个元素都会关联一个double类型的分数,通过分数来排序
游戏用这个数据结构非常的多,就是通过Zset进行排序的
如果两个元素的double的分数相同,怎么办?
# zadd zrange
127.0.0.1:6379> zadd zset01 50 v1 60 v2 70 v3 80 v4 90 v5
(integer) 5
127.0.0.1:6379> zadd zset01 50.5 v6
(integer) 1
127.0.0.1:6379> zrange zset01 0 -1 // 从小到大排列
1) "v1"
2) "v6"
3) "v2"
4) "v3"
5) "v4"
6) "v5"
解析配置文件 redis.conf
在redis启动的时候,需要指定配置文件
./redis-server /path/to/redis.conf
# Note on units: when memory size is needed, it is possible to specify
# it in the usual form of 1k 5GB 4M and so forth:
#
# 1k => 1000 bytes
# 1kb => 1024 bytes
# 1m => 1000000 bytes
# 1mb => 1024*1024 bytes
# 1g => 1000000000 bytes
# 1gb => 1024*1024*1024 bytes
redis日志的记录方式
dir ./是rdb和aom两者都生效的?
常用配置
参数说明
redis.conf 配置项说明如下:
- Redis默认不是以守护进程的方式运行,可以通过该配置项修改,使用yes启用守护进程
daemonize no - 当Redis以守护进程方式运行时,Redis默认会把pid写入/var/run/redis.pid文件,可以通过pidfile指定
pidfile /var/run/redis.pid - 指定Redis监听端口,默认端口为6379,作者在自己的一篇博文中解释了为什么选用6379作为默认端口,因为6379在手机按键上MERZ对应的号码,而MERZ取自意大利歌女Alessia Merz的名字
port 6379 - 绑定的主机地址
bind 127.0.0.1 - 当客户端闲置多长时间后关闭连接,如果指定为0,表示关闭该功能
timeout 300 - 指定日志记录级别,Redis总共支持四个级别:debug、verbose、notice、warning,默认为verbose
loglevel verbose - 日志记录方式,默认为标准输出,如果配置Redis为守护进程方式运行,而这里又配置为日志记录方式为标准输出,则日志将会发送给/dev/null
logfile stdout - 设置数据库的数量,默认数据库为0,可以使用SELECT <dbid>命令在连接上指定数据库id
databases 16 - 指定在多长时间内,有多少次更新操作,就将数据同步到数据文件,可以多个条件配合
save <seconds> <changes>
Redis默认配置文件中提供了三个条件:
save 900 1
save 300 10
save 60 10000
分别表示900秒(15分钟)内有1个更改,300秒(5分钟)内有10个更改以及60秒内有10000个更改。 - 指定存储至本地数据库时是否压缩数据,默认为yes,Redis采用LZF压缩,如果为了节省CPU时间,可以关闭该选项,但会导致数据库文件变的巨大
rdbcompression yes - 指定本地数据库文件名,默认值为dump.rdb
dbfilename dump.rdb - 指定本地数据库存放目录
dir ./ - 设置当本机为slave服务时,设置master服务的IP地址及端口,在Redis启动时,它会自动从master进行数据同步
slaveof <masterip> <masterport> - 当master服务设置了密码保护时,slave服务连接master的密码
masterauth <master-password> - 设置Redis连接密码,如果配置了连接密码,客户端在连接Redis时需要通过AUTH <password>命令提供密码,默认关闭
requirepass foobared - 设置同一时间最大客户端连接数,默认无限制,Redis可以同时打开的客户端连接数为Redis进程可以打开的最大文件描述符数,如果设置 maxclients 0,表示不作限制。当客户端连接数到达限制时,Redis会关闭新的连接并向客户端返回max number of clients reached错误信息
maxclients 128 - 指定Redis最大内存限制,Redis在启动时会把数据加载到内存中,达到最大内存后,Redis会先尝试清除已到期或即将到期的Key,当此方法处理 后,仍然到达最大内存设置,将无法再进行写入操作,但仍然可以进行读取操作。Redis新的vm机制,会把Key存放内存,Value会存放在swap区
maxmemory <bytes> - 指定是否在每次更新操作后进行日志记录,Redis在默认情况下是异步的把数据写入磁盘,如果不开启,可能会在断电时导致一段时间内的数据丢失。因为 redis本身同步数据文件是按上面save条件来同步的,所以有的数据会在一段时间内只存在于内存中。默认为no
appendonly no - 指定更新日志文件名,默认为appendonly.aof
appendfilename appendonly.aof - 指定更新日志条件,共有3个可选值:
no:表示等操作系统进行数据缓存同步到磁盘(快)
always:表示每次更新操作后手动调用fsync()将数据写到磁盘(慢,安全)
everysec:表示每秒同步一次(折衷,默认值)
appendfsync everysec - 指定是否启用虚拟内存机制,默认值为no,简单的介绍一下,VM机制将数据分页存放,由Redis将访问量较少的页即冷数据swap到磁盘上,访问多的页面由磁盘自动换出到内存中(在后面的文章我会仔细分析Redis的VM机制)
vm-enabled no -
虚拟内存文件路径,默认值为/tmp/redis.swap,不可多个Redis实例共享
vm-swap-file /tmp/redis.swap - 将所有大于vm-max-memory的数据存入虚拟内存,无论vm-max-memory设置多小,所有索引数据都是内存存储的(Redis的索引数据 就是keys),也就是说,当vm-max-memory设置为0的时候,其实是所有value都存在于磁盘。默认值为0
vm-max-memory 0 - Redis swap文件分成了很多的page,一个对象可以保存在多个page上面,但一个page上不能被多个对象共享,vm-page-size是要根据存储的 数据大小来设定的,作者建议如果存储很多小对象,page大小最好设置为32或者64bytes;如果存储很大大对象,则可以使用更大的page,如果不 确定,就使用默认值
vm-page-size 32 - 设置swap文件中的page数量,由于页表(一种表示页面空闲或使用的bitmap)是在放在内存中的,,在磁盘上每8个pages将消耗1byte的内存。
vm-pages 134217728 - 设置访问swap文件的线程数,最好不要超过机器的核数,如果设置为0,那么所有对swap文件的操作都是串行的,可能会造成比较长时间的延迟。默认值为4
vm-max-threads 4 - 设置在向客户端应答时,是否把较小的包合并为一个包发送,默认为开启
glueoutputbuf yes - 指定在超过一定的数量或者最大的元素超过某一临界值时,采用一种特殊的哈希算法
hash-max-zipmap-entries 64
hash-max-zipmap-value 512 - 指定是否激活重置哈希,默认为开启(后面在介绍Redis的哈希算法时具体介绍)
activerehashing yes - 指定包含其它的配置文件,可以在同一主机上多个Redis实例之间使用同一份配置文件,而同时各个实例又拥有自己的特定配置文件
include /path/to/local.conf
INCLUDES 可以包含其他配置文件
Include one or more other config files here. This is useful if you have a standard template that goes to all Redis servers but also need to customize a few per-server settings.
# include /path/to/local.conf
# include /path/to/other.conf
MODULES 不知道
NETWORK
让redis只监听指定ip地址下的客户端连接
如果不配置的话,redis服务会暴露在整个公网下,这样很危险
tcp-backlog 511
# Close the connection after a client is idle for N seconds (0 to disable)
timeout 0
# A reasonable value for this option is 300 seconds, which is the new
# Redis default starting with Redis 3.2.1.
tcp-keepalive 300
GENERAL
SNAPSHOTTING 和rdb有关
Redis是内存数据库,掉点应该保存到硬盘上
# Save the DB on disk:
#
# save <seconds> <changes> 如果有N个键有更改,那么X秒之后进行备份
save 900 1
save 300 10
save 60 10000
一分钟之内改了1w次,5分钟之内改了10次,15分钟之内改了1次
如果不想保存备份,就不设置任何save指令,或者save ""
# Note: you can disable saving completely by commenting out all "save" lines.
#
# It is also possible to remove all the previously configured save
# points by adding a save directive with a single empty string argument
# like in the following example:
#
# save ""
stop-writes-on-bgsave-error 如果后台在save的时候发生了错误,那么前台是否要停止写操作,默认yes
如果不介意数据的一致性,可以改成no
# By default Redis will stop accepting writes if RDB snapshots are enabled
# (at least one save point) and the latest background save failed.
# This will make the user aware (in a hard way) that data is not persisting
# on disk properly, otherwise chances are that no one will notice and some
# disaster will happen.
#
# If the background saving process will start working again Redis will
# automatically allow writes again.
#
# However if you have setup your proper monitoring of the Redis server
# and persistence, you may want to disable this feature so that Redis will
# continue to work as usual even if there are problems with disk,
# permissions, and so forth.
stop-writes-on-bgsave-error yes
是否启用LZF压缩算法来压缩.rdb文件,默认是yes
# Compress string objects using LZF when dump .rdb databases?
# For default that's set to 'yes' as it's almost always a win.
# If you want to save some CPU in the saving child set it to 'no' but
# the dataset will likely be bigger if you have compressible values or keys.
rdbcompression yes
在存储快照后,会通过CRC64进行数据校验
但是在保存或加载rdb文件的时候,会增加10%的性能消耗
如果希望提升性能,这一项可以改成no
# Since version 5 of RDB a CRC64 checksum is placed at the end of the file.
# This makes the format more resistant to corruption but there is a performance
# hit to pay (around 10%) when saving and loading RDB files, so you can disable it
# for maximum performances.
#
# RDB files created with checksum disabled have a checksum of zero that will
# tell the loading code to skip the check.
rdbchecksum yes
# The filename where to dump the DB
dbfilename dump.rdb
指定rdb的存放目录(这个配置说明是在当前目录存放数据库)(“当前”指的是,从那个目录启动的redis,哪个目录就是当前)
# The working directory.
#
# The DB will be written inside this directory, with the filename specified
# above using the 'dbfilename' configuration directive.
#
# The Append Only File will also be created inside this directory.
#
# Note that you must specify a directory here, not a file name.
dir ./
REPLICATION
SECURITY
Redis的安全理念是,既然你已经能进入到linux,就默认你是安全的用户,可以直接使用redis
但是如果你是一个无聊的强迫症, 那可以设置密码
一旦设置了密码,那执行命令之前,需要先输入密码
# Require clients to issue AUTH <PASSWORD> before processing any other
# commands.
127.0.0.1:6379> config get requirepass
1) "requirepass"
2) ""
# 设置密码
127.0.0.1:6379> config set requirepass "qweasd"
OK
# 此时再请求,就需要输入密码
127.0.0.1:6379> config get requirepass
(error) NOAUTH Authentication required.
# 输入密码
127.0.0.1:6379> auth qweasd
OK
127.0.0.1:6379> config get requirepass
1) "requirepass"
2) "qweasd"
127.0.0.1:6379> ping
PONG
# 取消密码
127.0.0.1:6379> config set requirepass ""
OK
CLIENTS
默认最大连接数是10000,设置redis同时可以与多少个客户端进行连接。默认情况下为10000个客户端。
如果达到了此限制,redis则会拒绝新的连接请求,并且向这些连接请求方发出“max number of clients reached”以作回应。
# maxclients 10000
MEMORY MANAGEMENT
When the memory limit is reached Redis will try to remove keys according to the eviction policy selected (see maxmemory-policy).
# maxmemory <bytes>
内存的移除策略(当内存不够用了,应该怎么清理内存),默认是noeviction,永不过期
# MAXMEMORY POLICY: how Redis will select what to remove when maxmemory
# is reached. You can select among five behaviors:
#
# volatile-lru -> Evict using approximated LRU among the keys with an expire set.
# allkeys-lru -> Evict any key using approximated LRU.
# volatile-lfu -> Evict using approximated LFU among the keys with an expire set.
# allkeys-lfu -> Evict any key using approximated LFU.
# volatile-random -> Remove a random key among the ones with an expire set.
# allkeys-random -> Remove a random key, any key.
# volatile-ttl -> Remove the key with the nearest expire time (minor TTL)
# noeviction -> Don't evict anything, just return an error on write operations.
#
# LRU means Least Recently Used
# LFU means Least Frequently Used
#
# The default is:
#
# maxmemory-policy noeviction
- noeviction: 不进行置换,表示即使内存达到上限也不进行置换,所有能引起内存增加的命令都会返回error
- allkeys-lru: 优先删除掉最近最不经常使用的key,用以保存新数据
- volatile-lru: 只从设置失效(expire set)的key中选择最近最不经常使用的key进行删除,用以保存新数据
- allkeys-random: 随机从all-keys中选择一些key进行删除,用以保存新数据
- volatile-random: 只从设置失效(expire set)的key中,选择一些key进行删除,用以保存新数据
- volatile-ttl: 只从设置失效(expire set)的key中,选出存活时间(TTL)最短的key进行删除,用以保存新数据
Redis有这样一个配置——maxmemory-samples,Redis的LRU是取出配置的数目的key,然后从中选择一个最近最不经常使用的key进行置换,默认的5
maxmemory-samples 5
LAZY FREEING
APPEND ONLY MODE
默认是关闭的
# By default Redis asynchronously dumps the dataset on disk. This mode is
# good enough in many applications, but an issue with the Redis process or
# a power outage may result into a few minutes of writes lost (depending on
# the configured save points).
#
# The Append Only File is an alternative persistence mode that provides
# much better durability.
# AOF和RDB是可以并存的,如果AOF存在,则redis在启动的时候会load AOF
# AOF and RDB persistence can be enabled at the same time without problems.
# If the AOF is enabled on startup Redis will load the AOF, that is the file
# with the better durability guarantees.
appendonly no
# The name of the append only file (default: "appendonly.aof")
appendfilename "appendonly.aof"
# appendfsync always // 同步持久化,每次发生数据变更会立刻记录到磁盘,性能差但是数据完整性好.但是在双十一大型活动的时候, 这个选项会严重拖慢机器性能
appendfsync everysec // 默认配置,异步操作,每一秒记录一次,如果一秒内宕机,则有数据丢失
# appendfsync no //
no-appendfsync-on-rewrite no
LUA SCRIPTING
REDIS CLUSTER
CLUSTER DOCKER/NAT support
SLOW LOG
LATENCY MONITOR
EVENT NOTIFICATION
ADVANCED CONFIG
ACTIVE DEFRAGMENTATION
Redis的持久化 Persistence
Redis支持数据持久化,可以将内存中的数据保存在硬盘上,然后重启的时候再次加载进行使用
RDB Redis DataBase 指定的时间间隔对数据进行快照存储
在指定的时间间隔之内将内存中的数据快照写入磁盘,即Snapshot快照
一个子进程,将数据写入到一个临时文件中,等待这个持久化结束后,将临时文件的内容替换到上一次持久化的文件中
主进程在这期间不进行IO操作(???不懂)
RDB持久化保存的是dump.rdb文件
只在本机保存dump.rdb文件肯定是不安全的,因此当拿到dump.rdb文件后,运维人员会写定时的脚本,定期的将本台的rdb文件备份到另一台机器上
如何触发rdb备份
TODO:执行flushall之后,也会立刻生成rdb文件(但是别手贱)
- 按照save的配置规则
- 使用save或bgsave命令
- save 命令会阻塞其他操作, 此时redis只能进行保存
- bgsave(background)后台异步的备份
127.0.0.1:6379> bgsave
Background saving started
恢复dump.rdb
只要有了dump.rdb,redis启动后会自动进行快速的回复
127.0.0.1:6379> config get dir
1) "dir"
2) "C:\\Program Files\\Redis"
修复dump.rdb redis-check-dump
AOF Append Only File 记录的写指令,服务器重启后会再执行一遍写指令
RDB会丢失最后一次的数据(比如在默认的配置文件中,会丢失15min内的数据),技术大拿们有强迫症,比如要解决这个问题,推出了AOF:
AOF 以日志形式记录了每个写操作(注意:只记录写操作),在redis启动之后会按照日志中的记录重新执行一遍写指令
appendonly.aof文件,内容如下
*2
$6
SELECT
$1
0
*3
$3
set
$2
k1
$2
v1
*3
$3
set
$2
k2
$2
v2
恢复
flushall之后,dump.rdb中是空,appendonly.aof中也是空
修复
如果appendonly.aof文件中有损坏,是否能启动服务???
答:如果appendonly.aof文件有损坏,服务不会启动!!!
但是有一个redis-check-aof.exe
C:\Program Files\Redis>redis-check-aof.exe ./appendonly.aof
0x 51: Expected prefix ''', got: '*'
AOF analyzed: size=90, ok_up_to=81, diff=9
AOF is not valid
# --fix 参数可以修复aof文件
C:\Program Files\Redis>redis-check-aof.exe --fix ./appendonly.aof
0x 68: Expected prefix '1', got: '*'
AOF analyzed: size=116, ok_up_to=104, diff=12
This will shrink the AOF from 116 bytes, with 12 bytes, to 104 bytes
Continue? [y/N]: y
Successfully truncated AOF
rewrite
AOF什么都好,但是AOF采用文件追加方式,文件会越来越大。尤其是大型活动的时候,文件瞬间就撑爆了。为避免出现此种情况,新增了重写机制,当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集。可以使用命令bgrewriteaof
AOF自身有一个精简的能力:
set k1 10
incr k1
incr k1
incr k1
... // 增加了1w次
这种操作在AOF中会被优化成
set k1 100000
精简典型的场景比如:
1+1+1+1...+1 = 8
2<<3 = 8
精简的触发条件是:当AOF文件的大小超过了所设定的阈值,Redis就会启动AOF文件的压缩
只保留能够恢复数据的最小指令集
bgrewriteaof
触发rewrite
-
bgrewriteof
:
127.0.0.1:6379> BGREWRITEAOF
Background append only file rewriting started
- 当AOF文件大小是上次rewire后大小的一倍,且文件大于64M时触发
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
在生产环境,3G是起步价
重写的原理
当AOF过大时,会fork出一个新进程来将文件重写(类似dump的过程),
总结
如何选择:
- 如果你只希望用redis做缓存,即只希望数据在服务器运行的时候存在,此时就不需要使用持久化
- 如果允许一些数据的丢失,就优先使用rdb
如果两个持久化策略都存在
可以同时使用两种持久化,重启后会使用AOF进行恢复,因为AOF的数据集更完整。
但是作者不建议只是用AOF,此时RDB是作为一个以防万一的backup
RDB
- 优势:对数据的完整性要求不高的时候,适合大规模的数据恢复
- 劣势:
- 因为是隔一段时间进行备份,那万一服务挂掉了,那数据不会即时保存
- 备份的时候会fork一个子线程进行备份,这样占用内存增大一倍
AOF
- 优势: 配置非常的灵活,appendsync有always,everysec,no
- 劣势:
- 相同数据集的数据要远大于rdb,恢复速度慢于rdb
- aof运行效率慢于rdb
Redis的事务 MULTI EXEC DISCARD WATCH
一个事务的所有命令都会被序列化,按顺序地
Starting with version 2.2, Redis allows for an extra guarantee to the above two, in the form of optimistic locking in a way very similar to a check-and-set (CAS) operation. This is documented later on this page.
一个事务顺利执行会有三个阶段
- MULTI 开启一个事务
- 入列
- EXEC 执行
Calling DISCARD instead will flush the transaction queue and will exit the transaction.(比如在敲命令的时候,打错了,此时就可以用DISCARD放弃此次事务)
Redis事务的特性:
- 没有隔离级别的概念
- 但是周扬的脑图说,redis不保证原子性:看冤有头债有主
怎么玩儿
正常执行
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> get k2 // 任何命令都可以放在事务中
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> EXEC
1) OK // 依次返回各个语句的执行结果
2) OK
3) "v2"
4) OK
放弃事务 DISCARD
比如说我在写一个事务的时候,中间写错了一个数据,可以用DISCARD来放弃此次事务
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k1 11
QUEUED
127.0.0.1:6379> set k2 22
QUEUED
127.0.0.1:6379> set k3 33
QUEUED
127.0.0.1:6379> DISCARD
OK
127.0.0.1:6379> get k1 // 全部不提交
"v1"
全体连坐
127.0.0.1:6379> get k1
"11"
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> fsjkdl fsa
(error) ERR unknown command 'fsjkdl' // 在事务中,插入一个错误的命令
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> EXEC // 整个事务中的操作都不提交
(error) EXECABORT Transaction discarded because o
127.0.0.1:6379> get k1
"11"
冤有头债有主 对的执行 错的抛出
127.0.0.1:6379> set k1 aa
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> incr k1 // aa不能加一,但是被正确的加入到了队列中
QUEUED
127.0.0.1:6379> set k2 22
QUEUED
127.0.0.1:6379> set k3 33
QUEUED
127.0.0.1:6379> set k4 44
QUEUED
127.0.0.1:6379> get k4
QUEUED
127.0.0.1:6379> EXEC // 只有incr k1没有执行,剩下的都照常执行
1) (error) ERR value is not an integer or out of range
2) OK
3) OK
4) OK
5) "44"
所以redis是部分支持事务,正确的会执行,错误的不执行
watch监控 类似乐观锁
悲观锁:传统的关系型数据库就用到了这种锁机制,比如行锁,表锁、读锁,写锁等,都是操作之前先上锁
其中表锁:一致性最好,但是并发性最差
乐观锁:提交的版本必须要大于记录的当前版本才能执行
乐观锁:提交的版本必须要大于记录的当前版本才能执行
乐观锁:提交的版本必须要大于记录的当前版本才能执行
WATCH 监控一个或者多个key
UNWATCH 取消WATCH命令对所有key的监控
watch基本操作
初始化信用卡的可用余额和欠额
127.0.0.1:6379> set balance 100
OK
127.0.0.1:6379> set debt 0
OK
// 模拟使用信用卡花费20块钱的操作
// 在执行事务之前,先对balance进行WATCH
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
127.0.0.1:6379> get balance
"80"
加塞篡改,会发生的事情
// 开启两个客户端
127.0.0.1:6379> WATCH balance
OK // 在这期间,有另一个客户端更改了key
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) // nil
WATCH之后,然后执行EXEC后,这个WATCH之后还有效吗?
Redis的发布订阅 SUBSCRIBE PUBLISH
类似在微信上订阅公众号,然后订阅后就会收到公众号推送的文章
发布订阅是进程之间的一种通讯方式
Redis很有野心,在高速缓存这块已经把memcache干掉了,那能不能再做做MQ?
但是企业中很少有人把Redis当消息中间件
redis 127.0.0.1:6379> SUBSCRIBE redisChat
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "redisChat"
3) (integer) 1
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的复制 Master/Slave
现在最流行主从复制、读写分离
是什么
是主从复制:当主机数据更新后,根据配置的策略,将数据自动同步到备机
其中Master以写为主,Slave以读为主(所以这就是读写分离???)
能干嘛
- 容灾备份:当主机挂了,从机依旧能够工作
- 读写分离:不懂为啥要读写分离
怎么玩
只配从机,不能配主机
配置:
- 启动的时候配置:
- 配置进从机的redis.conf中:这样即使从机跟主机断开后,再
redis-cli -p 6379
redis-cli -p 6380
redis-cli -p 6381
使用info replication查看
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:0
master_repl_offset:0
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
一主二仆 最常用的配置!!!
让6379为主,6380和6381为仆
// 80和81执行
slaveof 127.0.0.1 6379
通过info replication 查看一下三台机器的状态
// 6379
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:0
master_replid:9e0f66e7759801c7be8cdd6134e0c734b963acb3
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
//
注意:从机会把主机上所有的数据都拿到手
- 如果在79、80、81上都新写一个数据
set k6 v6
:在主机上可写,但是从机是readonly,不能写(因为读写分离) - 当主机出故障了,把79 shutdown,两个从机还是从机(原地待命,不会有人抢成为主)
- 当此时主机又恢复了(老领导回来了),老领导仍旧是主的身份
- 当从机出故障了,比如80 shutdown,此时79和81都能够正常的运作。
- 当此时80从机又恢复了,此时80机器的状态是master(info replication查看),因为该从机目前没有在之前的主从体系中。此时需要重新执行
slaveof 127.0.0.1 6379
,就会重新回来
- 当此时80从机又恢复了,此时80机器的状态是master(info replication查看),因为该从机目前没有在之前的主从体系中。此时需要重新执行
薪火相传
主从的问题是,主是固定不变的,分担的任务非常重
薪火相传:去中心化,上一个slave是下一个slave的master,
将81配置成slaveof 127.0.0.1 6381
将80配置成slaveof 127.0.0.1 6380
即79是80的主,80是81的主
此时info replication
79肯定是master
80得到的是salve
81肯定是slave
反客为主
反客为主:当主挂了,从不是向一主二仆那样原地待命,而是直接上位成为主
slavef no one
,使当前数据库停止与其他数据库的同步,转而成为新的主
最开始的时候,79是主,80、81是从
然后主79挂掉了(shutdown),在80中执行SLAVEOF no one
,此时80成为新的主,此时80可以进行写操作。而此时81有两种选择:1跟着新老板80(执行slaveof 127.0.0.1 6380
),2默默等着79
然后79重启,此时该机器是master(info replication查看),因为该从机目前没有在之前的主从体系中
哨兵模式 sentinel 最重要 反客为主的自动版
凌晨2点服务挂了,自动有一个从机成为新的主,运维不用起床了。。。
监控主是否故障,如果故障了将根据投票自动将从库专程主库
创建一个sentinel.conf
的配置文件
# 最后一个1表示,剩余的从机,谁的票数多余一票,谁就是新的领导
sentinel monitor host6379 127.0.0.1 6379 1
redis有一个服务叫redis-sentinel
,用来监控79服务,一旦79挂掉了,就让80和81进行投票,然后选举出新的主机
redis-sentinel /myredis/sentinel.conf
当79服务挂掉了,等几秒钟后,哨兵会选出新的主,比如选出了80服务,则通过info replication
查看后,80是master,此时81是slave
此时79如果服务回来了,不是像一仆二主和反客为主那样,79成为一个毫不相干的master。而是启动后,经过哨兵的通知,79会成为一个slave(通过info replication
可以查看到)
一组sentinel能够同时监控多个master,应该是在sentinel.conf
中写多个语句即可
sentinel monitor host6379 127.0.0.1 6379 1
sentinel monitor hostXXXX 127.0.0.1 6379 1
复制原理
首次复制是全量复制,之后是增量复制
但只要是重新连接master,就要执行一次全量复制
复制的缺点
由于所有写操作都是在master上进行,然后再同步到slave上,从master同步到slave上一定有延迟。
Redis的Java客户端Jedis
需要两个jar包,其中jedis jar依赖于commons-pool2
事务提交
会报错:Exception in thread "main" redis.clients.jedis.exceptions.JedisDataException: EXECABORT Transaction discarded because of previous errors.
public class Test {
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1", 6379);
Transaction transaction = jedis.multi();
transaction.set("k4", "v4");
transaction.set("k5", "v5");
transaction.exec();
}
}
public class Test {
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1", 6379);
Transaction transaction = jedis.multi();
transaction.set("k4", "v4");
transaction.set("k5", "v5");
// transaction.exec();
transaction.discard();
}
}
模拟一个消费的操作
public class TestTX {
public boolean transMethod() throws InterruptedException {
Jedis jedis = new Jedis("127.0.0.1", 6379);
/**
* 注意:模拟该操作之前,先在redis中建立balance和debt
*/
int balance;// 可用余额
int debt;// 欠额
// 模拟刷卡消费了10元
int amtToSubtract = 10;
// 在执行事务之前,需要先watch
jedis.watch("balance");
// jedis.set("balance","5");//此句不该出现,讲课方便。模拟其他程序已经修改了该条目
Thread.sleep(7000);
balance = Integer.parseInt(jedis.get("balance"));
// 要消费的金额必须要小于账户的可用余额
if (balance < amtToSubtract) {
jedis.unwatch();
System.out.println("modify");
return false;
} else {
System.out.println("***********transaction");
Transaction transaction = jedis.multi();
transaction.decrBy("balance", amtToSubtract);
transaction.incrBy("debt", amtToSubtract);
transaction.exec();
balance = Integer.parseInt(jedis.get("balance"));
debt = Integer.parseInt(jedis.get("debt"));
System.out.println("*******" + balance);
System.out.println("*******" + debt);
return true;
}
}
/**
* 通俗点讲,watch命令就是标记一个键,如果标记了一个键,在提交事务前如果该键被别人修改过,那事务就会失败,这种情况通常可以在程序中
* 重新再尝试一次。
* <p>
* 首先标记了键balance,
* 然后检查余额是否足够,不足就取消标记,并不做扣减;
* 足够的话,就启动事务进行更新操作
* <p>
* 如果在此期间键balance被其它人修改, 那在提交事务(执行exec)时就会报错,
* 程序中通常可以捕获这类错误再重新执行一次,直到成功。
*
* @throws InterruptedException
*/
public static void main(String[] args) throws InterruptedException {
TestTX test = new TestTX();
boolean retValue = test.transMethod();
System.out.println("main retValue-------: " + retValue);
}
}
主从复制
启动6379和6380两个服务
主机写,从机读的例子
public class TestMS {
public static void main(String[] args) {
Jedis jedisM = new Jedis("127.0.0.1", 6379);
Jedis jedisS = new Jedis("127.0.0.1", 6380);
// 让80成为79的从机
// 注意:这个命令其实不应该在Java中写,而是在linux下通过命令去配置主从关系!!!!
jedisS.slaveof("127.0.0.1", 6379);
jedisM.set("class", "1122V2");
String result = jedisS.get("class");
System.out.println(result);
}
}
JedisPool 通过池来获取Jedis实例
从JedisPool中获取jedis实例
然后使用完毕再放回JedisPool中
如何设计一个池子:池子应该是唯一的,因此池子是单例的
public class JedisPoolUtil {
private static volatile JedisPool jedisPool = null;
private JedisPoolUtil() {}
// 获取一个Jedis池子
public static JedisPool getJedisPoolInstance() {
if (null == jedisPool) {
synchronized (JedisPoolUtil.class) {
if (null == jedisPool) {
JedisPoolConfig poolConfig = new JedisPoolConfig();
// 池子中能分配多少个Jedis实例
poolConfig.setMaxActive(1000);
// 池子中最多能够有多少个状态是idle的jedis实例
poolConfig.setMaxIdle(32);
// 当从池子中取一个jedis的时候,最大的等待时间,如果超时,抛JedisConnectionException
poolConfig.setMaxWait(100 * 1000);
// 获得一个jedis实例的时候,是否检查连接可用(ping命令)
poolConfig.setTestOnBorrow(true);
jedisPool = new JedisPool(poolConfig, "127.0.0.1", 6379);
}
}
}
return jedisPool;
}
//
public static void release(JedisPool jedisPool, Jedis jedis) {
if (null != jedis) {
jedisPool.returnResourceObject(jedis);
}
}
}
public class TestPool {
public static void main(String[] args) {
JedisPool jedisPool = JedisPoolUtil.getJedisPoolInstance();
JedisPool jedisPool2 = JedisPoolUtil.getJedisPoolInstance();
System.out.println(jedisPool == jedisPool2);
Jedis jedis = null;
try {
// 通过getResource()来获取一个jedis
jedis = jedisPool.getResource();
jedis.set("aa", "bb");
} catch (Exception e) {
e.printStackTrace();
} finally {
JedisPoolUtil.release(jedisPool, jedis);
}
}
}
j
网友评论