Redis与Java - 实践
标签 : Java与NoSQL
Transaction
Redis事务(
transaction
)是一组命令的集合,同命令一样也是Redis的最小执行单位, Redis保证一个事务内的命令执行不被其他命令影响.
`MULTI`
SADD user:1:following 2
SADD user:2:follower 1
`EXEC`
vs. RDBMS
事务操作 | MySQL | Redis |
---|---|---|
开启 | start transaction |
MULTI |
语句 | DML | 普通命令 |
取消 | rollback |
DISCARD |
执行 | commit |
EXEC |
- MySQL的
rollback
与Redis的DISCARD
有一定的区别.
假设现在已经成功执行了事务内的前2条语句, 第3条语句出错:
- MySQL
rollback
后,前2条的语句影响消失. - Redis可以分为两种情况:
- 语法错误: 事务中断, 所有语句均得不到执行;
- 运行错误: (如语法正确,但适用数据类型不对: 像
ZADD
操作List
),EXEC
会执行前2条语句, 并跳过第3条语句.
这样的部分成功会导致数据不一致, 而这一点需要由开发人员负责, 比如提前规划好缓存key
的设计.
- MySQL
乐观锁与WATCH
悲观锁(Pessimistic Lock): 很悲观,每次读写数据都认为别人会修改,所以每次读数据都会上锁,这样如果别人也想读写这条数据就会阻塞, 直到加锁的人把锁释放. 传统的RDBMS中用到了很多这种锁机制, 如行锁、表锁、读锁、写锁等.
乐观锁(Optimistic Lock): 顾名思义非常乐观, 每次读写数据时候都认为别人不会修改,所以不再上锁,但在更新数据时会判断一下在此期间有没有人更新了这条数据, 这个判断过程可以使用版本号
等机制实现, 而Redis默认就对乐观锁提供了支持 –WATCH
命令.
WATCH
命令可以监控一个/多个key
, 一旦其中有一个被修改/删除, 则之后的事务就不会执行,如用WATCH
命令来模拟抢票场景:
SET ticket 1 # 现在假设只有一张票了
`WATCH` ticket # 监控票数变化
`MULTI`
DECRBY username 400
DECR ticket
[DECR ticket] # 现在假设有另外一个用户直接把这张票买走了
`EXEC`
-> `(nil)` # 则这条事务执行就不会成功
小结
WATCH
命令的作用只是当被监控的key
值修改后阻止事务执行,并不能阻止其他Client修改. 所以一旦EXEC
执行失败, 可以重新执行整个方法或使用UNWATCH
命令取消监控.乐观锁适用于读多写少情景,即冲突真的很少发生,这样可以省去大量锁的开销. 但如果经常产生冲突,上层应用需要不断的retry,反倒是降低了性能,所以这种情况悲观锁比较适用.
Expire & Cache
Redis可以使用
EXPIRE
命令设置key
的过期时间, 到期后Redis会自动删除它.
命令 | 作用 |
---|---|
EXPIRE key seconds |
Set a timeout on key. |
TTL key |
Get the time to live for a key |
PERSIST key |
Remove the expiration for a key |
除了
PERSIST
命令之外,SET
/GETSET
为key
赋值的同时也会清除key
的过期时间.另外如果WATCH
监控了一个拥有过期时间的key
,key
到期自动删除并不会被WATCH
认为该key
被修改.
- 缓存DB数据
为了提高网站负载能力,常需要将一些访问频率较高但对CPU/IO消耗较大的操作结果缓存起来,并希望让这些缓存过期自动删除, 下面我们就使用Redis缓存DB数据, 场景介绍可以参考:Memcached - In Action:缓存DB查询数据.
/**
* @author jifang.
* @since 2016/6/13 20:08.
*/
public class RedisDAO {
private static final int _1M = 60 * 1000;
private static final DataSource dataSource;
private static final Jedis redis;
static {
Properties properties = new Properties();
try {
properties.load(ClassLoader.getSystemResourceAsStream("db.properties"));
} catch (IOException ignored) {
}
/** 初始化连接池 **/
HikariConfig config = new HikariConfig();
config.setDriverClassName(properties.getProperty("mysql.driver.class"));
config.setJdbcUrl(properties.getProperty("mysql.url"));
config.setUsername(properties.getProperty("mysql.user"));
config.setPassword(properties.getProperty("mysql.password"));
dataSource = new HikariDataSource(config);
/** 初始化Redis **/
redis = new Jedis(properties.getProperty("redis.host"), Integer.valueOf(properties.getProperty("redis.port")));
}
public List<Map<String, Object>> executeQuery(String sql) {
List<Map<String, Object>> result;
try {
/** 首先请求Redis **/
String key = sql.replace(' ', '-');
String string = redis.get(key);
// 如果key未命中, 再请求DB
if (string == null || string.trim().isEmpty()) {
ResultSet resultSet = dataSource.getConnection().createStatement().executeQuery(sql);
/** 获得列数/列名 **/
ResultSetMetaData meta = resultSet.getMetaData();
int columnCount = meta.getColumnCount();
List<String> columnName = new ArrayList<>();
for (int i = 1; i <= columnCount; ++i) {
columnName.add(meta.getColumnName(i));
}
/** 填充实体 **/
result = new ArrayList<>();
while (resultSet.next()) {
Map<String, Object> entity = new HashMap<>(columnCount);
for (String name : columnName) {
entity.put(name, resultSet.getObject(name));
}
result.add(entity);
}
/**写入Redis**/
String value = JSON.toJSONString(result);
redis.set(key, value, "NX", "PX", _1M);
} else {
result = JSON.parseObject(string, List.class);
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
return result;
}
public static void main(String[] args) {
RedisDAO dao = new RedisDAO();
List<Map<String, Object>> execute = dao.executeQuery("select * from user");
System.out.println(execute);
}
}
当服务器内存有限时,如果大量使用缓存而且过期时间较长会导致Redis占满内存; 另一方面为了防止占用内存过大而设置过期时间过短, 则有可能导致缓存命中率过低而使系统整体性能下降.因此为缓存设计一个合理的过期时间是很纠结的, 在Redis中可以限制能够使用的最大内存,并让Redis按照一定规则的淘汰不再需要的key
: 修改maxmemory
参数,当超过限制会依据maxmemory-policy
参数指定的策略来删除不需要的key
:
maxmemory-policy |
规则说明 |
---|---|
volatile-lru |
只对设置了过期时间的key使用LRU算法删除 |
allkey-lru |
使用LRU删除一个key |
volatile-random |
只对设置了过期时间的key随机删除一个key |
allkey-random |
随机删除一个key |
volatile-ttl |
删除过期时间最近的一个key |
noevication |
不删除key, 只返回错误(默认) |
Sort
Redis的
SORT
命令可以对List
、Set
、Sorted-Set
类型排序, 并且可以完成与RDBMS 连接查询 类似的任务:
SORT key [BY pattern]
[LIMIT offset count]
[GET pattern [GET pattern ...]]
[ASC|DESC]
[ALPHA]
[STORE destination]
参数 | 描述 |
---|---|
ALPHA |
SORT 默认会将所有元素转换成双精度浮点数比较,无法转换则会提示错误,而使用ALPHA 参数可实现按字典序比较. |
DESC |
降序排序(SORT 默认升序排序). |
LIMIT |
指定返回结果范围. |
STORE |
SORT 默认直接返回排序结果, STORE 可将排序后结果保存为List . |
注:
SORT
在对Sorted-Set
排序时会忽略元素分数,只针对元素自身值排序.
BY
很多情况下key
实际存储的是对象ID, 有时单纯对ID自身排序意义不大,这就用到了BY
参数, 对ID关联的对象的某个属性进行排序:
[BY pattern]
pattern
可以是字符串类型key
或Hash
类型key
的某个字段(表示为键名 -> 字段名).如果提供了BY
参数, SORT
将使用ID值替换参考key
中的第一个*
并获取其值,然后根据该值对元素排序.
SORT mi.blog:1:my BY mi.blog:*:data->time DESC
- 注意:
- 当
pattern
不包含*
时,SORT
将不会执行排序操作; - 当ID元素的参考
key
不存在时,默认设置为0; - 如果几个ID元素的
pattern
值相同,则会再比较元素本身值排序.
- 当
GET
GET
参数不影响排序过程,它的作用是使SORT
返回结果不再是元素自身的值,而是GET
参数指定的键值:
[GET pattern [GET pattern ...]]
同BY
一样, GET
参数也支持String
类型和Hash
类型, 并使用*
作为占位符.
SORT mi.blog:1:my BY mi.blog:*:data->time GET mi.blog:*:data->content GET mi.blog:*:data->time
注:
GET
参数获取自身值需要使用#
:GET #
性能
SORT
的时间复杂度为O(N+M*log(M))
:
where N is the number of elements in the list or set to sort, and M the number of returned elements. When the elements are not sorted, complexity is currently O(N) as there is a copy step that will be avoided in next releases.
- 所以开发过程中使用
SORT
需要注意:
- 尽可能减小待排序
key
中元素数量(减小N
); - 使用
LIMIT
参数限制结果集大小(减小M
); - 如果待排序数据量较大,尽可能使用
STORE
将结果缓存.
- 尽可能减小待排序
Message
1. 消息队列
消息队列就是”传递消息的队列”,与消息队列进行交互的实体有两类, 一是生产者: 将需要处理的消息放入队列; 一是消费者: 不断从消息队列中读出消息并处理.
- 使用消息队列有如下好处:
- 松耦合: 生产者和消费者无需知道彼此的实现细节, 只需按照协商好的 消息格式读/写, 即可实现不同进程间通信,这就使得生产者和消费者可以由 不同的团队使用 不同的开发语言编写.
- 易扩展: 消费者可以有多个,且可以分布在不同的Server中, 降低单台Server负载, 横向扩展业务.
Redis提供了BRPOP
/BLPOP
命令来实现消息队列:
命令 | 描述 |
---|---|
BRPOP key [key ...] timeout |
Remove and get the last element in a list, or block until one is available |
BLPOP key [key ...] timeout |
Remove and get the first element in a list, or block until one is available |
BRPOPLPUSH source destination timeout |
Pop a value from a list, push it to another list and return it; or block until one is available |
注: 若Redis同时监听多个
key
, 且每个key
均有元素可取,则Redis按照从左到右的顺序去挨个读取key
的第一个元素.
2. 消息订阅
前面的BRPOP
/BLPOP
实现的消息队列有一个限制: 如果一个队列被多个消费者监听, 生产者发布一条消息只会被其中一个消费者获取. 因此Redis还提供了一组命令实现“发布/订阅”模式, 同样可用于进程间通信:
“发布/订阅”模式也包含两种角色: 发布者与订阅者. 订阅者可以订阅一个/多个频道, 而发布者可向指定频道发送消息, 所有订阅此频道的订阅者都会收到此消息.
命令 | 描述 |
---|---|
PUBLISH channel message |
Post a message to a channel |
SUBSCRIBE channel [channel ...] |
Listen for messages published to the given channels |
UNSUBSCRIBE [channel [channel ...]] |
Stop listening for messages posted to the given channels |
PSUBSCRIBE pattern [pattern ...] |
Listen for messages published to channels matching the given patterns |
PUNSUBSCRIBE [pattern [pattern ...]] |
Stop listening for messages posted to channels matching the given patterns |
- MessagesQueue
/**
* @author jifang
* @since 16/7/11 下午2:36.
*/
public class MessageQueue<T> {
private Jedis redis;
private String chanel;
public MessageQueue(Jedis redis, String chanel) {
this.redis = redis;
this.chanel = chanel;
}
public Long publish(T message) {
String json = JSON.toJSONString(message);
return redis.publish(chanel, json);
}
public void subscribe(final MessageHandler<T> handler) {
redis.subscribe(new JedisPubSub() {
@Override
public void onMessage(String channel, String message) {
for (Type type : handler.getClass().getGenericInterfaces()) {
if (type instanceof ParameterizedType) {
ParameterizedType pType = (ParameterizedType) type;
Type handlerClass = pType.getActualTypeArguments()[0];
T result = JSONObject.parseObject(message, handlerClass);
handler.handle(result);
}
}
}
}, chanel);
}
}
public interface MessageHandler<T> {
void handle(T object);
}
注: 发送的消息不会持久化,一个订阅者只能接收到后续发布的消息,之前发送的消息就接收不到了.
持久化
Redis支持两种持久化方式: RDB与AOF. RDB: Redis根据指定的规则“定时”将内存数据快照到硬盘; AOF:Redis在每次执行命令后将命令本身记录下来存放到硬盘.两种持久化方式可结合使用.
RDB
- 快照执行过程:
- Redis使用
fork()
函数复制一份当前进程副本;- 父进程继续接收并处理客户端请求, 而子进程将所有内存数据写入磁盘临时文件;
- 当子进程将所有数据写完会用该临时文件替换旧的RDB文件, 至此一次快照完成(可以看到自始至终RDB文件都是完整的).
Redis会在以下几种情况下对数据进行快照:
- 根据配置规则
配置由两个参数构成: 时间窗口M
和改动key
个数N
; 当时间M
内被改动的key
的个数大于N
时, 即符合自动快照条件:
save 900 1
save 300 10
save 60 10000
- 用户执行
SAVE
/BGSAVE
/FLUSHALL
命令:
除了让Redis自动快照, 当进行服务重启/手动迁移以及备份时也需要我们手动执行快照.
命令 | 描述 |
---|---|
SAVE |
SAVE 命令会使Redis同步地执行快照操作(过程中会阻塞所有来自客户端的请求, 因此尽量避免线上使用) |
BGSAVE |
在后台异步执行快照操作,Redis还可继续响应请求 |
FLUSHALL |
FLUSHALL 会清空所有数据,无论是否触发了自动快照条件(只要有配置了),Redis都会执行一次快照 |
LASTSAVE |
获取最近一次成功执行快照时间 |
- 执行复制
当设置了主从模式, Redis会在复制初始化时执行快照,即使没有配置自动快照条件.
通过RDB方式实现持久化, Redis在启动后会读取RDB快照文件, 将数据从硬盘导入内存, 但如果在持久化过程中Redis异常退出, 就会丢失最后一次快照以后更改的所有数据.
RDB其他配置参数
dir ./ # 设置工作目录,RDB文件(以及后面的AOF文件)会写入该目录
dbfilename dump.rdb # 设置RDB文件名
rdbcompression yes # 导出RDB是否压缩
rdbchecksum yes # 存储和加载RDB校验完整性
stop-writes-on-bgsave-error yes # 后台备份进程出错时,主进程停止写入.
AOF
AOF将Redis执行的每一条命令追加到硬盘文件中.然后在启动Redis时逐条执行AOF文件中的命令将数据载入内存.
Redis默认没有开启AOF, 需要以如下参数启用:
appendonly yes
no-appendfsync-on-rewrite yes: # 正在导出RDB快照的过程中,停止同步AOF.
AOF重写
开启AOF后, Redis会将每一条有可能更改数据的命令写入AOF文件,这样就导致AOF文件越来越大,即使有可能内存中实际存储的数据并没多少. 因此Redis每当达到一定条件就自动重写AOF文件,这个条件可以在配置文件中设置:
auto-aof-rewrite-percentage 100 # 比起上次重写时的大小,AOF增长率100%时重写
auto-aof-rewrite-min-size 64mb # AOF大小超过64M时重写
此外, 我们还可以使用BGREWRITEAOF
命令手动执行AOF重写.
硬盘数据同步
执行AOF持久化时, 由于操作系统缓存机制, 数据并没有真正写入磁盘,而是进入了磁盘缓存, 默认情况下系统每30S执行一次同步操作, 将缓存内容真正写入磁盘, 如果在这30S的系统异常退出则会导致磁盘缓存数据丢失, 如果应用无法忍受这样的损失, 可通过appendfsync
参数设置同步机制:
# appendfsync always # 每次执行写入都执行同步
appendfsync everyse # 每秒执行一次同步操作
# appendfsync no # 不主动进行同步, 而是完全由操作系统执行.
集群
1. Replication
复制(replication)中,Redis的角色可以分为两类, Master:可以执行读/写操作,当写操作导致数据修改时会自动将数据同步给Slave; Slave:一般是只读的,并接受Master同步过来的数据(Slave自身也可以作为Master存在, 如图):
- replication复制时序
- Slave启动后向Master发送
SYNC
命令;Master收到后在后台保存RDB快照, 并将快照期间接收到的所有命令缓存. - 快照执行完, Master将快照文件与所有缓存的命令发送给Slave;
- Slave接收并载入快照, 然后执行所有收到的缓存命令,这一过程称为复制初始化.
- 复制初始化完成后,Master每接收到写命令就同步给Slave,从而保证主从数据一致.
- Slave启动后向Master发送
- 通过Redis的复制功能可以实现以下应用:
- 读写分离:
通过复制可实现读写分离, 以提高服务器的负载能力, 可以通过复制建立多个Slave节点, Master只进行写操作, 而由Slave负责读操作, 这种一主多从的结构很适合读多写少的场景. - Slave持久化
持久化是一个相对耗时的操作, 为了提高性能, 可以通过复制功能建立一个/多个Slave, 并在Salve中启用持久化, Master禁用持久化. 当Master崩溃后:
- 在Slave使用
SLAVEOF NO ONE
命令将Slave提升成Master继续服务; - 启用之前崩溃的Master, 然后使用
SLAVEOF
将其设置为新Master的Slave, 即可将数据同步回来.
- 在Slave使用
- 读写分离:
注意: 当开启复制且Master关闭持久化时, Master崩溃后一定不能直接重启Master, 这是因为当Master重启后, 因为没有开启持久化, 所以Redis内的所有数据都会被清空, 这时Salve从Master接受数据, 所有的Slave也会被清空, 导致Slave持久化失去意义.
关于Redis复制的详细介绍以及配置方式可参考博客:Redis研究 -主从复制.
2. Sentinel
当Master遭遇异常中断服务后, 需要手动选择一个Slave升级为Master, 以使系统能够继续提供服务. 然而整个过程相对麻烦且需要人工介入, 难以实现自动化. 为此Redis提供了哨兵Sentinel.
Sentinel哨兵是Redis高可用性解决方案之一: 由一个/多个Sentinel实例组成的Sentinel系统可以监视任意多个Master以及下属Slave, 并在监控到Master进入下线状态时, 自动将其某个Slave提升为新的Master, 然后由新的Master代替已下线的Master继续处理命令请求.
- 如图: 若此时Master:server1进入下线状态, 那么Slave: server2,server3,server4对Master的复制将被迫中止,并且Sentinel系统也会察觉到server1已下线, 当下线时长超过用户设定的下线时长时, Sentinel系统就会对server1执行故障转移操作:
- Sentinel会挑选server1下属的其中一台Slave, 将其提升为新Master;
- 然后Sentinel向server1下属的所有Slave发送新的复制指令,让他们成为新Master的Salve, 当所有Salve都开始复制新Master时, 故障转移操作完成.
- 另外, Sentinel还会继续监视已下线的server1, 并在他重新上线时, 将其设置为新Master的Slave.
关于Redis哨兵的详细介绍以及配置方式可参考博客:Redis Sentinel(哨兵):集群解决方案.
3. Cluster
Cluster是Redis提供的另一高可用性解决方案:Redis集群通过分片(sharding)来进行数据共享, 并提供复制与故障转移功能.
一个 Redis 集群通常由多个节点组成, 最初每个节点都是相互独立的,要组建一个真正可工作的集群, 必须将各个独立的节点连接起来.连接各个节点的工作可以使用CLUSTER MEET
命令完成:
CLUSTER MEET <ip> <port>
向一个节点发送CLUSTER MEET
命令,可以使其与ip
+port
所指定的节点进行握手,当握手成功时, 就会将目标节点添加到当前节点所在的集群中.
- 案例
假设现在有三个独立的节点 127.0.0.1:7000 、 127.0.0.1:7001 、 127.0.0.1:7002:
- 通过向节点 7000 发送
CLUSTER MEET 127.0.0.1 7001
命令,可将节点7001添加到节点7000所在的集群中: - 继续向节点7000发送
CLUSTER MEET 127.0.0.1 7002
命令,同样也可将节点7002也拉进来: - 至此, 握手成功的三个节点处于同一个集群:
- 通过向节点 7000 发送
关于Redis-Cluster的详细介绍以及更多配置方式可参考博客:redis-cluster研究和使用.
管理
1. 数据库密码
通过在配置文件中使用requirepass
参数可为Redis设置密码:
requirepass œ∑´®†¥¨ˆøπ
这样客户端每次连接都需要发送密码,否则Redis拒绝执行客户端命令:
AUTH œ∑´®†¥¨ˆøπ
2. 重命名
Redis支持在配置文件中将命令重命名, 以保证只有自己的应用可以使用该命令:
rename-command FLUSHALL qwertyuiop
如果希望禁用某个命令,可将命令重命名为空字符串.
3. 工具
SLOWLOG
当一条命令执行超过时间限制时,Redis会将其执行时间等信息加入耗时统计日志, 超时时间等可通过以下配置实现:
slowlog-log-slower-than 10000 # 超时限制(单位微秒)
slowlog-max-len 128 # 记录条数限制
MONITOR
: 监控Redis执行的所有命令注意:
MONITOR
命令非常影响Redis性能, 一个客户端使用MONITOR
会降低Redis将近一半的负载能力. Instagram团队开发了一个基于MONITOR
命令的Redis查询分析工具redis-faina, 可根据MONITOR
的监控结果分析出最常用的命令/访问最频繁的key
等信息, 详细可参考博客:关于 Redis 的性能分析工具 Redis Faina.其他常用管理工具
TIME # 系统时间戳与微秒数
DBSIZE # 当前数据库的key数量
INFO # Redis服务器信息
CONFIG GET # 获取配置信息
CONFIG SET # 设置配置信息
CONFIG REWRITE # 把值写到配置文件
CONFIG RESTART # 更新INFO命令信息
CLIENT LIST # 客户端列表
CLIENT KILL # 关闭某个客户端
CLIENT SETNAME # 为客户端设置名字
CLIENT GETNAME # 获取客户端名字
DEBUG OBJECT key # 调试选项,查看一个key的信息
DEBUG SEGFAULT # 模拟段错误,使服务器崩溃
OBJECT (refcount|encoding|idletime) key
- 参考&拓展
- 高可用、开源的Redis缓存集群方案
- Twemproxy——针对MemCached与Redis的代理
- Redis 3.0正式版发布,正式支持Redis集群
- Redis应用实践:小红书海量Redis存储之道
- Redis内存优化实践
- 视频: Raft 教程
- 使用Redis作为时间序列数据库:原因及方法
- Redis复制与可扩展集群搭建