标签 : Java与NoSQLjavascript
Redis事务(
transaction
)是一组命令的集合,同命令同样也是Redis的最小执行单位, Redis保证一个事务内的命令执行不被其余命令影响.css
`MULTI`
SADD user:1:following 2
SADD user:2:follower 1
`EXEC`
事务操做 | MySQL | Redis |
---|---|---|
开启 | start transaction |
MULTI |
语句 | DML | 普通命令 |
取消 | rollback |
DISCARD |
执行 | commit |
EXEC |
rollback
与Redis的DISCARD
有必定的区别. rollback
后,前2条的语句影响消失.ZADD
操做List
), EXEC
会执行前2条语句, 并跳过第3条语句. key
的设计.WATCH
悲观锁(Pessimistic Lock): 很悲观,每次读写数据都认为别人会修改,因此每次读数据都会上锁,这样若是别人也想读写这条数据就会阻塞, 直到加锁的人把锁释放. 传统的RDBMS中用到了不少这种锁机制, 如行锁、表锁、读锁、写锁等.
乐观锁(Optimistic Lock): 顾名思义很是乐观, 每次读写数据时候都认为别人不会修改,因此再也不上锁,但在更新数据时会判断一下在此期间有没有人更新了这条数据, 这个判断过程可使用版本号
等机制实现, 而Redis默认就对乐观锁提供了支持 –WATCH
命令.html
WATCH
命令能够监控一个/多个key
, 一旦其中有一个被修改/删除, 则以后的事务就不会执行,如用WATCH
命令来模拟抢票场景:java
SET ticket 1 # 如今假设只有一张票了
`WATCH` ticket # 监控票数变化
`MULTI`
DECRBY username 400
DECR ticket
[DECR ticket] # 如今假设有另一个用户直接把这张票买走了
`EXEC` -> `(nil)` # 则这条事务执行就不会成功
WATCH
命令的做用只是当被监控的key
值修改后阻止事务执行,并不能阻止其余Client修改. 因此一旦EXEC
执行失败, 能够从新执行整个方法或使用UNWATCH
命令取消监控.mysql
乐观锁适用于读多写少情景,即冲突真的不多发生,这样能够省去大量锁的开销. 但若是常常产生冲突,上层应用须要不断的retry,反却是下降了性能,因此这种状况悲观锁比较适用.nginx
Redis可使用
EXPIRE
命令设置key
的过时时间, 到期后Redis会自动删除它.git
命令 | 做用 |
---|---|
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
被修改.github
/** * @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
:redis
maxmemory-policy |
规则说明 |
---|---|
volatile-lru |
只对设置了过时时间的key使用LRU算法删除 |
allkey-lru |
使用LRU删除一个key |
volatile-random |
只对设置了过时时间的key随机删除一个key |
allkey-random |
随机删除一个key |
volatile-ttl |
删除过时时间最近的一个key |
noevication |
不删除key, 只返回错误(默认) |
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
排序时会忽略元素分数,只针对元素自身值排序.
不少状况下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
将不会执行排序操做;key
不存在时,默认设置为0;pattern
值相同,则会再比较元素自己值排序.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
将结果缓存.消息队列就是”传递消息的队列”,与消息队列进行交互的实体有两类, 一是生产者: 将须要处理的消息放入队列; 一是消费者: 不断从消息队列中读出消息并处理.
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
的第一个元素.
前面的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 |
/** * @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在每次执行命令后将命令自己记录下来存放到硬盘.两种持久化方式可结合使用.
- 快照执行过程:
- Redis使用
fork()
函数复制一份当前进程副本;- 父进程继续接收并处理客户端请求, 而子进程将全部内存数据写入磁盘临时文件;
- 当子进程将全部数据写完会用该临时文件替换旧的RDB文件, 至此一次快照完成(能够看到自始至终RDB文件都是完整的).
Redis会在如下几种状况下对数据进行快照:
M
和改动key
个数N
; 当时间M
内被改动的key
的个数大于N
时, 即符合自动快照条件:save 900 1 save 300 10 save 60 10000
SAVE
/BGSAVE
/FLUSHALL
命令: 命令 | 描述 |
---|---|
SAVE |
SAVE 命令会使Redis同步地执行快照操做(过程当中会阻塞全部来自客户端的请求, 所以尽可能避免线上使用) |
BGSAVE |
在后台异步执行快照操做,Redis还可继续响应请求 |
FLUSHALL |
FLUSHALL 会清空全部数据,不管是否触发了自动快照条件(只要有配置了),Redis都会执行一次快照 |
LASTSAVE |
获取最近一次成功执行快照时间 |
经过RDB方式实现持久化, Redis在启动后会读取RDB快照文件, 将数据从硬盘导入内存, 但若是在持久化过程当中Redis异常退出, 就会丢失最后一次快照之后更改的全部数据.
dir ./ # 设置工做目录,RDB文件(以及后面的AOF文件)会写入该目录
dbfilename dump.rdb # 设置RDB文件名
rdbcompression yes # 导出RDB是否压缩
rdbchecksum yes # 存储和加载RDB校验完整性
stop-writes-on-bgsave-error yes # 后台备份进程出错时,主进程中止写入.
AOF将Redis执行的每一条命令追加到硬盘文件中.而后在启动Redis时逐条执行AOF文件中的命令将数据载入内存.
Redis默认没有开启AOF, 须要以以下参数启用:
appendonly yes
no-appendfsync-on-rewrite yes: # 正在导出RDB快照的过程当中,中止同步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 # 不主动进行同步, 而是彻底由操做系统执行.
复制(replication)中,Redis的角色能够分为两类, Master:能够执行读/写操做,当写操做致使数据修改时会自动将数据同步给Slave; Slave:通常是只读的,并接受Master同步过来的数据(Slave自身也能够做为Master存在, 如图):
SYNC
命令;Master收到后在后台保存RDB快照, 并将快照期间接收到的全部命令缓存.SLAVEOF NO ONE
命令将Slave提高成Master继续服务;SLAVEOF
将其设置为新Master的Slave, 便可将数据同步回来.注意: 当开启复制且Master关闭持久化时, Master崩溃后必定不能直接重启Master, 这是由于当Master重启后, 由于没有开启持久化, 因此Redis内的全部数据都会被清空, 这时Salve从Master接受数据, 全部的Slave也会被清空, 致使Slave持久化失去意义.
关于Redis复制的详细介绍以及配置方式可参考博客:Redis研究 -主从复制.
当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(哨兵):集群解决方案.
Cluster是Redis提供的另外一高可用性解决方案:Redis集群经过分片(sharding)来进行数据共享, 并提供复制与故障转移功能.
一个 Redis 集群一般由多个节点组成, 最初每一个节点都是相互独立的,要组建一个真正可工做的集群, 必须将各个独立的节点链接起来.链接各个节点的工做可使用CLUSTER MEET
命令完成:
CLUSTER MEET <ip> <port>
向一个节点发送CLUSTER MEET
命令,可使其与ip
+port
所指定的节点进行握手,当握手成功时, 就会将目标节点添加到当前节点所在的集群中.
CLUSTER MEET 127.0.0.1 7001
命令,可将节点7001添加到节点7000所在的集群中: CLUSTER MEET 127.0.0.1 7002
命令,一样也可将节点7002也拉进来: 关于Redis-Cluster的详细介绍以及更多配置方式可参考博客:redis-cluster研究和使用.
经过在配置文件中使用requirepass
参数可为Redis设置密码:
requirepass œ∑´®†¥¨ˆøπ
这样客户端每次链接都须要发送密码,不然Redis拒绝执行客户端命令:
AUTH œ∑´®†¥¨ˆøπ
Redis支持在配置文件中将命令重命名, 以保证只有本身的应用可使用该命令:
rename-command FLUSHALL qwertyuiop
若是但愿禁用某个命令,可将命令重命名为空字符串.
SLOWLOG
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