cache:位于项目中Dao层与数据库之间,主要用于数据访问量大时使用缓存技术来缓解数据库的压力
一些频繁需要访问的数据放在关系型数据库中,每次查询开销很大,而放在cache中可以高效地被访问
Redis是运行在内存上的NoSQL(Not Only SQL)数据库,也是一种cache
NoSQL与传统数据库相比无需为要存储的数据建立字段,没有表的概念所以可以随时存储自定义的数据格式
其他常见的NoSQL数据库:Memcache(与Redis作用类似,也是运行在内存上)、MongoDB(运行在硬盘上)
分布式数据库具备C(强一致性)、A(高可用性)、P(分区容错性)
Consistency:所有节点在同一时间的数据完全一致
Availability:服务一直可用
Partition tolerance:某节点或网络分区故障时,仍然能够对外提供满足一致性或可用性的服务
对于分布式数据库来说分区不可避免(CA表示非分布式但同时满足一致性和可用性,这种扩展性不强),
分区容错性的分布式数据库有两种情况:CP和AP(一致性和可用性一般不可同时满足)
CP:满足一致性,可用性不强
AP:满足可用性,一致性不强
加载配置文件(配置后台运行)的启动方式
/usr/local/bin/redis-server /opt/redis-5.0.4/redis.conf
查看运行状态:检测6379端口是否在监听
netstat -lntp | grep 6379
查看运行状态:查看后台进程是否存在
ps -ef|grep redis
单实例关闭(/usr/local/bin/
下操作)
redis-cli shutdown
多实例关闭(/usr/local/bin/
下操作)
redis-cli -p 6379 shutdown
进入客户端(/usr/local/bin/
下操作)
redis-cli
进入客户端后关闭Redis
shutdown
测试连接(正常返回PONG)
ping
切换数据库(共16个库,索引范围0-15,默认是0不显示库号)
select xxx
查看当前库的所有键数
dbsize
查看当前库的键(支持模糊查询:*
一次占用多位,?
一次占用一位,[xxx]
指定与xxx中的每个字符都匹配一次并且占用一位)
查看所有的键
keys *
keys xxx*
keys *xxx
查看包含xxx的键
keys *xxx*
清空当前库
flushdb
清空所有(16)库
flushall
判断键是否存在
exists xxx
移动指定的键到指定的库中
move 键名 库索引号
查看过期时间(-1永不过期,-2已过期)
ttl 键名
设置过期时间(过期后相当于从当前库中自动移除了指定的键,执行ttl
返回-2)
expire 键名 时间(s)
查看指定键的数据类型
type 键名
特点:不存在重复的键,键对应的值是一个字符串,可向其中追加其他字符串
相关命令
set
:添加指定的键和值,示例set 键名 值
添加已存在的键时,会替换原来的键值
get
:查询指定的键对应的值,示例get 键名
del
:删除指定的键,示例del 键名
append
:向指定键对应的值追加字符串,示例append 键名 xxx
strlen
:返回指定键对应值的长度,示例strlen 键名
incr
:键值自动增长1(值必须为数字),示例incr 键名
decr
:键值自动减少1(值必须为数字),示例decr 键名
incrby
:键值按指定值增加(值必须为数字),示例incrby 键名 值
decrby
:键值按指定值减少(值必须为数字),示例decrby 键名 值
getrange
:查询指定键的指定索引范围的值,返回字符串,示例getrange 键名 索引开始 索引结束
索引范围是0到-1时,查询全部的键值
setrange
:替换指定键的指定索引范围的值(从指定索引开始替换,替换长度为指定字符串长度),
示例setrange 键名 索引 xxx
setex
:添加键时同时设定过期时间,示例setex 键名 时间(s) 值
setnx
:添加键时判断该键是否已经存在(只添加不存在的键,防止替换已存在的键值),
示例setnx 键名 值
添加已存在的键时,会返回0,表示添加失败
mset
:一次添加多组键值,示例mset 键名1 值1 键名2 值2 键名3 值3...
mget
:一次查询指定的多个键对应的值,示例mget 键名1 键名2 键名3...
msetnx
:一次添加多组键值,若有已存在的键则返回0,示例msetnx 键名1 值1 键名2 值2 键名3 值3...
getset
:先查询当前的键值(返回当前键值),再设置值为指定值,示例getset 键名 值
特点:不存在重复的键,键对应的值是元素可重复集合
相关命令
lpush
:从左开始压栈,示例:lpush list01 1 2 3 4 5
rpush
:从右开始压栈,示例:rpush list02 1 2 3 4 5
lrange
:从栈顶开始向下读取List,示例:lrange list02 0 -1
0表示开始,-1表示结尾
lpop
:从栈顶移除一个元素,示例lpop list02
rpop
:从栈底移除一个元素,示例rpop list02
lindex
:从栈顶开始读取指定索引的元素,示例lindex list01 2
索引从0开始
llen
:返回集合的总元素数,示例llen list01
lrem
:从集合中移除指定个数的指定元素,示例lrem list01 个数 元素
ltrim
:截取集合中指定索引的元素其余元素被移除,示例ltrim list01 起始索引 结束索引
rpoplpush
:将集合1从栈底移除一个元素后集合2从栈顶压入这个元素,示例rpoplpush list01 list02
lset
:修改指定索引的元素,示例lset list01 索引 元素
linsert
:在指定元素的前或后插入元素,示例 linsert list01 before/after 集合中的元素 插入元素
从左边进入
List类似链表,头尾操作效率高,中间操作效率低
特点:不存在重复的键,键对应的值是元素不重复的集合
相关命令
sadd
:添加不重复的集合元素,示例sadd set01 1 2 2 3 3 3
sadd自动去重后添加
smembers
:查看集合中的元素,示例smembers set01
sismemeber
:查看指定的元素是否为集合中的元素,示例sismember set01 元素
scard
:获取集合中的元素个数,示例scard set01
srem
:移除指定元素,示例srem set01 元素
srandmember
:获取指定个数的随机集合元素,示例srandmember set01 元素个数
spop
:随机移除集合中的一个元素,示例spop set01
smove
:将元素从集合1移动到集合2中,示例smove set01 set02 元素
sinter
:返回两个集合中元素的交集,示例sinter set01 set02
sunion
:返回两个集合中元素的并集,示例sunion set01 set02
sdiff
:返回两个集合中元素的差集,示例sdiff set01 set02
sdiff set01 set02:返回在set01中存在,在set02中不存在的元素
特点:不存在重复的键,键对应的值是一个/多个键值对集合,键值对的键是不重复的,键值对的值可重复
相关命令
hset
:为一个Hash key添加一个指定的key和value,示例hset hash01 key value
hget
:返回Hash key中指定key的value,示例hget hash01 key
hmget
:返回Hash key中指定多个key的多个value,示例hmget hash01 key1 key2...
重复添加已存在的键的键值对,会将其值进行替换
hmset
:为一个Hash key添加多个指定的key和value,示例hmset hash02 key1 value1 key2 value2 key3 value3...
hgetall
:返回Hash key的所有key和value,示例hgetall hash02
hdel
:删除Hash key中指定key和对应的value,示例hdel hash02 key
hlen
:返回指定Hash key的属性个数,示例hlen hash02
hexists
:返回指定Hash key中是否存在某个key,示例hexists hash02 key
hkeys
:返回指定Hash key中的所有key,示例hkeys hash02
hvals
:返回指定Hash key中的所有value,示例hvals hash02
hincrby
:增加指定的Hash key中某个key的value,示例hincrby hash02 key 增加值
hincrbyfloat
:增加指定的Hash key中某个key的value(小数),示例hincrbyfloat hash02 key 增加值
hsetnx
:为一个Hash key添加一个指定的key和value若key已存在则添加失败,示例hsetnx hash02 key value
特点:不存在重复的键,键对应值是一个/多个键值对集合(会根据键的数字自动排序,键值对中可以存在重复的键,值是不重复的)
键值对的键一般也是内容为数字的字符串
相关命令
zadd
:为指定的键添加一个/多个值,示例zadd 键名 数字1 值1 数字2 值2...
zrange
:查询指定键对应的所有键值对的值,示例zset 键名 0 -1 (withscores)
添加withscores后同时会返回键值对的键(数字)
zrangebyscore
:模糊查询指定键对应的键值对的值(按键值对的键查询),示例zrangebyscore 键名 数字1 数字2 (limit x y)
默认的在数字1(min)和2(max)上是闭区间,数字前添加
(
表示不包含指定的数字
添加
limit x y
,表示跳过查询结果中的前x个并取y个
zrem
:移除指定键对应的键值对(按值删),示例zrem 键名 xxx
zcard
:查询指定键对应的键值对的个数,示例zcard 键名
zcount
:查询指定键对应的某个范围下(键值对中的键)键值对的个数,示例zcount 键名 数字1 数字2
zrank
:查询指定键对应的某个键值对索引(按键值对中的值进行查询),示例zrank 键名 值
键值对索引从0开始,索引不等于键值对的键(数字)
zscore
:查询指定键对应的某个键值对的键(按键值对中的值进行查询),示例zscore 键名 值
zrevrank
:同zrank,但返回结果是以逆序查找,示例zrevrank 键名 值
zrevrange
:同zrange,但返回结果是以逆序查找,示例zrevrange 键名 0 -1 (withscores)
zrevrangebyscore
:同zrangebyscore,但返回结果是以逆序查找,示例zrevrangebyscore 键名 数字1 数字2 (limit x y)
默认的在数字1(max)和2(min)上是闭区间,数字前添加
(
表示不包含指定的数字
自动备份
在指定的时间间隔内,将内存中数据的快照写入硬盘中(默认保存在bin目录下的dump.rdb中)
可以修改redis.config来改变默认配置,设置自动备份的频率或者直接关闭持久化
在Redis客户端中执行shutdown后会自动备份
手动备份
将Redis执行过程中的写指令以追加的方式全部记录在appendonly.aof中
Redis在启动之初会读取该文件,从头到尾执行一遍实现数据恢复
flushdb后再重启Redis,不影响aof文件中的内容
aof文件不建议手动修改,Redis在启动时若读取aof错误将导致Redis无法正常启动
aof错误解决方案:手动执行命令恢复aof
RDB和AOF共存时,Redis优先载入aof文件
Redis持久化机制中RDB和AOF各有利弊,开发中采用主从复制来实现持久化
事务控制方式(在Redis客户端中操作)
开启事务(multi),加入队列(如添加键后自动加入,返回QUEUED),一起执行(exec),一起加入队列将执行操作成功
开启事务(multi),执行后放弃(discard)之前的操作,所有的键和对应的值都恢复到原来
开启事务(multi),加入队列报错(如输入不存在的指令),一起执行(exec),提示EXECABORT,所有的键和对应的值都恢复到原来
开启事务(multi),加入队列成功但实际操作报错(如incr一个不是数字内容的字符串),一起执行(exec),除了实际操作报错,其他加入队列将执行操作成功
该操作类似编译成功,运行时报错
开启事务前对键加入监控watch 键名
,开启事务(multi),加入队列(操作监控的键值),同时开启另一个线程操作监控的键值,这时在原来的线程中执行(exec),返回(nil)
表示本次事务被打断导致失败
发布/订阅模式是Redis的一种使用场景,即实现简单消息队列
进程间的一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。例如:微信订阅号
实现方式:在Redis客户端订阅subscribe 频道名1 频道名2 ...
,在另一个线程的Redis客户端中发布publish 频道名 消息内容
,
此时在订阅端会收到发布的消息
配从不配主:在从机端配置该机器的主机,主机端默认成为该从机的主机
集群测试环境:配置三台装了Redis的服务器
slaveof 主机IP地址 6379
查看主从复制信息
info replication
读写分离:从机只负责读取数据,无权写入数据;在主机写入数据时,所有从机将会同步数据
从机将会同步主机成为主机之前和之后的所有数据
一个主机理论上可以多个从机,但这样主机负担很大,可以使用继承传递性来解决,减轻主机的负担
主机1->主机2(主机1的从机)->主机3(主机2的从机)...
这种情况下还是保持读写分离:只有主机1负责写入数据,后面的主机只有权读取
谋权篡位:1个主机,2个从机,当1个主机挂掉了,其余从机主机仍然是slave并显示它们的master已离线
可以手动将一台slave设定为主机
slaveof no one
另一台根据需要设定为新主机的从机
slaveof 主机IP地址 6379
经过以上两步,原主机上线后会成为新的master并无其他的slave
复制的主要流程
相关概念
全量复制:slave初始化阶段,这时slave需要将master上的所有数据都复制一份slave接收到数据文件后,存盘,并加载到内存中
涉及复制的主要流程中的1,2,3
只要是slave重新连接master,一次性(全量复制)同步将自动执行
增量复制:slave初始化阶段后,开始正常工作时主服务器发生的写操作同步到从服务器的过程
涉及复制的主要流程中的4
Redis主从同步策略:主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步
如果有需要,slave在任何时候都可以发起全量同步;
Redis策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步
哨兵模式(Sentinel):
由一个或多个Sentinel实例组成的Sentinel系统可以监视任意多个主服务器,以及所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求
Sentinel是Redis的高可用性解决方案,可以看作是自动版的谋权篡位
设定一个哨兵监控master当master关闭后,自动从slave中分配新的master,维持了主从复制的读写分离特性,保证了系统的高可用性
实现方式
在/usr/local/bin
下创建sentinel.conf
,同时添加以下配置(每台服务器上的Redis分别给自身投一票)
集群测试环境:预先设定好128为master,129和130为slave
sentinel monitor 被监控主机名(自定义) ip port 票数
重启三台服务器的Redis,并复制128,129,130窗口,在新的复制中执行以下命令启动Sentinel
redis-sentinel sentinel.conf
测试将原来的master128服务器Redis下线,会自动发起投票,在slave129和130中选出新的master
Redis Sentinal的缺点
数据同步较慢
由于所有的写操作都是在master上完成的,然后再同步到slave上,所以两台机器之间通信会有延迟;
当系统很繁忙的时候,延迟问题会加重;
slave机器数量增加,问题也会加重
解决方式:
关闭服务器
redis-cli shutdown
关闭防火墙
systemctl stop firewalld.service
重启服务器
redis-server /opt/redis-5.0.4/redis.conf
String
@Test public void testString() { Jedis jedis = new Jedis("192.168.197.128", 6379); // 添加指定的键和值 jedis.set("k1", "v1"); jedis.set("k2", "v2"); jedis.set("k3", "v3"); jedis.set("k33", "v3"); // 查看所有的键 Set<String> keys = jedis.keys("*"); // 遍历 Set 集合 ,每次查询指定的键对应的值 for (String key : keys) { System.out.println(key + "->" + jedis.get(key)); } // 查看指定键是否存在 Boolean k2 = jedis.exists("k2"); System.out.println("k2 exists = " + k2); // 查看指定键的过期时间 System.out.println(jedis.ttl("k1")); // 清空当前数据库 jedis.flushDB(); // 一次添加多组键值 jedis.mset("k4", "v4", "k5", "v5"); // 一次查询指定的多个键对应的值 List<String> mGet = jedis.mget("k4", "k5"); System.out.println(mGet); } /* k3->v3 k4->v4 k5->v5 k33->v3 k1->v1 k2->v2 k2 exists = true -1 [v4, v5] */
List
@Test public void testList() { Jedis jedis = new Jedis("192.168.197.128", 6379); // 清空当前数据库 jedis.flushDB(); // 从左开始压栈 jedis.lpush("list01", "l1", "l2", "l3", "l4", "l5"); // 从栈顶开始向下读取 List 集合 List<String> list01 = jedis.lrange("list01", 0, -1); System.out.println(list01); } /* [l5, l4, l3, l2, l1] */
Set
@Test public void testSet() { Jedis jedis = new Jedis("192.168.197.128", 6379); // 清空当前数据库 jedis.flushDB(); // 添加不重复的键值 jedis.sadd("order", "o1"); jedis.sadd("order", "o2"); jedis.sadd("order", "o3"); // 查看集合中的元素 Set<String> order = jedis.smembers("order"); System.out.println(order); // 移除指定的元素后查看当前键值的元素个数 jedis.srem("order", "o2"); System.out.println(jedis.smembers("order").size()); } /* [o3, o2, o1] 2 */
Hash
@Test public void testHash() { Jedis jedis = new Jedis("192.168.197.128", 6379); // 清空当前数据库 jedis.flushDB(); // 为一个 Hash key 添加一个指定的 key 和 value jedis.hset("hash01", "username", "Jeff"); // 返回 Hash key 中指定 key 的 value String hGet = jedis.hget("hash01", "username"); System.out.println(hGet); // 准备一个 map HashMap<String, String> map = new HashMap<>(); map.put("gender", "male"); map.put("address", "Beijing"); map.put("phoneNumber", "123456"); // 通过传入 map 为一个 Hash key 添加多个指定的 key 和 value jedis.hmset("hash01", map); // 返回 Hash key 的所有 key 和 value Map<String, String> hash01 = jedis.hgetAll("hash01"); System.out.println(hash01); } /* Jeff {gender=male, username=Jeff, address=Beijing, phoneNumber=123456} */
Zset
@Test public void testZset() { Jedis jedis = new Jedis("192.168.197.128", 6379); // 清空当前数据库 jedis.flushDB(); // 为指定的键添加一个/多个值 jedis.zadd("zset01", 60d, "zs1"); jedis.zadd("zset01", 60d, "zs2"); jedis.zadd("zset01", 60d, "zs3"); jedis.zadd("zset01", 70d, "zs4"); jedis.zadd("zset01", 80d, "zs5"); jedis.zadd("zset01", 90d, "zs6"); jedis.zadd("zset01", 50d, "zs7"); // 查询指定键对应的所有键值对的值 Set<String> zSet01 = jedis.zrange("zset01", 0, -1); System.out.println(zSet01); } /* [zs7, zs1, zs2, zs3, zs4, zs5, zs6] */
事务控制前,建议先将指定的键监控起来
public class JedisTransaction { @Test public void resetBalanceAndExpense() { Jedis jedis = new Jedis("192.168.197.128", 6379); // 清空当前数据库 jedis.flushDB(); // 设定余额和支出的初始值 jedis.set("balance", "100"); jedis.set("expense", "0"); } @Test public void testTransaction() { Jedis jedis = new Jedis("192.168.197.128", 6379); // 获取当前的余额 int balance = Integer.parseInt(jedis.get("balance")); // 指定每次消费金额 int expense = 10; // 将余额监控起来 jedis.watch("balance"); if (balance < expense) { // 解除监控 jedis.unwatch(); System.out.println("balance is not enough"); } else { // 开启事务 Transaction transaction = jedis.multi(); // 余额减少 transaction.decrBy("balance", expense); // 累计消费增加 transaction.incrBy("expense", expense); transaction.exec(); System.out.println("balance:" + jedis.get("balance")); System.out.println("expense:" + jedis.get("expense")); } } }
单例模式优化Jedis连接池
双层检查锁定(Double check locked)
这个例子在单线程环境可以正常运行,但是在多线程环境就有可能会抛出空指针异常。为了防止这种情况,我们需要在该方法上使用 synchronized。这样该方法在多线程环境就是安全的,但是这么做就会导致每次方法调用都需要获取与释放锁,开销很大
private JedisPool jedisPool; public JedisPool getJedisPool() { if (jedisPool == null) { jedisPool = new JedisPool(); } return jedisPool; }
分析可以得知只有在初始化的变量需要真正加锁,一旦初始化之后,直接返回对象即可。
所以可以将该方法改造以下的样子
这个方法首先判断变量是否被初始化,没有被初始化,再去获取锁。获取锁之后,再次判断变量是否被初始化。第二次判断目的在于有可能其他线程获取过锁,已经初始化该变量。第二次检查还未通过,才会真正初始化变量。
这个方法检查判定两次,并使用锁,所以称为双重检查锁定模式
private JedisPool jedisPool; public JedisPool getJedisPool() { if (jedisPool == null) { synchronized (this) { if (jedisPool == null) { jedisPool = new JedisPool(); } } } return jedisPool; }
双层检查锁定的问题
指令重排在单线程下可用,在多线程下会发生异常
创建一个对象(
JedisPool jedisPool = new JedisPool()
)的过程:
分配对象内存
调用构造器方法,执行初始化
将对象引用赋值给变量
JVM运行时,指令1会先执行,指令2,3会在不影响整体结果的情况下,进行重新排序来提高程序的执行性能
根据上一个双层检查锁定的示例代码,假设有两个线程,线程1先执行了指令1和3完成后此时线程2判断到对象不为null就会访问该对象但是该对象还未被初始化,触发异常
使用 volatile可以禁止指令的重排序,保证多线程环境内的系统安全
volatile关键字的作用:
- 保证可见性:使用 volatile定义的变量,将会保证对所有线程的可见性。
- 禁止指令重排序优化:由于 volatile禁止对象创建时指令之间重排序,所以其他线程不会访问到一个未初始化的对象,从而保证安全性。
单例模式优化Jedis连接池的工具类
定义工具类
public class JedisPoolUtil { private JedisPoolUtil() { } private volatile static JedisPool jedisPool = null; private volatile static Jedis jedis = null; // 返回一个连接池对象 private static JedisPool getInstance() { // 双层检查锁定 if (jedisPool == null) { synchronized (JedisPoolUtil.class) { if (jedisPool == null) { // 设定连接池参数 JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); jedisPoolConfig.setMaxTotal(1000); jedisPoolConfig.setMaxIdle(30); jedisPoolConfig.setMaxWaitMillis(60*1000); jedisPoolConfig.setTestOnBorrow(true); // 创建连接池对象 jedisPool = new JedisPool(jedisPoolConfig, "192.168.197.128", 6379); } } } return jedisPool; } // 返回 Jedis 对象 public static Jedis getJedis() { if (jedis == null) { jedis = getInstance().getResource(); } return jedis; } }
测试工具类
public class TestJedisPool { @Test public void testGetJedis() { Jedis jedis01 = JedisPoolUtil.getJedis(); Jedis jedis02 = JedisPoolUtil.getJedis(); System.out.println(jedis01 == jedis02); } } /* true */
使用Synchronized锁解决
使用Jmeter进行并发请求测试,1秒100个线程发送请求
该方案仅适用于单进程(一个Tomcat)的并发问题,如果是分布式环境,多个进程并发,这种方案就会失效
@Controller public class TestKill { @Autowired private StringRedisTemplate stringRedisTemplate; @RequestMapping("kill") @ResponseBody public synchronized String kill () { // 从 redis 中获取库存 int phoneCount = Integer.parseInt(stringRedisTemplate.opsForValue().get("phone")); // 判断数量是否够秒杀 if (phoneCount > 0) { phoneCount--; // 库存减少后,再将库存的值保存回 redis stringRedisTemplate.opsForValue().set("phone", phoneCount+""); System.out.println("库存-1,剩余:" + phoneCount); } else { System.out.println("库存不足"); } return "over!"; } }
分布式锁实现思路:因为Redis是单线程的,所以命令也就具备原子性,使用setnx命令实现锁,保存k-v
Redisson是用于在Java程序中操作Redis的库,在java.util中常用接口的基础上,提供了一系列具有分布式特性的工具类
使用Redis分布式锁解决多进程多线程并发(Redisson)
运行环境:使用nginx作为代理服务器接收请求转发给两个Tomcat,每个Tomcat部署以下程序,使用JMeter发送多线程请求
@Controller public class TestKill { @Autowired private Redisson redisson; @Autowired private StringRedisTemplate stringRedisTemplate; @RequestMapping("kill") @ResponseBody public synchronized String kill () { // 定义商品 id String productKey = "phone01"; // 通过 redisson 获取锁 // 底层源码集成了 setnx ,过期时间等操作 RLock lock = redisson.getLock(productKey); // 上锁 lock.lock(30, TimeUnit.SECONDS); try { // 从 redis 中获取库存 int phoneCount = Integer.parseInt(stringRedisTemplate.opsForValue().get("phone")); // 判断数量是否够秒杀 if (phoneCount > 0) { phoneCount--; // 库存减少后,再将库存的值保存回 redis stringRedisTemplate.opsForValue().set("phone", phoneCount+""); System.out.println("库存-1,剩余:" + phoneCount); } else { System.out.println("库存不足"); } } catch (NumberFormatException e) { e.printStackTrace(); } finally { // 释放锁 lock.unlock(); } return "over!"; } @Bean public Redisson redisson() { Config config = new Config(); // 使用单个redis服务器 config.useSingleServer().setAddress("redis://192.168.197.128:6379").setDatabase(0); // 使用集群redis // config.useClusterServers().setScanInterval(2000).addNodeAddress("", "", "") return (Redisson)Redisson.create(config); } }