Redis Cluster采用无中心结构,每个节点都保存数据和整个集群的状态
每个节点都和其他所有节点连接,这些连接保持活跃
使用gossip协议传播信息以及发现新节点
redis节点不作为client请求的代理,client根据节点返回的信息重定向请求
redis是单线程结构,避免了并发对数据结构加锁引起的额外性能损耗。而且redis的性能瓶颈是在网络上,单线程就够用。Redis采取纯内存操作,采用非阻塞IO多路复用(操作系统提供select, epoll, evport, kqueue等机制)。
Redis 提供了多种不同级别的持久化方式:
RDB持久化可以在指定的时间间隔内生成数据集的时间点快照(point-in-time snapshot)。RDB模式可能需要调整Linux的内核参数。这主要是因为fork子进程时,需要申请巨大的内存空间(大小和父进程一致),但是由于linux的写时复制机制,这些内存实际上都不需要被使用。所以内存不会被使用。但是内核不知道这件事情,会因为该内核参数向linux进程say no。
https://blog.csdn.net/houjixin/article/details/46412557
RDB生成的文件为dump.rdb。如果不作处理,下一次会redis先建立一个临时文件,然后将其替换。
AOF 持久化记录服务器执行的所有写操作命令,并在服务器启动时,通过重新执行这些命令来还原数据集。 AOF 文件中的命令全部以 Redis 协议的格式来保存,新命令会被追加到文件的末尾。 Redis 还可以在后台对 AOF 文件进行重写(rewrite),使得 AOF 文件的体积不会超出保存数据集状态所需的实际大小。
显然AOF更不容易丢失数据(一般认为最多只丢失最后一秒的数据),但是慢,占用空间大,。AOF文件也可以设置为同步更新到磁盘,但是这样速度太慢,一般不推荐。
Redis中包含5种数据类型:STRING、LIST、SET、HASH、ZSET。
5种数据结构最终存储的数据类型实际只有两种:字符和数值,Redis能够区分存储的值是字符还是数字;
key只能是STRING,不要使用特殊字符。
这五种数据结构的存储方式实际上非常重要,帮助redis在内存消耗和效率上达到平衡。
最关键的区别就是,对节点和数据,都做一次哈希运算,然后比较节点和数据的哈希值,数据取和节点最相近的节点做为存放节点。这样就保证当节点增加或者减少的时候,影响的数据最少。
先构造一个长度为2的32次方的整数环(这个环被称为一致性Hash环),根据节点名称的Hash值(其分布为[0, 2的32次方-1])将服务器节点放置在这个Hash环上,然后根据数据的Key值计算得到其Hash值(其分布也为[0, 2的32次方-1]),接着在Hash环上顺时针查找距离这个Key值的Hash值最近的服务器节点,完成Key到服务器的映射查找。
一般的,在一致性哈希算法中,如果一台服务器不可用,则受影响的数据仅仅是此服务器到其环空间中前一台服务器(即沿着逆时针方向行走遇到的第一台服务器)之间数据(即本该映射到该台服务器上的数据被映射到了下一台),其它不会受到影响。
除了用于缓存,一致性hash算法也用于ngnix的负载均衡中。然而,REDIS并没有使用一致性hash算法。
一致性HASH有如下缺点:
一个 Redis Cluster包含16384(0~16383)个哈希槽,存储在Redis Cluster中的所有键都会被映射到这些slot中,
集群中的每个键都属于这16384个哈希槽中的一个,集群使用公式slot=CRC16(key)/16384来计算key属于哪个槽,其中CRC16(key)语句用于计算key的CRC16 校验和。
例如当前集群有3个节点,槽默认是平均分的:
节点 A (6381)包含 0 到 5499号哈希槽.
节点 B (6382)包含5500 到 10999 号哈希槽.
节点 C (6383)包含11000 到 16383号哈希槽.
当新增或者减少节点时,调整槽的分布即可。虽然影响的数据比一致性HASH多,但是大大减少了数据管理的成本。
Redis 集群中内置了 16384 个哈希槽,当需要在 Redis 集群中放置一个 key-value时,redis 先对 key 使用 crc16 算法算出一个结果,然后把结果对 16384 取模,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,redis 会根据节点数量大致均等的将哈希槽映射到不同的节点。
它先以 MULTI 开始一个事务, 然后将多个命令入队到事务中, 最后由 EXEC 命令触发事务, 一并执行事务中的所有命令。
事务可以理解为一个打包的批量执行脚本。这个批量指令并非原子化的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。
MULTI:使用该命令,标记一个事务块的开始,通常在执行之后会回复OK,(但不一定真的OK),这个时候用户可以输入多个操作来代替逐条操作,redis会将这些操作放入队列中。
EXEC:执行这个事务内的所有命令
DISCARD:放弃事务,即该事务内的所有命令都将取消
WATCH:监控一个或者多个key,如果这些key在提交事务(EXEC)之前被其他用户修改过,那么事务将执行失败,需要重新获取最新数据重头操作(类似于乐观锁)。
UNWATCH:取消WATCH命令对多有key的监控,所有监控锁将会被取消。
一个主服务器可以有多个从服务器。
Redis的自身支持主从复制,只要更改下启动配置即可。从节点可以自动从主节点同步数据,主节点可以自动把读请求转给从节点从而实现读写分离减轻自身压力。但是,主节点如果完蛋了,从节点无法升级为主节点,client也无法自动切换,所以没有高可用的能力。
Redis-Sentinel是Redis官方推荐的高可用性(HA)解决方案,当用Redis做Master-slave的高可用方案时,假如master宕机了,Redis本身(包括它的很多客户端)都没有实现自动进行主备切换。Redis-sentinel本身也是一个独立运行的进程,它能监控多个master-slave集群,发现master宕机后能进行自动切换。
Redis3.0版本之后支持Cluster。Redis集群至少要三主三丛,主从不需要配置,系统自动选择。
每个Redis集群中的节点都需要打开两个TCP连接。一个连接用于正常的给Client提供服务,比如6379,还有一个额外的端口(通过在这个端口号上加10000)作为数据端口,比如16379。第二个端口(本例中就是16379)用于集群总线,
(1)领着选举过程是集群中所有master参与,如果半数以上master节点与master节点通信超过(cluster-node-timeout),认为当前master节点挂掉.
(2)下列情况下整个集群不可用(cluster_state:fail),当集群不可用时,所有对集群的操作做都不可用,收到((error) CLUSTERDOWN The cluster is down)错误
客户端会可以对任意一个redis实例去发送命令,每个redis实例接收到命令,都会计算key对应的hash slot。然后告诉客户端。如果使用下面的指令登陆redis的shell,会实现自动的重定向。
#查看key对应的hash slot
cluster keyslot key
#连接客户端时加-c参数可以自动重定向
redis-cli -c
第一种方案,利用redis的list数据结构。生产者lpush,消费者brpop。
brpop是RPOP命令的阻塞版本: 命令会以从左到右的顺序,访问给定的各个列表,并弹出首个非空列表最右端的项; 如果所有给定列表都为空,那么客户端将被阻塞,直到等待超时,或者有可弹出的项出现为 止;
设置 timeout参数为0表示永远阻塞。
第二种方案,利用redis的发布订阅机制,生产者发布,消费者订阅。但是这个一旦有网络抖动等问题就会丢数,仅使用于日志或者统计。
redis的消息队列没有ACK机制。处理失败要记得手动放回队列。
https://www.cnblogs.com/cww0814/p/7805854.html
乐观锁的一种方式是利用数据库。数据表中存储数据版本的字段(也就是上一个用过跑步机的人的号码)。我们也可以利用REDIS中的WATCH和事务来实现这个问题。以描述系统为例,首先先WATCH这个KEY(库存),然后读取KEY值,然后MULTI 开启一个事务,修改KEY值(库存-1),然后EXEC触发一个事务。如果期间库存被别人修改,那么就就没有抢到乐观锁。
下面是一个10件商品的秒杀流程。
public class MyRunnable implements Runnable {
String watchkeys = "watchkeys";// 监视keys
Jedis jedis = new Jedis("192.168.3.202", 6379);
public MyRunnable() {
}
@Override
public void run() {
try {
jedis.watch("watchkeys");// watchkeys
String val = jedis.get(watchkeys);
int valint = Integer.valueOf(val);
String userifo = UUID.randomUUID().toString();
if (valint < 10) {
Transaction tx = jedis.multi();// 开启事务
tx.incr("watchkeys");
List<Object> list = tx.exec();// 提交事务,如果此时watchkeys被改动了,则返回null
if (list != null) {
System.out.println("用户:" + userifo + "抢购成功,当前抢购成功人数:"
+ (valint + 1));
/* 抢购成功业务逻辑 */
jedis.sadd("setsucc", userifo);
} else {
System.out.println("用户:" + userifo + "抢购失败");
/* 抢购失败业务逻辑 */
jedis.sadd("setfail", userifo);
}
} else {
System.out.println("用户:" + userifo + "抢购失败");
jedis.sadd("setfail", userifo);
// Thread.sleep(500);
return;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
jedis.close();
}
}
}
System.out.println("_____________begin______________");
RLock rLock1= redissonClient.getLock("GDL_HAHA");
System.out.println(rLock1);
RLock rLock2 = redissonClient.getLock("GDL_HAHAA");//注意,如果锁的名称相同,虽然getlock返回的是相同的对象,但是实际上是一把锁
System.out.println(rLock2);
boolean locked = rLock1.tryLock();
System.out.println(rLock1.isLocked());
System.out.println(rLock2.isLocked());
System.out.println("_____________end______________");
如下代码会产生异常
@Test
public void testParallelRedisLock(){
System.out.println("_____________begin______________");
redissonClient.getLock("GDL_TEST");
new Thread(new Runnable() {
@Override
public void run() {
RLock rLock = redissonClient.getLock("GDL_HAHA");
rLock.tryLock();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
RLock rLock= redissonClient.getLock("GDL_HAHA");
rLock.unlock();
}
}).start();
}
Exception in thread "Thread-5" java.lang.IllegalMonitorStateException: attempt to unlock lock, not locked by current thread by node id: c1493d67-873a-462f-b26e-4c63dd51736b thread-id: 37
at org.redisson.RedissonLock.unlock(RedissonLock.java:366)
使用Redis分布式锁的的基本代码是
String lockKey = key + LOCK_KEY_TAIL;
RLock lock = redissonClient.getLock(lockKey);
boolean locked = false;
try {
locked = lock.tryLock(15000, 7500, TimeUnit.MILLISECONDS);
//Do some job here
} catch (Exception e) {
log.error("updateRedis Exception key: {} ,reason ,{}", key, reason);
} finally {
if (locked && lock.isHeldByCurrentThread()) {// 必须要加上判断是否被当前线程所有,不然超时释放锁后会产生IllegalMonitorStateException异常
lock.unlock();
}
}
在上面的处理方式中,如果获取锁的客户端端执行时间过长,进程被kill掉,或者因为其他异常崩溃,导致无法释放锁,就会造成死锁。所以,需要对加锁要做时效性检测。如果不想等待redis中的内容超时的话,我们在加锁时,把当前时间戳作为value存入此锁中,通过当前时间戳和Redis中的时间戳进行对比,如果超过一定差值,认为锁已经时效,防止锁无限期的锁下去,但是,在大并发情况,如果同时检测锁失效,并简单粗暴的删除死锁,再通过SETNX上锁,可能会导致竞争条件的产生,即多个客户端同时获取锁。
1. C1获取锁,并崩溃。C2和C3调用SETNX上锁返回0后,获得foo.lock的时间戳,通过比对时间戳,发现锁超时。
2. C2 向foo.lock发送DEL命令。
3. C2 向foo.lock发送SETNX获取锁。
4. C3 向foo.lock发送DEL命令,此时C3发送DEL时,其实DEL掉的是C2的锁。
5. C3 向foo.lock发送SETNX获取锁。
所以这样做是不行的。如果锁超时,那我们必须要等待redis自动超时。
还有一种方法,就是利用getset命令。将给定 key 的值设为 value ,并返回 key 的旧值 (old value),当 key 存在但不是字符串类型时,返回一个错误,当key不存在时,返回nil。
进程P4执行 SETNX lock.foo 以尝试获取锁
由于进程P1已获得了锁,所以P4执行 SETNX lock.foo 返回0,即获取锁失败
P4执行 GET lock.foo 来检测锁是否已超时,如果没超时,则等待一段时间,再次检测
如果P4检测到锁已超时,即当前的时间大于键 lock.foo 的值,P4会执行以下操作
GETSET lock.foo
由于 GETSET 操作在设置键的值的同时,还会返回键的旧值,通过比较键 lock.foo 的旧值是否小于当前时间,可以判断进程是否已获得锁
假如另一个进程P5也检测到锁已超时,并在P4之前执行了 GETSET 操作,那么P4的 GETSET 操作返回的是一个大于当前时间的时间戳,这样P4就不会获得锁而继续等待。注意到,即使P4接下来将键 lock.foo 的值设置了比P5设置的更大的值也没影响。
比如说淘宝的订单号,肯定不能相同,而且也应该保持一个大致的顺序。REDIS自增操作是原子性的。这个可以让我们轻松搞定这一问题。但是!redis的同一个key只存在一个server(或者一个主从)里,所以可能会有性能问题。可以使用Redis集群来获取更高的吞吐量。假如一个集群中有5台Redis。可以初始化每台Redis的值分别是1,2,3,4,5,然后步长都是5。各个Redis生成的ID为:
A:1,6,11,16,21
B:2,7,12,17,22
C:3,8,13,18,23
D:4,9,14,19,24
E:5,10,15,20,25
事实上,采用时间+UUID的做法也是可以的。
黑客故意取请求缓存中不存在的数据,导致所有请求都会被堆积到数据库上,引起数据库瘫痪。可以采取异步起一个线程读数据库,预热和更新缓存。也可以采取布隆滤波器,迅速判断出请求所携带的key是否合法。但是这两种方法都有局限性,所以还是建议采取封IP等前端的反DDOS措施。
同一时间缓存大面积失效。假设你是批量把数据库中的内容更新到缓存中,并设置了相同的缓存时间,那么到时后,缓存将集体失效,所有请求瞬间全部打入数据库,这显然不合理。建议设置随机的缓存时间。
缓存一致性是一个非常复杂的问题。议采取先更新数据库,再删除redis缓存的方法。
redis在java中可以使用jedis,也可以使用spring封装的jedis(Spring Data Redis)
主要有以下两个包
[kettle@vm-kvm11559-app bin]$ ./pip list|grep -i redis
redis (2.10.5)
[kettle@vm-kvm11559-app bin]$ ./pip show redis
---
Metadata-Version: 1.1
Name: redis
Version: 2.10.5
Summary: Python client for Redis key-value store
Home-page: http://github.com/andymccurdy/redis-py
Author: Andy McCurdy
Author-email: [email protected]
License: MIT
Location: /DATA/software/anaconda2/lib/python2.7/site-packages
Requires:
Classifiers:
Development Status :: 5 - Production/Stable
Environment :: Console
Intended Audience :: Developers
License :: OSI Approved :: MIT License
Operating System :: OS Independent
Programming Language :: Python
Programming Language :: Python :: 2.6
Programming Language :: Python :: 2.7
Programming Language :: Python :: 3
Programming Language :: Python :: 3.2
Programming Language :: Python :: 3.3
ziplist是一个经过特殊编码的双向链表,它的设计目标就是为了提高存储效率。ziplist可以用于存储字符串或整数,其中整数是按真正的二进制表示进行编码的,而不是编码成字符串序列。它能以O(1)的时间复杂度在表的两端提供push和pop操作。
实际上,ziplist充分体现了Redis对于存储效率的追求。一个普通的双向链表,链表中每一项都占用独立的一块内存,各项之间用地址指针(或引用)连接起来。这种方式会带来大量的内存碎片,而且地址指针也会占用额外的内存。而ziplist却是将表中每一项存放在前后连续的地址空间内,一个ziplist整体占用一大块内存。它是一个表(list),但其实不是一个链表(linked list)。
另外,ziplist为了在细节上节省内存,对于值的存储采用了变长的编码方式,大概意思是说,对于大的整数,就多用一些字节来存储,而对于小的整数,就少用一些字节来存储。我们接下来很快就会讨论到这些实现细节。