redis 安装
首先,目前redis最新的稳定版是5.0.3版本下载地址:http://download.redis.io/releases/redis-5.0.3.tar.gz 下载最新版本后使用远程工具上传到Linux服务器,或 wget http://download.redis.io/releases/redis-5.0.3.tar.gz 然后
执行解压命令:tar xzf redis-5.0.3.tar.gz
cd redis-5.0.3
make
需要注意执行make命令时需要依赖gcc包,如果当前Linux不存在此环境,需要手动安装gcc
yum install gcc-c++
执行命令:
src/redis-server ./redis.conf
启动完毕
redis的主从
redis的主从很简单,只需要修改从节点上redis服务的redis.conf配置文件即可,
################################# REPLICATION #################################
# Master-Replica replication. Use replicaof to make a Redis instance a copy of
# another Redis server. A few things to understand ASAP about Redis replication.
#
# +------------------+ +---------------+
# | Master | ---> | Replica |
# | (receive writes) | | (exact copy) |
# +------------------+ +---------------+
#
# 1) Redis replication is asynchronous, but you can configure a master to
# stop accepting writes if it appears to be not connected with at least
# a given number of replicas.
# 2) Redis replicas are able to perform a partial resynchronization with the
# master if the replication link is lost for a relatively small amount of
# time. You may want to configure the replication backlog size (see the next
# sections of this file) with a sensible value depending on your needs.
# 3) Replication is automatic and does not need user intervention. After a
# network partition replicas automatically try to reconnect to masters
# and resynchronize with them.
#
# replicaof <masterip> <masterport>
# <masterip> 主节点ip <masterport> 主节点监听端口,一般是6379
replicaof 192.168.235.xxx 6379
此时我们以此启动主节点和从节点(一般从节点数量大于等于2),并在主节点中保存某个参数值:
127.0.0.1:6379> set name "zhangsan"
OK
127.0.0.1:6379>
从节点查看刚刚设置的参数值:
127.0.0.1:6379> get name
"zhangsan"
127.0.0.1:6379>
显然我们可以在从节点上获取到主节点设置的数值,那么原理是什么呢?
这时候就是redis的主从复制出场的时候了.
概念:
- redis的复制功能是支持多个数据库之间的数据同步。一类是主数据库(master)一类是从数据库(slave),主数据库可以进行读写操作,当发生写操作的时候自动将数据同步到从数据库,而从数据库一般是只读的,并接收主数据库同步过来的数据,一个主数据库可以有多个从数据库,而一个从数据库只能有一个主数据库。
- 通过redis的复制功能可以很好的实现数据库的读写分离,提高服务器的负载能力。主数据库主要进行写操作,而从数据库负责读操作。
主从复制过程:
过程解析:
- 当一个从数据库启动时,会向主数据库发送sync命令,
- 主数据库接收到sync命令后会开始在后台保存快照(执行rdb操作),并将保存期间接收到的命令缓存起来
- 当快照完成后,redis会将快照文件和所有缓存的命令发送给从数据库。
- 从数据库收到后,会载入快照文件并执行收到的缓存的命令。
从节点也设置一个值:
127.0.0.1:6379> set name "lisi"
(error) READONLY You can't write against a read only replica.
127.0.0.1:6379>
很显然,从节点只有读的权利,没有写的权利;
redis高可用实现(哨兵机制)
什么是哨兵机制
Redis的哨兵(sentinel) 系统用于管理多个 Redis 服务器,该系统执行以下三个任务:
- 监控(Monitoring): 哨兵(sentinel) 会不断地检查你的Master和Slave是否运作正常。
- 提醒(Notification): 当被监控的某个 Redis出现问题时, 哨兵(sentinel) 可以通过 API 向管理员或者其他应用程序发送通知。
- 自动故障迁移(Automatic failover): 当一个Master不能正常工作时,哨兵(sentinel) 会开始一次自动故障迁移操作,它会将失效Master的其中一个Slave升级为新的Master, 并让失效Master的其他Slave改为复制新的Master; 当客户端试图连接失效的Master时,集群也会向客户端返回新Master的地址,使得集群可以使用Master代替失效Master。
哨兵(sentinel) 是一个分布式系统,你可以在一个架构中运行多个哨兵(sentinel) 进程,这些进程使用流言协议(gossipprotocols)来接收关于Master是否下线的信息,并使用投票协议(agreement protocols)来决定是否执行自动故障迁移,以及选择哪个Slave作为新的Master.
每个哨兵(sentinel) 会向其它哨兵(sentinel)、master、slave定时发送消息,以确认对方是否”活”着,如果发现对方在指定时间(可配置)内未回应,则暂时认为对方已挂 (所谓的”主观认为宕机” Subjective Down,简称sdown).
若“哨兵群”中的多数sentinel,都报告某一master没响应,系统才认为该master"彻底死亡" (即:客观上的真正down机,Objective Down,简称odown),通过一定的vote算法,从剩下的slave节点中,选一台提升为master,然后自动修改相关配置.
虽然哨兵(sentinel) 释出为一个单独的可执行文件 redis-sentinel ,但实际上它只是一个运行在特殊模式下的 Redis 服务器,你可以在启动一个普通 Redis 服务器时通过给定 --sentinel 选项来启动哨兵(sentinel).
哨兵启动:
配置3个哨兵,每个哨兵的配置都是一样的。在Redis安装目录下有一个sentinel.conf文件
# port <sentinel-port>
# The port that this sentinel instance will run on
port 26379
bind 0.0.0.0
# By default Redis Sentinel does not run as a daemon. Use 'yes' if you need it.
# Note that Redis will write a pid file in /var/run/redis-sentinel.pid when
# daemonized.
daemonize yes
# How many replicas we can reconfigure to point to the new replica simultaneously
# during the failover. Use a low number if you use the replicas to serve query
# to avoid that all the replicas will be unreachable at about the same
# time while performing the synchronization with the master.
sentinel monitor mymaster 192.168.235.141 6379 2
有了上述的修改,我们可以进入Redis的安装目录的src目录,通过下面的命令启动服务器和哨兵
# 启动Redis服务器进程
./redis-server ../redis.conf
# 启动哨兵进程
./redis-sentinel ../sentinel.conf
注意启动的顺序。首先是主机(192.168.235.141)的Redis服务进程,然后启动从机的服务进程,最后启动3个哨兵的服务进程。
启动完成后可以检查一下当前节点上redis的role
192.168.235.141节点情况如下:
127.0.0.1:6379> role
1) "master"
2) (integer) 315064
3) 1) 1) "192.168.235.142"
2) "6379"
3) "315064"
2) 1) "192.168.235.143"
2) "6379"
3) "315064"
192.168.235.142节点情况如下:
127.0.0.1:6379> role
1) "slave"
2) "192.168.235.141"
3) (integer) 6379
4) "connected"
5) (integer) 285845
192.168.235.143节点情况如下:
127.0.0.1:6379> role
1) "slave"
2) "192.168.235.141"
3) (integer) 6379
4) "connected"
5) (integer) 334765
可以看到192.168.235.141 此时是主节点,那么如果我们让该节点redis服务宕机,会出现什么情况呢?
理论上来说,哨兵服务就会从剩余的两个节点选举一个新的主节点,让我们检查一下:
192.168.235.141节点情况如下:
127.0.0.1:6379> role
Could not connect to Redis at 127.0.0.1:6379: Connection refused
192.168.235.142节点情况如下:
127.0.0.1:6379> role
1) "slave"
2) "192.168.235.141"
3) (integer) 6379
4) "connected"
5) (integer) 285845
192.168.235.143节点情况如下:
127.0.0.1:6379> role
1) "slave"
2) "192.168.235.141"
3) (integer) 6379
4) "connected"
5) (integer) 334765
咦~~,为嘛没有新的master?不合理啊!!!
不要着急,我们手动kill 主redis后 ,哨兵并不会立刻认为当前主服务已死,会有一个等待时间;
该等待时间在sentinel.conf中可以配置:
# sentinel down-after-milliseconds <master-name> <milliseconds>
#
# Number of milliseconds the master (or any attached replica or sentinel) should
# be unreachable (as in, not acceptable reply to PING, continuously, for the
# specified period) in order to consider it in S_DOWN state (Subjectively
# Down).
#
# Default is 30 seconds.
查看后发现,
如果一个实例(instance)距离最后一次有效回复PING命令的时间超过 own-after-milliseconds 选项所指定的值,则这个实例会被Sentinel标记为主观下线
默认30s,所以我们一般需要等待30s后才会进行选举新的主服务
之前没有注意到这点,被坑了一次 nnp~~
redis 事务
这个功能目前貌似并不常用,一次性执行多个redis命令,不太清除什么场景下会用到这个
功能与关系型数据库事务的功能一样,redis事务从开始到执行会经历以下三个阶段:
开始事务 —> 命令入队 ----> 执行事
下面是一个事务的例子:
redis 127.0.0.1:6379> MULTI
OK
redis 127.0.0.1:6379> SET book-name "Mastering C++ in 21 days"
QUEUED
redis 127.0.0.1:6379> GET book-name
QUEUED
redis 127.0.0.1:6379> SADD tag "C++" "Programming" "Mastering Series"
QUEUED
redis 127.0.0.1:6379> SMEMBERS tag
QUEUED
redis 127.0.0.1:6379> EXEC
1) OK
2) "Mastering C++ in 21 days"
3) (integer) 3
4) 1) "Mastering Series"
2) "C++"
3) "Programming"(⊙o⊙)…
事务由 MULTI 开启,中间可以输入各种命令,最后以EXEC执行
redis发布订阅
Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。
Redis 客户端可以订阅任意数量的频道。
下图展示了频道 channel1 , 以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 之间的关系:
当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端:
以下实例演示了发布订阅是如何工作的。在我们实例中我们创建了订阅频道名为 redisChat:
redis 127.0.0.1:6379> SUBSCRIBE redisChat
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "redisChat"
3) (integer) 1
现在,我们先重新开启个 redis 客户端,然后在同一个频道 redisChat 发布两次消息,订阅者就能接收到消息。
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"
发布订阅命令:
命令 | 描述 |
PSUBSCRIBE pattern [pattern …] | 订阅一个或多个符合给定模式的频道。 |
PUBSUB subcommand [argument [argument …]] | 查看订阅与发布系统状态。 |
PUBLISH channel message | 将信息发送到指定的频道。 |
PUNSUBSCRIBE [pattern [pattern …]] | 退订所有给定模式的频道。 |
SUBSCRIBE channel [channel …] | 订阅给定的一个或多个频道的信息。 |
UNSUBSCRIBE [channel [channel …]] | 退订给定的频道。 |
redis 集群模式
redis 分布式锁
redis 实现分布式锁,可靠性不如zk,但是效率比zk更快,可用于那些对可靠性要求没这么高的场景
实现原理:
redis.setnx() 当且仅当 当前redis中不存在该key时才能保存成功,多个线程同时操作setnx() ,
成功设置的那个认为得到了锁,其他的则获取失败,进入等待队列,等待锁的释放
隐患
- redis.setnx() 与 expire 命令之间并不是原子操作,可能出现问题
- redis 的主从复制是异步过程,一旦出现单点故障,可能出现数据还未复制到从机时,其他线程获取锁,导致多个线程拥有锁
- 过期时间的设置是个问题,过大会导致其他线程等待时间加长,过小,业务未处理完锁已经失效了,会导致多个线程拥有锁
下面贴实现代码:(较为简易的实现,不太牢靠)
package com.study.lock.redisLock;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import java.util.UUID;
/**
* redis 实现分布式锁,可靠性不如zk,但是效率比zk更快,可用于那些对可靠性要求没这么高的场景
*
* 实现原理:
* redis.setnx() 当且仅当 当前redis中不存在该key时才能保存成功,多个线程同时操作setnx() ,
* 成功设置的那个认为得到了锁,其他的则获取失败,进入等待队列,等待锁的释放
*
*
* 可能出现的隐患:
*
* redis.setnx() 与 expire 命令之间并不是原子操作,可能出现问题
* 过期时间的设置是个问题,过大会导致其他线程等待时间加长,过小,业务未处理完锁已经失效了,会导致多个线程获取到锁
*
* @date: 2019/03/01
* @author: zhenguo.yao
*/
public class RedisDistributeLock {
private JedisPool jedisPool;
private final String redisLockKey = "redis_lock";
public RedisDistributeLock(JedisPool jedisPool){
this.jedisPool = jedisPool;
}
/**
* 超时时间的问题:
* acquireTimeout ---- 尝试获取锁的时候如果在规定的时间内没有拿到锁,直接放弃
* timeOut ---- 得到锁后,对应key的有效期,超出时间key失效
*
* 可能出现的问题,A线程得到锁,并执行业务逻辑,由于某种原因导致key 超时失效,但是A并没有主动释放锁,此时A依然认为自己拥有锁
* B线程得到A释放锁的通知,成功获取到了锁,此时就会出现多个线程同时获取到了锁. 如何解决?
*
*
*
* @param acquireTimeout 在获取锁之前的超时时间
* @param timeOut 在获取锁之后的超时时间
* @return
*/
public String getRedisLock(Long acquireTimeout, Long timeOut){
try(Jedis jedis = jedisPool.getResource()){
// 获取到与key 对应的随机value 标识;释放锁的时候需要用到
String identifierValue = UUID.randomUUID().toString();
// 定义在获取锁之后的超时时间,单位:秒
int expireLock = (int) (timeOut / 1000);
// 定义获取锁之前的超时时间
long endTime = System.currentTimeMillis() + acquireTimeout;
// 使用循环机制,如果没有获取锁,要在规定的时间内,保证重复的尝试获取锁(乐观锁)
while (System.currentTimeMillis() < endTime){
// 使用setnx命令插入对应的redisLockKey ,如果返回为1 成功获取锁
if(jedis.setnx(redisLockKey,identifierValue) == 1){
// 设置当前key的有效期,目的是防止死锁;
// 场景:A获取到锁后,由于某种原因服务宕机了,此时如果没有过期机制会导致A锁永远释放不了,其他主机上的线程就会一直等待
jedis.expire(redisLockKey,expireLock);
return identifierValue;
}
}
}
return null;
}
/**
* 释放锁
* @param identifierValue
*/
public void unRedisLock(String identifierValue){
try(Jedis jedis = jedisPool.getResource()){
String actualValue = jedis.get(redisLockKey);
// 当actualValue = null 时有两种可能;
// 1. 没有线程获取到锁
// 2. 该key已经过期失效
if(actualValue != null){
if(actualValue.equals(identifierValue)){
jedis.del(redisLockKey);
}
}
}
}
}