Redis
1 性能高,读每秒是11w,写每秒是8w
2 丰富的数据结构,支持string,list,set,hash,sortedset
3 原子性操作,要不全部成功,要不全部失败
4 发布与订阅,完成类似队列功能
5 分布式锁的内在支持
6 高可用,高性能,支持集群,支持哨兵,支持读写分离
数据缓存(商品数据、新闻、热点数据)
单点登录
秒杀、抢购
网站访问排名,排行榜
应用的模块开发
Redis 的客户端和服务端之间采取了一种独立名为 RESP(REdis Serialization Protocol) 的协议,它的特点是容易实现和解析快,可读性强
在 RESP 中, 一些数据的类型通过它的第一个字节进行判断:
单行回复:回复的第一个字节是 “+”
错误信息:回复的第一个字节是 “-”
整形数字:回复的第一个字节是 “:”
多行字符串:回复的第一个字节是 “$”
数组:回复的第一个字节是 “*”
举例
比如使用set命令, SET simpleKey simpleValue,
3 代表有三个字符串
9 代表有九个字符串
11 代表有十一个字符串
\\r\\n代表空格和换行
*3\\r\\n$3\\r\\nSET\\r\\n$9\\r\\nsimpleKey\\r\\n$11\\r\\nsimpleValue\\r\\n"
使用get命令.GET simpleKey
“*2\\r\\n$3\\r\\nGET\\r\\n$9\\r\\nsimpleKey\\r\\n”
2 代表命令有两个字符串
9 代表KEY的长度是9
get,获取指定的值
set,设置指定key对应的value
del,删除指定的key
incr,原子操作+1
decr,原子操作-1
incrby,将key所存储的值加上增量返回增加之后的值
decrby,将key所存储的值减去decrment,返回减去之后的值
字符串最经典的使用场景,redis最为缓存层,Mysql作为储存层,绝大部分请求数据都是redis中获取,由于redis具有支撑高并发特性,所以缓存通常能起到加速读写和降低 后端压力的作用。
许多运用都会使用redis作为计数的基础工具,他可以实现快速计数、查询缓存的功能,同时数据可以一步落地到其他的数据源。
如:视频播放数系统就是使用redis作为视频播放数计数的基础组件。
出于负载均衡的考虑,分布式服务会将用户信息的访问均衡到不同服务器上,
用户刷新一次访问可能会需要重新登录,为避免这个问题可以用redis将用户session集中管理,
在这种模式下只要保证redis的高可用和扩展性的,每次获取用户更新或查询登录信息都直接从redis中集中获取。
处于安全考虑,每次进行登录时让用户输入手机验证码,为了短信接口不被频繁访问,会限制用户每分钟获取验证码的频率
键值对集合,即编程语言中的Map类型
hset,在散列里面关联起指定的键值对
hget,获取指定散列键的值
hgetall, 获取散列包含的所有键值对
hdel, 如果给定键存在于散列里面,那么移除这个键
存储、读取、修改用户属性
一个列表可以有序地存储多个字符串,并且列表里的元素是可以重复的
LPUSH,将元素推入列表的左端
RPUSH,将元素推入列表的右端
LPOP,从列表左端弹出元素
RPOP,从列表右端弹出元素
LINDEX,获取列表在给定位置上的一个元素
LRANGE,获取列表在给定范围上的所有元素
常用命令
在key对应list的头部添加字符串元素
±---------------------------------------------+
| 127.0.0.1:6379> lpush address “Shang Hai” |
| |
| (integer) 1 |
| |
| 127.0.0.1:6379> lpush address huangpu |
| |
| (integer) 2 |
| |
| 127.0.0.1:6379> lrange address 0 -1 |
| |
| 1) “huangpu” |
| |
| 2) “Shang Hai” |
±---------------------------------------------+
在key对应list的尾部添加字符串元素
±----------------------------------------------+
| 127.0.0.1:6379> rpush address2 “Shang Hai” |
| |
| (integer) 1 |
| |
| 127.0.0.1:6379> rpush address2 “huangpu” |
| |
| (integer) 2 |
| |
| 127.0.0.1:6379> lrange address2 0 -1 |
| |
| 1) “Shang Hai” |
| |
| 2) “huangpu” |
±----------------------------------------------+
从key对应list中删除n个和value相同的元素
±-----------------------------------------+
| 127.0.0.1:6379> lrange myColour 0 -1 |
| |
| 1) “pink” |
| |
| 2) “red” |
| |
| 3) “red” |
| |
| 4) “purple” |
| |
| 5) “red” |
| |
| 6) “yellow” |
| |
| 127.0.0.1:6379> lrem myColour 1 “red” |
| |
| (integer) 1 |
| |
| 127.0.0.1:6379> lrange myColour 0 -1 |
| |
| 1) “pink” |
| |
| 2) “red” |
| |
| 3) “purple” |
| |
| 4) “red” |
| |
| 5) “yellow” |
±-----------------------------------------+
保留指定key的值范围内的数据。即保留下标指定范围的field,其他的被删除。(用法:ltrim list链表名称 位置索引1 位置索引2) 保留位置索引1 到位置索引2的元素,其余全部删除
±--------------------------------------+
| 127.0.0.1:6379> lrange myColour 0 -1 |
| |
| 1) “yellow” |
| |
| 2) “purple” |
| |
| 3) “pink” |
| |
| 4) “red” |
| |
| 127.0.0.1:6379> ltrim myColour 2 -1 |
| |
| OK |
| |
| 127.0.0.1:6379> lrange myColour 0 -1 |
| |
| 1) “pink” |
| |
| 2) “red” |
±--------------------------------------+
从list的头部删除元素,并返回删除元素。
±--------------------------------------+
| 127.0.0.1:6379> lrange myColour 0 -1 |
| |
| 1) “pink” |
| |
| 2) “red” |
| |
| 127.0.0.1:6379> lpop myColour |
| |
| “pink” |
| |
| 127.0.0.1:6379> lrange myColour 0 -1 |
| |
| 1) “red” |
±--------------------------------------+
返回名称为key的list中index位置的元素,元素位置索引号从0开始
±--------------------------------------+
| 127.0.0.1:6379> lrange myColour 0 -1 |
| |
| 1) “red” |
| |
| 2) “black” |
| |
| 3) “blue” |
| |
| 127.0.0.1:6379> lindex myColour 0 |
| |
| “red” |
| |
| 127.0.0.1:6379> lindex myColour 1 |
| |
| “black” |
±--------------------------------------+
1,最新消息排行等功能(比如朋友圈的时间线)
2,消息队列
集合类型也是用来保存多个字符串的元素,但和列表不同的是集合中不允许有重复的元素,并且集合中的元素是
无序的,不能通过索引下标获取元素
SADD,将元素添加到集合成功添加返回1,如果返回0则表示集合中已经有这个元素了
SRE,M从集合里面移除元素 存在返回1,不存在返回0
SISMEMBER,快速地检查一个元素是否已经存在于集合中
SMEMBERS,获取集合包含的所有元素
1、共同好友
2、利用唯一性,统计访问网站的所有独立ip
3、好友推荐时,根据tag求交集,大于某个阈值就可以推荐
有序集合的键被成为成员,每个成员都是各不相同的。有序集合的值被成为分值,分值必须为浮点数。
ZADD,将一个带有给定分值的成员添加到有序集合里面
ZRANGE,根据元素在有序排列中所处的位置,从有序集合里面获取多个元素
ZRANGEBYSCORE,获取有序集合在给定分值范围内的所有元素
ZREM,如果给定成员存在于有序集合,那么移除这个成员
ZSCORE,返回有序集合中,成员的分数值
ZRANGE, 通过索引区间返回有序集合成指定区间内的成员
ZRANK, 返回有序集合中指定成员的索引
ZINCRBY,有序集合中对指定成员的分数加上增量 increment
备注: 它采用了复合结构, 字典维护了name=>score的映射表, 而跳跃表则维护了按score排序的列表. 按name和按score的范围查询都天然支持.
命令详解
向名称为key的zset中添加元素member,score用于排序。如果该元素存在,则更新其顺序。(用法:zadd 有序集合 顺序编号 元素值)
±-----------------------------------------------+
| 127.0.0.1:6379> zadd zset1 1 two\ |
| (integer) 1\ |
| 127.0.0.1:6379> zadd zset1 2 one\ |
| (integer) 1\ |
| 127.0.0.1:6379> zadd zset1 3 seven\ |
| (integer) 1 |
| |
| 127.0.0.1:6379> zrange zset1 0 -1 \ |
| 1) “two”\ |
| 2) “one”\ |
| 3) “seven”\ |
| 127.0.0.1:6379> zrange zset1 0 -1 withscores\ |
| 1) “two”\ |
| 2) “1”\ |
| 3) “one”\ |
| 4) “2”\ |
| 5) “seven”\ |
| 6) “2” |
±-----------------------------------------------+
删除名称为key的zset中的元素。(用法:zrem 有序集合 要删除的元素值)
127.0.0.1:6379> zrange zset1 0 -1 withscores\
如果在名称为key的zset中已经存在元素member,则该元素的score增加increment,否则向该集合中添加该元素,其score的值为increment.即对元素的顺序号进行增加或减少操作
127.0.0.1:6379> zrange zset1 0 -1 withscores\
返回名称为key的member元素的排名(按score从小到大排序)即下标
127.0.0.1:6379> zrange zset1 0 -1 withscores\
返回名称为key的member元素的排名(按score从大到小排序)即下标
127.0.0.1:6379> zrange zset1 0 -1 withscores\
显示集合中指定下标的元素值(按score从小到大排序)。如果需要显示元素的顺序编号,带上withscores
127.0.0.1:6379> zrange zset1 0 -1 withscores\
显示集合中指定下标的元素值(按score从大到小排序)。如果需要显示元素的顺序编号,带上withscores
127.0.0.1:6379> zrevrange zset1 0 -1 withscores\
返回集合中score在给定区间的数量
127.0.0.1:6379> zcount zset1 2 7
(integer) 3
返回集合中元素个数
127.0.0.1:6379> zrange zset1 0 -1\
删除集合中排名在给定区间的元素。(按索引下标删除)
127.0.0.1:6379> zrange zset1 0 -1 withscores\
删除集合中score在给定区间的元素(按顺序score值来删除)
127.0.0.1:6379> zrange zset1 0 -1 withscores\
1、排行榜
2、带权重的消息队列
下载redis安装包
tar -zxvf 安装包
在redis目录下 执行 make
可以通过make test测试编译状态
make install [prefix=/path]完成安装
./redis-server …/redis.conf
./redis-cli shutdown
以后台进程的方式启动,修改redis.conf daemonize =yes
连接到redis的命令
./redis-cli -h 127.0.0.1 -p 6379
redis-benchmark 性能测试的工具
redis-check-aof aof文件进行检测的工具
redis-check-dump rdb文件检查工具
redis-sentinel sentinel 服务器配置
slowlog 慢日志相关信息查看
Redis 的复制(replication)功能允许用户根据一个 Redis 服务器来创建任意多个该服务器的复制品,其中被复制的服务器为主服务器(master),而通过复制创建出来的服务器复制品则为从服务器(slave)。 只要主从服务器之间的网络连接正常,主从服务器两者会具有相同的数据,主服务器就会一直将发生在自己身上的数据更新同步 给从服务器,从而一直保证主从服务器的数据相同
第1步:cp reids.conf redis2.conf
第2步:Vim redis2.conf(slave)
第3步:slaveof 192.168.0.12 6379(master的地址)
第4步:Vim redis.conf (master)
第5步:bind 0.0.0.0 #无ip 都可以访问
第6步:./redis-server …/redis.conf #master
第7步./redis-server …/redis.2conf #slave
是否成功set get请求来判断或执行info命令role
1、master客户端set值,slave客户端能不能获取到
2、config get ‘slaveof*’
1、master/slave角色
2、master/slave数据相同
3、降低master读压力在转交从库
无法保证高可用
没有解决master写的压力
如果是slave node第一次连接master node,那么会触发一次full resynchronization(全量复制)
开始full resynchronization的时候,master会启动一个后台线程,开始生成一份RDB快照文件,同时还会将从客户端收到的所有写命令缓存在内存中。RDB文件生成完毕之后,master会将这个RDB发送给slave,slave会先写入本地磁盘,然后再从本地磁盘加载到内存中。然后master会将内存中缓存的写命令发送给slave,slave也会同步这些数据
master在内存中直接创建rdb,然后发送给slave,不会在自己本地落地磁盘了
repl-diskless-sync
repl-diskless-sync-delay,等待一定时长再开始复制,因为要等更多slave重新连接过来
如果主从复制过程中,网络连接断掉了,那么可以接着上次复制的地方,继续复制下去,而不是从头开始复制一份
master node会在内存中创建一个backlog,master和slave都会保存一个replica offset还有一个master id,offset就是保存在backlog中的。如果master和slave网络连接断掉了,slave会让master从上次的replica offset开始继续复制
但是如果没有找到对应的offset,那么就会执行一次resynchronization
slave不会过期key,只会等待master过期key。如果master过期了一个key,或者通过LRU淘汰了一个key,那么会模拟一条del命令发送给slave。
Redis sentinel是一个分布式系统中监控redis主从服务器,并在主服务器下线时自动进行故障转移。其中三个特性:
监控(Monitoring): Sentinel 会不断地检查你的主服务器和从服务器是否运作正常。
提醒(Notification): 当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。
自动故障迁移(Automatic failover): 当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作。
sdown和odown两种失败状态
sdown是主观宕机,就一个哨兵如果自己觉得一个master宕机了,那么就是主观宕机
odown是客观宕机,如果quorum数量的哨兵都觉得一个master宕机了,那么就是客观宕机
sdown达成的条件很简单,如果一个哨兵ping一个master,超过了is-master-down-after-milliseconds指定的毫秒数之后,就主观认为master宕机
sdown到odown转换的条件很简单,如果一个哨兵在指定时间内,收到了quorum指定数量的其他哨兵也认为那个master是sdown了,那么就认为是odown了,客观认为master宕机
哨兵互相之间的发现,是通过redis的pub/sub系统实现的,每个哨兵都会往__sentinel__:hello这个channel里发送一个消息,这时候所有其他哨兵都可以消费到这个消息,并感知到其他的哨兵的存在
每隔两秒钟,每个哨兵都会往自己监控的某个master+slaves对应的__sentinel__:hello channel里发送一个消息,内容是自己的host、ip和runid还有对这个master的监控配置
每个哨兵也会去监听自己监控的每个master+slaves对应的__sentinel__:hello channel,然后去感知到同样在监听这个master+slaves的其他哨兵的存在
每个哨兵还会跟其他哨兵交换对master的监控配置,互相进行监控配置的同步
哨兵会负责自动纠正slave的一些配置,比如slave如果要成为潜在的master候选人,哨兵会确保slave在复制现有master的数据; 如果slave连接到了一个错误的master上,比如故障转移之后,那么哨兵会确保它们连接到正确的master上
如果一个master被认为odown了,而且majority哨兵都允许了主备切换,那么某个哨兵就会执行主备切换操作,此时首先要选举一个slave来
会考虑slave的一些信息
(1)跟master断开连接的时长
(2)slave优先级
(3)复制offset
(4)run id
如果一个slave跟master断开连接已经超过了down-after-milliseconds的10倍,外加master宕机的时长,那么slave就被认为不适合选举为master
(down-after-milliseconds * 10) + milliseconds_since_master_is_in_SDOWN_state
接下来会对slave进行排序
(1)按照slave优先级进行排序,slave priority越低,优先级就越高
(2)如果slave priority相同,那么看replica offset,哪个slave复制了越多的数据,offset越靠后,优先级就越高
(3)如果上面两个条件都相同,那么选择一个run id比较小的那个slave
备注:优先级>offset>runid
每次一个哨兵要做主备切换,首先需要quorum数量的哨兵认为odown,然后选举出一个哨兵来做切换,这个哨兵还得得到majority哨兵的授权,才能正式执行切换
如果quorum < majority,比如5个哨兵,majority就是3,quorum设置为2,那么就3个哨兵授权就可以执行切换
但是如果quorum >= majority,那么必须quorum数量的哨兵都授权,比如5个哨兵,quorum是5,那么必须5个哨兵都同意授权,才能执行切换
哨兵会对一套redis master+slave进行监控,有相应的监控的配置
执行切换的那个哨兵,会从要切换到的新master(salve->master)那里得到一个configuration epoch,这就是一个version号,每次切换的version号都必须是唯一的
如果第一个选举出的哨兵切换失败了,那么其他哨兵,会等待failover-timeout时间,然后接替继续执行切换,此时会重新获取一个新的configuration epoch,作为新的version号
哨兵完成切换之后,会在自己本地更新生成最新的master配置,然后同步给其他的哨兵,就是通过之前说的pub/sub消息机制
这里之前的version号就很重要了,因为各种消息都是通过一个channel去发布和监听的,所以一个哨兵完成一次新的切换之后,新的master配置是跟着新的version号的
其他的哨兵都是根据版本号的大小来更新自己的master配置的
部署哨兵
前提:先搭好一主两从redis的主从复制,和之前的主从复制搭建一样,再单独部署一个redis,修改配置文件sentinel.conf,默认只需要修改一行.
sentinel monitor mymaster 192.168.1.2 6379 2 只需要修改这一行
sentinel down-after-milliseconds mymaster 60000
sentinel failover-timeout mymaster 180000
sentinel parallel-syncs mymaster 1
down-after-milliseconds,超过多少毫秒跟一个redis实例断了连接,哨兵就可能认为这个redis实例挂了
parallel-syncs,新的master别切换之后,同时有多少个slave被切换到去连接新master,重新做同步,数字越低,花费的时间越多
failover-timeout,执行故障转移的timeout超时时长
启动哨兵服务.
./redis-server conf/redis6379.conf &
之后可以把主节点的进程给杀死,然后观察是否有从节点,被选举为主节点.
1 保证高可用
2 监控各个节点
3 自动故障转移
1 主从模式,切换的时候需要一定的时候,可能会存在数据丢失
2 没有解决Master写的压力
3 可能会产生Split-Brain
Twemproxy是一个Twitter开源的一个redis和memcache快速/轻量级代理服务器;Twemproxy是一个快速的单线程代理程序,支持Memcached ASCII协议和redis协议
1、多种hash算法:MD5、CRC16、CRC32、CRC32a、hsieh、murmur、Jenkins
2、支持失败节点自动删除
3、后端Sharding分片逻辑对业务透明,业务方的读写方式和操作单个Redis一致
**1.**增加了新的proxy,需要维护其高可用。
2.failover逻辑需要自己实现,其本身不能支持故障的自动转移
3.无法支持平滑的扩容或者缩容。
(不自动故障转移,不支持动态扩容和缩容)
Codis 是一个分布式 [Redis]{.underline} 解决方案, 对于上层的应用来说, 连接到 Codis Proxy 和连接原生的 Redis Server 没有明显的区别 (有一些命令不支持), 上层应用可以像使用单机的 Redis 一样使用, Codis 底层会处理请求的转发, 不停机的数据迁移等工作, 所有后边的一切事情, 对于前面的客户端来说是透明的, 可以简单的认为后边连接的是一个内存无限大的 Redis 服务
codis支持动态水平扩展,对client完全透明不影响服务的情况下可以完成增减redis实例的操作;
codis是用go语言写的并支持多线程,twemproxy用C并只用单线程。 后者又意味着:codis在多核机器上的性能会好于twemproxy;codis的最坏响应时间可能会因为GC的STW而变大,不过go1.5发布后会显著降低STW的时间;如果只用一个CPU的话go语言的性能不如C,因此在一些短连接而非长连接的场景中,整个系统的瓶颈可能变成accept新tcp连接的速度,这时codis的性能可能会差于twemproxy。
redis cluster基于smart client和无中心的设计,client必须按key的哈希将请求直接发送到对应的节点。这意味着:使用官方cluster必须要等对应语言的redis driver对cluster支持的开发和不断成熟;client不能直接像单机一样使用pipeline来提高效率,想同时执行多个请求来提速必须在client端自行实现异步逻辑。 而codis因其有中心节点、基于proxy的设计,对client来说可以像对单机redis一样去操作proxy
Jodis-Client
Codis-Proxy
Codis-Group
Codis-Dashboard
Codis-Server
Codis-HA
Codis-Config
codis的部署步骤还是比较多的,大概有10步,不过按照一步步的步骤来就好,其中ha机制是要单独部署的.
wget https://storage.googleapis.com/golang/go1.4.1.linux-amd64.tar.gz
tar -zxvf go1.4.1.linux-amd64.tar.gz
mv go /usr/local/
cd /usr/local/go/src/
bash all.bash
cat >> ~/.bashrc << _bashrc_export
export GOROOT=/usr/local/go
export PATH=\$PATH:\$GOROOT/bin
export GOARCH=amd64
export GOOS=linux
_bashrc_export
source ~/.bashrc
codis(codis-config、codis-proxy、codis-server所在的机器)
mkdir /data/go
export GOPATH=/data/go
/usr/local/go/bin/go get github.com/wandoulabs/codis
cd /data/go/src/github.com/wandoulabs/codis/
./bootstrap.sh
make gotest
(codis-config上操作)
cat /etc/codis/config_10.ini ##撰写配置文件
zk=10.10.0.47:2181,10.10.0.48:2181,10.10.1.76:2181
product=zh_news
proxy_id=codis-proxy_10
net_timeout=5000
proto=tcp4
dashboard_addr=10.10.32.10:18087
1 cd /data/go/src/github.com/wandoulabs/codis/ && ./bin/codis-config -c /etc/codis/config_10.ini dashboard &
(codis-config上操作)
cd /data/go/src/github.com/wandoulabs/codis/ && ./bin/codis-config -c /etc/codis/config_10.ini slot init
和官方的Redis Server参数一样(codis-server上操作)
cd /data/go/src/github.com/wandoulabs/codis/ && ./bin/codis-server /etc/redis_6379.conf &
每一个 Server Group 作为一个 Redis 服务器组存在, 只允许有一个 master, 可以有多个 slave, group id 仅支持大于等于1的整数(codis-config上操作)
cd /data/go/src/github.com/wandoulabs/codis/
./bin/codis-config -c /etc/codis/config_10.ini server add 1 10.10.32.42:6379 master
./bin/codis-config -c /etc/codis/config_10.ini server add 1 10.10.32.43:6380 slave
./bin/codis-config -c /etc/codis/config_10.ini server add 2 10.10.32.43:6379 master
./bin/codis-config -c /etc/codis/config_10.ini server add 2 10.10.32.44:6380 slave
./bin/codis-config -c /etc/codis/config_10.ini server add 3 10.10.32.44:6379 master
./bin/codis-config -c /etc/codis/config_10.ini server add 3 10.10.32.42:6380 slave
Codis 采用 Pre-sharding 的技术来实现数据的分片, 默认分成 1024 个 slots (0-1023), 对于每个key来说, 通过以下公式确定所属的 Slot Id : SlotId = crc32(key) % 1024 每一个 slot 都会有一个特定的 server group id 来表示这个 slot 的数据由哪个 server group 来提供.(codis-config上操作)
cd /data/go/src/github.com/wandoulabs/codis/
./bin/codis-config -c /etc/codis/config_10.ini slot range-set 0 300 1 online
./bin/codis-config -c /etc/codis/config_10.ini slot range-set 301 700 2 online
./bin/codis-config -c /etc/codis/config_10.ini slot range-set 701 1023 3 online
(codis-proxy上操作)
cat /etc/codis/config_10.ini ##撰写配置文件
zk=10.10.0.47:2181,10.10.0.48:2181,10.10.1.76:2181
product=zh_news
proxy_id=codis-proxy_10 ##10.10.32.49上改成codis-proxy_49,多个proxy,proxy_id 需要唯一
net_timeout=5000
proto=tcp4
dashboard_addr=10.10.32.10:18087
1
2 cd /data/go/src/github.com/wandoulabs/codis/ && ./bin/codis-proxy -c /etc/codis/config_10.ini -L /data/log/codis-proxy_10.log --cpu=4 --addr=0.0.0.0:19000 --http-addr=0.0.0.0:11000 &
cd /data/go/src/github.com/wandoulabs/codis/ && ./bin/codis-proxy -c /etc/codis/config_49.ini -L /data/log/codis-proxy_49.log --cpu=4 --addr=0.0.0.0:19000 --http-addr=0.0.0.0:11000 &
OK,整个集群已经搭建成功了,访问[http://10.10.32.10:18087/admin]{.underline},可以看一下效果
odis-ha实现codis-server的主从切换,codis-server主库挂了会提升一个从库为主库,从库挂了会设置这个从库从集群下线
export GOPATH=/data/go
/usr/local/go/bin/go get github.com/ngaut/codis-ha
cd /data/go/src/github.com/ngaut/codis-ha
go build
cp codis-ha /data/go/src/github.com/wandoulabs/codis/bin/
使用方法:
codis-ha --codis-config=dashboard地址:18087 --productName=集群项目名称
使用supervisord管理codis-ha进程
yum -y install supervisord
/etc/supervisord.conf中添加如下内容:
[program:codis-ha]
autorestart = True
stopwaitsecs = 10
startsecs = 1
stopsignal = QUIT
command = /data/go/src/github.com/wandoulabs/codis/bin/codis-ha --codis-config=10.10.32.17:18087 --productName=zh_news
user = root
startretries = 3
autostart = True
exitcodes = 0,2
启动supervisord服务
/etc/init.d/supervisord start
chkconfig supervisord on
ps -ef |grep codis-ha 你回发现codis-ha进程已经启动,这个时候你去停掉一个codis-server的master,看看slave会不会提升为master
1 不支持随redis的升级而升级
2 codis集群内部通讯是通过主机名的,如果主机名没有做域名解析那dashboard是通过主机名访问不到proxy的http-addr地址的,这会导致从web界面上看不到 OP/s的数据
3 codis proxy 不支持热重启
在redis3.0以后的版本才支持该功能
Redis Cluster中,Sharding采用slot(槽)的概念,一共分成16384个槽,这有点儿类似前面讲的pre sharding思路。对于每个进入Redis的键值对,根据key进行散列,分配到这16384个slot中的某一个中。使用的hash算法也比较简单,就是CRC16后16384取模。Redis集群中的每个node(节点)负责分摊这16384个slot中的一部分,也就是说,每个slot都对应一个node负责处理。当动态添加或减少node节点时,需要将16384个槽做个再分配,槽中的键值也要迁移。当然,这一过程,在目前实现中,还处于半自动状态,需要人工介入。Redis集群,要保证16384个槽对应的node都正常工作,如果某个node发生故障,那它负责的slots也就失效,整个集群将不能工作。为了增加集群的可访问性,官方推荐的方案是将node配置成主从结构,即一个master主节点,挂n个slave从节点。这时,如果主节点失效,Redis Cluster会根据选举算法从slave节点中选择一个上升为主节点,整个集群继续对外提供服务。这非常类似服务器节点通过Sentinel监控架构成主从结构,只是Redis Cluster本身提供了故障转移容错的能力。
1 槽的总数为16384个
2 基于CRC16算法: 循环冗余校验码,是信息系统中一种常见的检错码
3 动态添加或者减少Node,需要人工介入
4 建议配置主从结构
192.168.0.11
192.168.0.12
192.168.0.13
每台服务器1主1从,共3主3从
相关安装包存储路径:/root/svr/
wget http://download.redis.io/releases/redis-3.2.9.tar.gz
tar xvf redis-3.2.9.tar.gz
cd redis-3.2.9
make install PREFIX=/root/svr/redis-3.2.9 安装
cd /usr/local/redis-3.2.9
创建集群配置文件夹:mkdir cluster-conf
cd cluster-conf
创建集群端口文件夹:mkdir 7001 mkdir 7002
cd 7001
复制配置文件:cp /root/svr/redis-3.2.9/redis.conf ./
Redis的log及持久化文件建议存储到磁盘空间较大的目录,本次存储路径:/root/svr/redis-cluster/
修改配置文件:vi redis.conf
port 7001
logfile "/root/svr/redis-3.2.9/cluster-conf/7001/redis.log"
dir /root/svr/redis-cluster/7001/ #事先创建好
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes
bind 0.0.0.0
复制redis.conf至7002并修改端口及存储路径
scp其他服务器
scp -r redis-3.2.9/ [email protected]:/root/svr/
/root/svr/redis-3.2.9/bin/redis-server /root/svr/redis-3.2.9/cluster-conf/7002/redis.conf &
./redis-trib.rb create --replicas 1 192.168.0.11:7001 192.168.0.12:7001 192.168.0.13:7001 192.168.0.11:7002 192.168.0.12:7002 192.168.0.13:7002
以上命令的意思就是让 redis-trib 程序创建一个包含三个主节点和三个从节点的集群。
命令的意义如下:
1、给定 redis-trib.rb 程序的命令是 create , 这表示我们希望创建一个新的集群。
2、选项 --replicas 1 表示我们希望为集群中的每个主节点创建一个从节点(百分比 选举master按先后顺序)
ps -ef |grep redis
或者netsta -tnlp |grep redis
./redis-cli -c -p 7001
表示安装成功了
增加
./redis-trib.rb add-node 127.0.0.1:7006 127.0.0.1:7000
从节点(masterid 和被加的节点)
./redis-trib.rb add-node --slave masterid 192.168.0.11:7002
移除
./redis-trib del-node 127.0.0.1:7000 `<node-id>`
关闭服务:./redis-cli -h 192.168.0.11 -p 7001 shutdown
删除:rm -rf /root/svr/redis-cluster/7001/*
1 集群启动脚本语言依赖于ruby,所以要安装yum install rubygems
2 修改redis.conf里面的bind为0.0.0.0
无中心架构。
数据按照slot存储分布在多个节点,节点间数据共享,可动态调整数据分布。
可扩展性,可线性扩展到1000个节点,节点可动态添加或删除。
高可用性,部分节点不可用时,集群仍可用。通过增加Slave做standby数据副本,能够实现故障自动failover,节点之间通过gossip协议交换状态信息,用投票机制完成Slave到Master的角色提升。
降低运维成本,提高系统的扩展性和可用性。
严重依赖外部Redis-Trib
缺乏监控管理
需要依赖Smart Client(连接维护, 缓存路由表, MultiOp和Pipeline支持)
Failover节点的检测过慢,不如"中心节点ZooKeeper"及时
Gossip消息的开销
无法根据统计区分冷热数据
Slave"冷备",不能缓解读压力
直连型,又可以称之为经典型或者传统型,是官方的默认使用方式,架构图见图6。这种使用方式的优缺点在上面的介绍中已经有所说明,这里不再过多重复赘述。但值得一提的是,这种方式使用Redis Cluster需要依赖Smart Client,诸如连接维护、缓存路由表、MultiOp和Pipeline的支持都需要在Client上实现,而且很多语言的Client目前都还是没有的
优点
无中心节点
数据按照Slot存储分布在多个Redis实例上
平滑的进行扩容/缩容节点
自动故障转移(节点之间通过Gossip协议交换状态信息,进行投票机制完成Slave到Master角色的提升)
降低运维成本,提高了系统的可扩展性和高可用性
缺点
严重依赖外部Redis-Trib
缺乏监控管理
需要依赖Smart Client(连接维护, 缓存路由表, MultiOp和Pipeline支持)
Failover节点的检测过慢,不如“中心节点ZooKeeper”及时
Gossip消息的开销
无法根据统计区分冷热数据
Slave“冷备”,不能缓解读压力
在Redis Cluster还没有那么稳定的时候,很多公司都已经开始探索分布式Redis的实现了,比如有基于Twemproxy或者Codis的实现
优点:
后端Sharding逻辑对业务透明,业务方的读写方式和操作单个Redis一致;
可以作为Cache和Storage的Proxy,Proxy的逻辑和Redis资源层的逻辑是隔离的;
Proxy层可以用来兼容那些目前还不支持的Clients。
缺点:
结构复杂,运维成本高;
可扩展性差,进行扩缩容都需要手动干预;
failover逻辑需要自己实现,其本身不能支持故障的自动转移;
Proxy层多了一次转发,性能有所损耗。
目前业界Smart Proxy的方案了解到的有基于Nginx Proxy和自研的,自研的如饿了么开源部分功能的Corvus,优酷土豆是则通过Nginx来实现,滴滴也在展开基于这种方式的探索。选用Nginx Proxy主要是考虑到Nginx的高性能,包括异步非阻塞处理方式、高效的内存管理、和Redis一样都是基于epoll事件驱动模式等优点。优酷土豆的Redis服务化就是采用这种结构。
优点:
提供一套HTTP Restful接口,隔离底层资源,对客户端完全透明,跨语言调用变得简单;
升级维护较为容易,维护Redis Cluster,只需平滑升级Proxy;
层次化存储,底层存储做冷热异构存储;
权限控制,Proxy可以通过密钥管理白名单,把一些不合法的请求都过滤掉,并且也可以对用户请求的超大value进行控制和过滤;
安全性,可以屏蔽掉一些危险命令,比如keys *、save、flushall等,当然这些也可以在Redis上进行设置;
资源逻辑隔离,根据不同用户的key加上前缀,来实现动态路由和资源隔离;
监控埋点,对于不同的接口进行埋点监控。
缺点:
Proxy层做了一次转发,性能有所损耗;
增加了运维成本和管理成本,需要对架构和Nginx Proxy的实现细节足够了解,因为Nginx Proxy在批量接口调用高并发下可能会瞬间向Redis Cluster发起几百甚至上千的协程去访问,导致Redis的连接数或系统负载的不稳定,进而影响集群整体的稳定性。
目前都是通过Proxy+RedisCluster,这种结构,其中proxy一般会使用go语言进行开发或者使用高性能的nginx proxy框架开发
1. 用户在ACL平台申请集群资源,如果申请成功返回秘钥信息。
2. 用户请求接口必须包含申请的秘钥信息,请求至LVS服务器。
3. LVS根据负载均衡策略将请求转发至Nginx Proxy。
4. Nginx Proxy首先会获取秘钥信息,然后根据秘钥信息去ACL服务上获取集群的种子信息。(种子信息是集群内任意几台IP:PORT节点)
5. 然后把秘钥信息和对应的集群种子信息缓存起来。并且第一次访问会根据种子IP:PORT获取集群Slot对应节点的Mapping路由信息,进行缓存起来。最后根据Key计算SlotId,从缓存路由找到节点信息。
6. 把相应的K/V信息发送到对应的Redis节点上。
7. Nginx Proxy定时(60s)上报请求接口埋点的QPS,RT,Err等信息到Open-Falcon平台。
8. Redis Cluster定时(60s)上报集群相关指标的信息到Open-Falcon平台。
备注:client(key)->lvs->nginx proxy->routing->redis->slots
根据用户Post数据获取该用户申请的NameSpace,然后以NameSpace作为该用户请求Key的前缀,从而达到不同用户的不同NameSpace,进行逻辑资源隔离
针对后端Redis节点出现Moved,Ask,Err,TimeOut等进行重试,重试次数可配置
通过在Nginx Proxy Limit模块进行限速,超过集群的承载能力,进行过载保护。从而保证部分用户可用,不至于压垮服务器
Nginx Proxy接入了Open-Falcon对系统级别,应用级别,业务级别进行监控和告警
由于Redis Cluster是通过Gossip通信, 超过半数以上Master节点通信(cluster-node-timeout)认为当前Master节点宕机,才真的确认该节点宕机。判断节点宕机时间过长,在Proxy层加入Raft算法,加快失效节点判定,主动Failover
主要是针对像mget,mset这种批量的查询或者插入进行了很多的优化,因为默认的rediscluster对multiop和pipeline的支持有限,在这里主要使用了nginx的特性进行很多方面的优化.
####### 子请求变为协程(轻量级的线程)
优化前:
a) 用户请求mget(k1,k2)到Proxy
b) Proxy根据k1,k2分别发起子请求subrequest1,subrequest2
c) 子请求根据key计算slotid,然后去缓存路由表查找节点
d) 子请求请求Redis Cluster的相关节点,然后响应返回给Proxy
e) Proxy会合并所有的子请求返回的结果,然后进行解析包装返回给用户
优化后:
a) 用户请求mget(k1,k2)到Proxy
b) Proxy根据k1,k2分别计算slotid, 然后去缓存路由表查找节点
c) Proxy发起多个协程coroutine1, coroutine2并发的请求Redis Cluster的相关节点
d) Proxy会合并多个协程返回的结果,然后进行解析包装返回给用户
####### 合并相同槽,批量执行命令,减少网络开销
优化前:
a) 用户请求mget(k1,k2,k3,k4) 到Proxy。
b) Proxy会解析请求串,然后计算k1,k2,k3,k4所对应的slotid。
c) Proxy会根据slotid去路由缓存中找到后端服务器的节点,并发的发起多个请求到后端服务器。
d) 后端服务器返回结果给Proxy,然后Proxy进行解析获取key对应的value。
e) Proxy把key,value对应数据包装返回给用户。
优化后:
a) 用户请求mget(k1,k2,k3,k4) 到Proxy。
b) Proxy会解析请求串,然后计算k1,k2,k3,k4所对应的slotid,然后把相同的slotid进行合并为一次Pipeline请求。
c) Proxy会根据slotid去路由缓存中找到后端服务器的节点,并发的发起多个请求到后端服务器。
d) 后端服务器返回结果给Proxy,然后Proxy进行解析获取key对应的value。
e) Proxy把key,value对应数据包装返回给用户。
####### 请求并发度的合理控制
优化前
a) 用户请求批量接口mset(200个key)。(这里先忽略合并相同槽的逻辑)
b) Proxy会解析这200个key,会同时发起200个协程请求并发的去请求Redis Cluster。
c) Proxy等待所有协程请求完成,然后合并所有协程请求的响应结果,进行解析,包装返回给用户。
优化后
a) 用户请求批量接口mset(200个key)。 (这里先忽略合并相同槽的逻辑)
b) Proxy会解析这200个key,进行分组。100个key为一组,分批次进行并发请求。
c) Proxy先同时发起第一组100个协程(coroutine1, coroutine100)请求并发的去请求Redis Cluster。
d) Proxy等待所有协程请求完成,然后合并所有协程请求的响应结果。
e) Proxy然后同时发起第二组100个协程(coroutine101, coroutine200)请求并发的去请求Redis Cluster。
f) Proxy等待所有协程请求完成,然后合并所有协程请求的响应结果。
g) Proxy把所有协程响应的结果进行解析,包装,返回给用户。
####### 单work分散多work
优化前
a) 用户请求批量接口mset(200个key)。(这里先忽略合并相同槽的逻辑)
b) Proxy会解析这200个key,会同时发起200个协程请求并发的去请求Redis Cluster。
c) Proxy等待所有协程请求完成,然后合并所有协程请求的响应结果,进行解析,包装返回给用户。
优化后
a) 用户请求批量接口mset(200个key)。(这里先忽略合并相同槽的key的逻辑)
b) Proxy会解析这200个key,然后进行拆分分组以此来控制并发度。
c) Proxy会根据划分好的组进行一组一组的发起请求。
d) Proxy等待所有请求完成,然后合并所有协程请求的响应结果,进行解析,包装返回给用户。
4.1 系统级别
通过Open-Falcon Agent采集服务器的CPU、内存、网卡流量、网络连接、磁盘等信息
4.2 应用级别
通过Open-Falcon Plugin采集Nginx/Redis进程级别的CPU,内存,Pid等信息。
4.3 业务级别
通过在Proxy里面埋点监控业务接口QPS,RT(50%,99%,999%),请求流量,错误次数等信息,定时的上报给Open-Falcon
备注: 深入理解Nginx模块开发与架构解析,这本书对掌握Nginx模块开发有很好的讲解,可以去看一下.
redis-rebalance.rb进行扩展开发。运维非常方便快捷
redis-migrate-tool是唯品会开源针对redis数据运维工具
redis-cluster-tool
思考,如果自己设计一个类似的代理集群,有哪些核心东西和组件需要思考
角色:
Client: gcache客户端
Proxy: 访问redis的代理,负责分发请求
Redis Group: redis的副本集(M-M,M-S)
Monitor: redis的监控
Zookeeper: 存储路由信息
高可用:
Proxy至少一个节点存活,可随意加减
Zookeeper可全部宕,proxy缓存了路由
Redis Group中至少存活一个节点
Monitor 双机热备
伸缩性:
Proxy可动态加机器,client通过zk感知
Group动态增加,扩展集群/新业务存储
Group内redis动态增加,应对读多的操作
按照规则定时把内存里面的数据保存到磁盘
save <seconds> <changes>
save 900 1 当在900秒内被更改的key的数量大于1的时候,就执行快照
save 300 10
save 60 10000
save: 执行内存的数据同步到磁盘的操作,这个操作会阻塞客户端的请求
bgsave: 在后台异步执行快照操作,这个操作不会阻塞客户端的请求
redis使用fork函数复制一份当前进程的副本(子进程)
父进程继续接收并处理客户端发来的命令,而子进程开始将内存中的数据写入硬盘中的临时文件
当子进程写入完所有数据后会用该临时文件替换旧的RDB文件,至此,一次快照操作完成
备注
redis在进行快照的过程中不会修改RDB文件,只有快照结束后才会将旧的文件替换成新的,也就是说任何时候RDB文件都是完整的。 这就使得我们可以通过定时备份RDB文件来实现redis数据库的备份, RDB文件是经过压缩的二进制文件,占用的空间会小于内存中的数据,更加利于传输
(1)RDB会生成多个数据文件,每个数据文件都代表了某一个时刻中redis的数据,这种多个数据文件的方式,非常适合做冷备,可以将这种完整的数据文件发送到一些远程的安全存储上去,比如说Amazon的S3云服务上去,在国内可以是阿里云的ODPS分布式存储上,以预定好的备份策略来定期备份redis中的数据
(2)RDB对redis对外提供的读写服务,影响非常小,可以让redis保持高性能,因为redis主进程只需要fork一个子进程,让子进程执行磁盘IO操作来进行RDB持久化即可
(3)相对于AOF持久化机制来说,直接基于RDB数据文件来重启和恢复redis进程,更加快速
(1)如果想要在redis故障时,尽可能少的丢失数据,那么RDB没有AOF好。一般来说,RDB数据快照文件,都是每隔5分钟,或者更长时间生成一次,这个时候就得接受一旦redis进程宕机,那么会丢失最近5分钟的数据
(2)RDB每次在fork子进程来执行RDB快照数据文件生成的时候,如果数据文件特别大,可能会导致对客户端提供的服务暂停数毫秒,或者甚至数秒
AOF可以将Redis执行的每一条写命令追加到硬盘文件中,这一过程显然会降低Redis的性能,但大部分情况下这个影响是能够接受的,另外使用较快的硬盘可以提高AOF的性能
Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写: 重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。 整个重写操作是绝对安全的,因为 Redis 在创建新 AOF 文件的过程中,会继续将命令追加到现有的 AOF 文件里面,即使重写过程中发生停机,现有的 AOF 文件也不会丢失。 而一旦新 AOF 文件创建完毕,Redis 就会从旧 AOF 文件切换到新 AOF 文件,并开始对新 AOF 文件进行追加操作。AOF 文件有序地保存了对数据库执行的所有写入操作, 这些写入操作以 Redis 协议的格式保存, 因此 AOF 文件的内容非常容易被人读懂, 对文件进行分析(parse)也很轻松
redis每次更改数据的时候, aof机制都会讲命令记录到aof文件,但是实际上由于操作系统的缓存机制,数据并没有实时写入到硬盘,而是进入硬盘缓存。再通过硬盘缓存机制去刷新到保存到文件
appendfsync always 每次执行写入都会进行同步 , 这个是最安全但是是效率比较低的方式
appendfsync everysec 每一秒执行
appendfsync no 不主动进行同步操作,由操作系统去执行,这个是最快但是最不安全的方式
1)AOF可以更好的保护数据不丢失,一般AOF会每隔1秒,通过一个后台线程执行一次fsync操作,最多丢失1秒钟的数据
(2)AOF日志文件以append-only模式写入,所以没有任何磁盘寻址的开销,写入性能非常高,而且文件不容易破损,即使文件尾部破损,也很容易修复
(3)AOF日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写。因为在rewrite
log的时候,会对其中的指导进行压缩,创建出一份需要恢复数据的最小日志出来。再创建新日志文件的时候,老的日志文件还是照常写入。当新的merge后的日志文件ready的时候,再交换新老日志文件即可。
(4)AOF日志文件的命令通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复。比如某人不小心用flushall命令清空了所有数据,只要这个时候后台rewrite还没有发生,那么就可以立即拷贝AOF文件,将最后一条flushall命令给删了,然后再将该AOF文件放回去,就可以通过恢复机制,自动恢复所有数据
(1)对于同一份数据来说,AOF日志文件通常比RDB数据快照文件更大
(2)AOF开启后,支持的写QPS会比RDB支持的写QPS低,因为AOF一般会配置成每秒fsync一次日志文件,当然,每秒一次fsync,性能也还是很高的
(3)以前AOF发生过bug,就是通过AOF记录的日志,进行数据恢复的时候,没有恢复一模一样的数据出来。所以说,类似AOF这种较为复杂的基于命令日志/merge/回放的方式,比基于RDB每次持久化一份完整的数据快照文件的方式,更加脆弱一些,容易有bug。不过AOF就是为了避免rewrite过程导致的bug,因此每次rewrite并不是基于旧的指令日志进行merge的,而是基于当时内存中的数据进行指令的重新构建,这样健壮性会好很多。
1)不要仅仅使用RDB,因为那样会导致你丢失很多数据
(2)也不要仅仅使用AOF,因为那样有两个问题,第一,你通过AOF做冷备,没有RDB做冷备,来的恢复速度更快; 第二,RDB每次简单粗暴生成数据快照,更加健壮,可以避免AOF这种复杂的备份和恢复机制的bug
(3)综合使用AOF和RDB两种持久化机制,用AOF来保证数据不丢失,作为数据恢复的第一选择; 用RDB来做不同程度的冷备,在AOF文件都丢失或损坏不可用的时候,还可以使用RDB来进行快速的数据恢复
备注:如果同时使用RDB和AOF两种持久化机制,那么在redis重启的时候,会使用AOF来重新构建数据,因为AOF中的数据更加完整
目前几乎很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题。分布式的CAP理论告诉我们"任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。“所以,很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证"最终一致性”,只要这个最终时间是在用户可以接受的范围内即可。
在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。
Redis有很高的性能
Redis命令对此支持较好,实现起来比较方便
SETNX
SETNX key val
当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0。
expire
expire key timeout
为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。
delete
delete key
删除key
• 获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。
• 获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
• 释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放
一般封装一个工具类,提供两个方法一个是获取锁,一个是释放锁
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.Transaction;
import redis.clients.jedis.exceptions.JedisException;
import java.util.List;
import java.util.UUID;
public class DistributedLock {
private final JedisPool jedisPool;
public DistributedLock(JedisPool jedisPool) {
this.jedisPool = jedisPool;
}
/**
* 加锁
* @param locaName 锁的key
* @param acquireTimeout 获取超时时间
* @param timeout 锁的超时时间
* @return 锁标识
*/
public String getLock(String locaName, long acquireTimeout, long timeout) {
Jedis conn = null;
String retIdentifier = null;
try {
// 获取连接
conn = jedisPool.getResource();
// 随机生成一个value
String identifier = UUID.randomUUID().toString();
// 锁名,即key值
String lockKey = "lock:" + locaName;
// 超时时间,上锁后超过此时间则自动释放锁
int lockExpire = (int)(timeout / 1000);
// 获取锁的超时时间,超过这个时间则放弃获取锁
long end = System.currentTimeMillis() + acquireTimeout;
while (System.currentTimeMillis() < end) {
if (conn.setnx(lockKey, identifier) == 1) {
conn.expire(lockKey, lockExpire);
// 返回value值,用于释放锁时间确认
retIdentifier = identifier;
return retIdentifier;
}
// 返回-1代表key没有设置超时时间,为key设置一个超时时间
if (conn.ttl(lockKey) == -1) {
conn.expire(lockKey, lockExpire);
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
} catch (JedisException e) {
e.printStackTrace();
} finally {
if (conn != null) {
conn.close();
}
}
return retIdentifier;
}
/**
* 释放锁
* @param lockName 锁的key
* @param identifier 释放锁的标识
* @return
*/
public boolean releaseLock(String lockName, String identifier) {
Jedis conn = null;
String lockKey = "lock:" + lockName;
boolean retFlag = false;
try {
conn = jedisPool.getResource();
while (true) {
// 监视lock,准备开始事务
conn.watch(lockKey);
// 通过前面返回的value值判断是不是该锁,若是该锁,则删除,释放锁
if (identifier.equals(conn.get(lockKey))) {
Transaction transaction = conn.multi();
transaction.del(lockKey);
List<Object> results = transaction.exec();
if (results == null) {
continue;
}
retFlag = true;
}
conn.unwatch();
break;
}
} catch (JedisException e) {
e.printStackTrace();
} finally {
if (conn != null) {
conn.close();
}
}
return retFlag;
}
}
备注:比如超时时间的选取,获取锁时间的选取对并发量都有很大的影响,所以在使用redis做分布式锁的时候还是有很多细节需要考虑的,如果对性能不是非常高,可以使用ZK来做.
当然可以不选用基于Jedis的,还有一个叫Redission专门封装了基于redis的常用分布式锁操作,使用起来更加简单.
redis是一个cs模式的tcp server,使用和http类似的请求响应协议。一个client可以通过一个socket连接发起多个请求命令。每个请求命令发出后client通常会阻塞并等待redis服务处理,redis处理完后请求命令后会将结果通过响应报文返回给client
利用pipeline的方式从client打包多条命令一起发出,不需要等待单条命令的响应返回,而redis服务端会处理完多条命令后会将多条命令的处理结果打包到一起返回给客户端
备注:对于使用redis的java客户端,只有Jedis,ShardJedis支持管道,但JedisCluster不支持管道技术,当然我们可以分析已实现的Pipeline机制,来扩展我们自己的实现,下面就分析一下如何实现基于集群版的Pipeline。
pipeline模式下命令将被缓存到对应的连接(OutputStream)上,而在真正向服务端发送数据时,节点可能发生了改变,数据就可能发向了错误的节点,这导致批量操作失败,而要处理这种失败是非常复杂的
package com.springboot.redis.pipeline;
import org.apache.log4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.*;
import redis.clients.jedis.exceptions.JedisMovedDataException;
import redis.clients.jedis.exceptions.JedisRedirectionException;
import redis.clients.util.JedisClusterCRC16;
import redis.clients.util.SafeEncoder;
import java.io.Closeable;
import java.lang.reflect.Field;
import java.util.*;
public class JedisClusterPipeline extends PipelineBase implements Closeable {
private static final String SPLIT_WORD = ":";
// 部分字段没有对应的获取方法,只能采用反射来做
// 你也可以去继承JedisCluster和JedisSlotBasedConnectionHandler来提供访问接口
private static final Field FIELD_CONNECTION_HANDLER;
protected static final Field FIELD_CACHE;
static {
FIELD_CONNECTION_HANDLER = getField(BinaryJedisCluster.class, "connectionHandler");
FIELD_CACHE = getField(JedisClusterConnectionHandler.class, "cache");
}
private JedisSlotBasedConnectionHandler connectionHandler;
private JedisClusterInfoCache clusterInfoCache;
private Queue<Client> clients = new LinkedList<Client>(); // 根据顺序存储每个命令对应的Client
private Map<JedisPool, Map<Long, Jedis>> jedisMap = new HashMap<JedisPool, Map<Long, Jedis>>(); // 用于缓存连接
private boolean hasDataInBuf = false; // 是否有数据在缓存区
public JedisClusterPipeline(JedisCluster jedisCluster) {
connectionHandler = getValue(jedisCluster, FIELD_CONNECTION_HANDLER);
clusterInfoCache = getValue(connectionHandler, FIELD_CACHE);
}
/**
* 刷新集群信息,当集群信息发生变更时调用
*
* @param
* @return
*/
public void refreshCluster() {
connectionHandler.renewSlotCache();
}
/**
* 同步读取所有数据. 与syncAndReturnAll()相比,sync()只是没有对数据做反序列化
*/
public void sync() {
innerSync(null);
}
@Override
public void close() {
clean();
clients.clear();
for (Map.Entry<JedisPool, Map<Long, Jedis>> poolEntry : jedisMap.entrySet()) {
for (Map.Entry<Long, Jedis> jedisEntry : poolEntry.getValue().entrySet()) {
if (hasDataInBuf) {
flushCachedData(jedisEntry.getValue());
}
jedisEntry.getValue().close();
}
}
jedisMap.clear();
hasDataInBuf = false;
}
/**
* 同步读取所有数据 并按命令顺序返回一个列表
*
* @return 按照命令的顺序返回所有的数据
*/
public List<Object> syncAndReturnAll() {
List<Object> responseList = new ArrayList<Object>();
innerSync(responseList);
return responseList;
}
private void innerSync(List<Object> formatted) {
HashSet<Client> clientSet = new HashSet<Client>();
try {
for (Client client : clients) {
// 在sync()调用时其实是不需要解析结果数据的,但是如果不调用get方法,发生了JedisMovedDataException这样的错误应用是不知道的,因此需要调用get()来触发错误。
// 其实如果Response的data属性可以直接获取,可以省掉解析数据的时间,然而它并没有提供对应方法,要获取data属性就得用反射,不想再反射了,所以就这样了
Object data = generateResponse(client.getOne()).get();
if (null != formatted) {
formatted.add(data);
}
// size相同说明所有的client都已经添加,就不用再调用add方法了
if (clientSet.size() != jedisMap.size()) {
clientSet.add(client);
}
}
} catch (JedisRedirectionException jre) {
if (jre instanceof JedisMovedDataException) {
// if MOVED redirection occurred, rebuilds cluster's slot cache,
// recommended by Redis cluster specification
refreshCluster();
}
throw jre;
} finally {
if (clientSet.size() != jedisMap.size()) {
// 所有还没有执行过的client要保证执行(flush),防止放回连接池后后面的命令被污染
for (Map.Entry<JedisPool, Map<Long, Jedis>> poolEntry : jedisMap.entrySet()) {
for (Map.Entry<Long, Jedis> jedisEntry : poolEntry.getValue().entrySet()) {
if (clientSet.contains(jedisEntry.getValue().getClient())) {
continue;
}
flushCachedData(jedisEntry.getValue());
}
}
}
hasDataInBuf = false;
close();
}
}
private void flushCachedData(Jedis jedis) {
try {
jedis.getClient().getAll();
} catch (RuntimeException ex) {
}
}
@Override
protected Client getClient(String key) {
byte[] bKey = SafeEncoder.encode(key);
return getClient(bKey);
}
@Override
protected Client getClient(byte[] key) {
Jedis jedis = getJedis(JedisClusterCRC16.getSlot(key));
Client client = jedis.getClient();
clients.add(client);
return client;
}
private Jedis getJedis(int slot) {
// 根据线程id从缓存中获取Jedis
Jedis jedis = null;
Map<Long, Jedis> tmpMap = null;
//获取线程id
long id = Thread.currentThread().getId();
//获取jedispool
JedisPool pool = clusterInfoCache.getSlotPool(slot);
if (jedisMap.containsKey(pool)) {
tmpMap = jedisMap.get(pool);
if (tmpMap.containsKey(id)) {
jedis = tmpMap.get(id);
} else {
jedis = pool.getResource();
tmpMap.put(id, jedis);
}
} else {
tmpMap = new HashMap<Long, Jedis>();
jedis = pool.getResource();
tmpMap.put(id, jedis);
jedisMap.put(pool,tmpMap);
}
hasDataInBuf = true;
return jedis;
}
private static Field getField(Class<?> cls, String fieldName) {
try {
Field field = cls.getDeclaredField(fieldName);
field.setAccessible(true);
return field;
} catch (NoSuchFieldException | SecurityException e) {
throw new RuntimeException("cannot find or access field '" + fieldName + "' from " + cls.getName(), e);
}
}
@SuppressWarnings({"unchecked"})
private static <T> T getValue(Object obj, Field field) {
try {
return (T) field.get(obj);
} catch (IllegalArgumentException | IllegalAccessException e) {
System.out.println("get value fail");
throw new RuntimeException(e);
}
}
}
这个实现主要参考了Jedis内置的ShardedJedisPipeline.java
它提供了redis三种操作方式,分别是基于单机模式,分片模式,集群模式
单机模式,就是Jedis
分片模式,就是ShardedJedis
集群模式,就是JedisCluster
其实它的本质就是封装了一个Socket,通过它来请求redis服务,并返回结果.
核心类分析
JedisCommands:定义redis常用命令
MultikeyCommands:定义redis批量请求命令
AdvancedJedisCommands:定义redis高级请求命令,比如慢日志查看
ScriptingCommands:定义redis对脚本的支持,这里主要是lua脚本
BasicCommands,定义对redis一些基本属性的命令,主要是ping,查看库,同步数据等
ClusterCommands:定义查看集群相关信息
SentinelCommands:定义哨兵相关监控方法
BinaryJedis,它是封装了所有实现的方法,留给其它子类直接继承使用
Client:BinaryJedis直接调用它,它继承与BinaryClient,都是有这个方法实现的
BinaryClient:调用Connection的sendCommand方法来执行最终请求命令的发送
Connection:通过封装Socket的客户端请求,同时实现了具体的sendCommand发送方法.
Connect方法就是创建socket连接
Protocol.sendCommand方法就是封装了一些请求头,然后写入信息
通过上面流程分析,jedis本身就是线程不安全的,所以后面引入JedisPool来优化Jedis的连接数.
MultikeyPipelineBase:继承与PipelineBase,它本身就是一个抽象类,直接透传client对象给父类
Pipeline:继承与上面这个类,且实现了setClient和getClient方法,主要就这个方法
备注:管道的技术就是等所有命令都执行完毕,会一次性把结果读取出来,返回给客户端,从而可以让命令之间的操作是异步的.
只能支持单redis实例
Jedis,单机非并发
JedisPool,单机并发
Pipeline,单机管道技术
一个实例挂了,整个集群都不可用,底层使用Hash,不好扩容
ShardedJedis,非并发多个数据分片
SharedJedisPool,并发,多个数据分片
ShardedJedisPipeline,使用分片管道技术
支持集群模式,但是自身提供对管道的支持,需要开发者自己扩展
在使用redis过程中,如果遭遇攻击或者高并发下,很容易出现一些十分可怕的问题,如果不及时处理,可能导致整个集群不可用.
这种情况,一般是多个线程同时查询缓存,因为缓存没有及时查询到值,而会查询数据库,量大了,会把数据库给拖垮.常用的解决方案有以下几种
分布式锁
本地锁
软过期
其中加锁的话,要注意锁的粒度,且可以在查询DB之前,再一次判断缓存中是否有值,核心代码如下,不要直接在方法上加synchroized,这样太粗了,耗性能
这种一般是系统遭遇到攻击,一直查询一个数据库不存在的值,从而击垮数据库.解决方案如下
参数校验,比如key的格式是否合法
参数正确的情况下,可以把value设置为null
可以适当使用Filter,进行过滤
数据量比较大的情况下,可以使用BoolmFilter进行过滤
(不存在的肯定能判断出来,存在的有可能会判断不存在)
这种主要是因为在某段时间内,所有的key都失效了,从而导致所有的查询都透传到DB,让DB彻底跨了,一般解决方案如下.
Key失效时间设置成随机数(增加盐),均匀点,避免同一时间内,有大量的key失效
查询key加分布式锁
key永不过期
把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期
为每个 value 设置一个逻辑过期时间,当发现超过逻辑过期时间后,会使用单独的线程去构建缓存。
从实战看,这种方法对于性能非常友好,唯一不足的就是构建缓存时候,其余线程(非构建缓存的线程)可能访问的是老数据,但是对于一般的互联网功能来说这个还是可以忍受
这种情况主要是产生在缓存里面的数据和db的数据有时候会不完全一致,实际中也是要根据业务需求,来选择适当的解决方案,通常我们说的一致性就分为两种,一种是强一致性,一种是弱一致性(最终一致性)。一般解决方案如下
在写库前后都进行redis.del(key)操作,并且设定合理的超时时间。
具体的步骤
具体的步骤就是:
1)先删除缓存
2)再写数据库
3)休眠500毫秒
4)再次删除缓存
主要技术架构如下
读取binlog后分析 ,利用消息队列,推送更新各台的redis缓存数据。
这样一旦MySQL中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息推送至Redis,Redis再根据binlog中的记录,对Redis进行更新。
其实这种机制,很类似MySQL的主从备份机制,因为MySQL的主备也是通过binlog来实现的数据一致性。
这里可以结合使用canal(阿里的一款开源框架),通过该框架可以对MySQL的binlog进行订阅,而canal正是模仿了mysql的slave数据库的备份请求,使得Redis的数据更新达到了相同的效果。
当然,这里的消息推送工具你也可以采用别的第三方:kafka、rabbitMQ等来实现推送更新Redis。
(一般数据库都是基于mysql,binglog+cancal+kafka+redis/db:maxwell)
首先要想办法通过手段识别出哪些是热点数据.
客户端热点key缓存:将热点key对应value并缓存在客户端本地,并且设置一个失效时间。对于每次读请求,将首先检查key是否存在于本地缓存中,如果存在则直接返回,如果不存在再去访问分布式缓存的机器。
将热点key分散为多个子key,然后存储到缓存集群的不同机器上,这些子key对应的value都和热点key是一样的。当通过热点key去查询数据时,通过某种hash算法随机选择一个子key,然后再去访问缓存机器,将热点分散到了多个子key上。
Reids 内部采用多级 LRU 的数据结构,通过将访问数据 Key 的频率和大小设定不同权值,从而放到不同层级的 LRU 上。这样淘汰时可以确保权值高的那批 Key 得到保留,最终保留下来且超过阈值设定的就会判断为热点 Key。
当发现热点后,应用服务器和 redis 服务端就会联动起来,根据预先设定好的访问模型,将热点数据动态散列到 redis 服务端其他数据节点的 Hot Zone 存储区域去访问。
Bloom Filter是一种空间效率很高的随机数据结构,它利用位数组很简洁地表示一个集合,并能判断一个元素是否属于这个集合。Bloom Filter的这种高效是有一定代价的:在判断一个元素是否属于某个集合时,有可能会把不属于这个集合的元素误认为属于这个集合(false positive)。因此,Bloom Filter不适合那些"零错误"的应用场合。而在能容忍低错误率的应用场合下,Bloom Filter通过极少的错误换取了存储空间的极大节省。
下面我们具体来看Bloom Filter是如何用位数组表示集合的。初始状态时,Bloom Filter是一个包含m位的位数组,每一位都置为0。
为了表达S={x1, x2,…,xn}这样一个n个元素的集合,Bloom Filter使用k个相互独立的哈希函数(Hash Function),它们分别将集合中的每个元素映射到{1,…,m}的范围中。对任意一个元素x,第i个哈希函数映射的位置hi(x)就会被置为1(1≤i≤k)。注意,如果一个位置多次被置为1,那么只有第一次会起作用,后面几次将没有任何效果。在下图中,k=3,且有两个哈希函数选中同一个位置(从左边数第五位)。
在判断y是否属于这个集合时,我们对y应用k次哈希函数,如果所有hi(y)的位置都是1(1≤i≤k),那么我们就认为y是集合中的元素,否则就认为y不是集合中的元素。下图中*y1*就不是集合中的元素。y2或者属于这个集合,或者刚好是一个false positive。
备注: 它实际上是由一个很长的二进制向量和一系列随机映射函数组成
Redis里面已经提供了自带的实现
一般对于大数据量进行去重,首先要计算一下转换为位存储之后的内存,可以使用如下的计算公式
bitSize = (int) Math.ceil(maxKey * (Math.log(errorRate) / Math.log(0.6185)));
保证errorRate不变的前提下,bloomfilter 的maxKey越大,bloomfilter所需要的内存也就越大
为了更好的提供缓存服务,一般会对缓存进行预热,避免系统一开始就查询数据,这个可以根据的业务场景和数据量进行选择
数据量不大,可以在项目启动的时候自动进行加载;
如果数据量很大的话,可以把高频率词全部加载到内存
如果实时性高且并发高的话(建立实时热点统计系统),可以结合nginx+lua,之后上报到kafka,然后用storm进行实时消费,计算完结果同步到redis或者内存结构中,如果是分布式还要考虑锁的问题.
Lua是一种脚本语言,使用简单且语法比较灵活,而且redis内置就提供了对它的支持,实际业务场景有很多都可以直接使用redis+lua进行解决,最典型的应用场景,比如用户访问限制,统计等。现在比如限制某个IP,1分钟之内只能访问10次
使用方式有两种,
备注:语法为 ./redis-cli --eval [lua脚本] [key…]空格,空格[args…],且lua接受参数的类型是集合.
批量查询使用Pipeline
禁止使用耗性能的相关命令,比如keys,flushall,bgsave,monitor
禁止系统中存在过大的KEY或者VALUE
尽量合理设置每个KEY的有效期
常见问题
redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: Read timed out。
解决方案:查看网络以及慢日志,看是否有存在比较耗时的命令,比如keys
redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool
解决方案,合理设置timoue和tcp-keepavlive属性的值
Can’t save in background: fork: Cannot allocate memory
解决方案,修改Linux相关内核参数. vm.overcommit_memory=1
缓存内容的大小
缓存内容的数量
缓存的淘汰策略
缓存的数据结构
每秒读取的峰值
每秒写入的峰值
线程模型
缓存分片
缓存预热
缓存并发
缓存失效
缓存穿透
失效转移
持久化
复制模型
集群模型
缓存重建
缓存服务监控
缓存容量监控
缓存请求监控
缓存响应时间监控
是否有可能发生缓存穿透,雪崩,并发
是否有大key,大value
是否支持Lua的脚本
是否避免了竞争条件-资源冲突
缓存是我们在解决高并发情况下绝对不可少的一种技术,但是缓存的范围是很广的,并不是唯独指redis,encache,memcache等,从架构的角度来说,缓存可以分为全局缓存架构和部分缓存架构,下面分析一下这两种
作为一个架构师,考虑缓存设计或者应用的时候,需要从整体考虑缓存的使用,这一般包括前端+后端+数据库,换言之就是各个地方都有可能存在缓存,而且我们也要正确使用它,可以看下面这样的一个图
可以发现各个层都是有对应的缓存,当然这里还缺少一个部分,就是浏览器,浏览器也有专门的缓存,所以如果要想通过使用缓存来提升网站整体性能,这些方面都需要考虑到
这种缓存一般是偏某个端,比如前段或者后端,针对是开发工程师而言,比如在实际环境中一般从性能角度考虑,服务端有可能会使用多级缓存架构,获取数据的流程如下
1 首先从local缓存获取数据,有直接返回客户端,没有走分布式缓存
2 从分布式缓存中获取数据,如果有写入local缓存并返回客户端端,如果没有
3 查询数据库,写入local缓存和分布式缓存,然后返回客户端
架构图如下