Redis
1.介绍
Redis是当前比较热门的NOSQL系统之一,它是一个开源的使用ANSI c语言编写的key-value存储系统(区别于MySQL的二维表格的形式存储。)。和Memcache类似,但很大程度补偿了Memcache的不足。和Memcache一样,Redis数据都是缓存在计算机内存中,不同的是,Memcache只能将数据缓存到内存中,无法自动定期写入硬盘,这就表示,一断电或重启,内存清空,数据丢失。所以Memcache的应用场景适用于缓存无需持久化的数据。而Redis不同的是它会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,实现数据的持久化。
1.1 特点
-
Redis读取的速度是110000次/s,写的速度是81000次/s
-
原子 。Redis的所有操作都是原子性的,同时Redis还支持对几个操作全并后的原子性执行。
-
支持多种数据结构:string(字符串);list(列表);hash(哈希),set(集合);zset(有序集合)
-
持久化,主从复制(集群)
-
支持过期时间,支持事务,消息订阅。
-
官方不支持window,但是有第三方版本。
1.2 Redis与memcache
/*
1、Redis和Memcache都是将数据存放在内存中,都是内存数据库。不过memcache还可用于缓存其他东西,例如图片、视频等等。
2、Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,hash等数据结构的存储。
3、虚拟内存--Redis当物理内存用完时,可以将一些很久没用到的value 交换到磁盘
4、过期策略--memcache在set时就指定,例如set key1 0 0 8,即永不过期。Redis可以通过例如expire 设定,例如expire name 10
5、分布式--设定memcache集群,利用magent做一主多从;redis可以做一主多从。都可以一主一从
6、存储数据安全--memcache挂掉后,数据没了;redis可以定期保存到磁盘(持久化)
7、灾难恢复--memcache挂掉后,数据不可恢复; redis数据丢失后可以通过aof恢复
8、Redis支持数据的备份,即master-slave模式的数据备份。
*/
2.使用
将一些数据在短时间之内不会发生变化,而且它们还要被频繁访问,为了提高用户的请求速度和降低网站的负载,降低数据库的读写次数,就把这些数据放到缓存中。
页面缓存:第一次从数据库中读取,然后生成一个静态页面,以后所有的读取,只加载这个静态页面就可以了。
数据缓存:由于一个页面有几种需要从不同的缓存中读取数据的模块,所以不适合使用页面缓存。
/*
将redis当做缓存使用LRU算法的缓存来使用;LRU是Redis唯一支持的回收方法。
如果你想把Redis当做一个缓存来用,所有的key都有过期时间,那么你可以考虑 使用以下设置(假设最大内存使用量为2M):
maxmemory 2mb #maxmemory配置指令用于配置Redis存储数据时指定限制的内存大小。
maxmemory-policy allkeys-lru #设置maxmemory为0代表没有内存限制。
以上设置并不需要我们的应用使用EXPIRE(或相似的命令)命令去设置每个key的过期时间,因为 只要内存使用量到达2M,Redis就会使用类LRU算法自动删除某些key。
相比使用额外内存空间存储多个键的过期时间,使用缓存设置是一种更加有效利用内存的方式。而且相比每个键固定的 过期时间,使用LRU也是一种更加推荐的方式,因为这样能使应用的热数据(更频繁使用的键) 在内存中停留时间更久。
基本上这么配置下的Redis可以当成memcached使用。
当我们把Redis当成缓存来使用的时候,如果应用程序同时也需要把Redis当成存储系统来使用,那么强烈建议 使用两个Redis实例。一个是缓存,使用上述方法进行配置,另一个是存储,根据应用的持久化需求进行配置,并且 只存储那些不需要被缓存的数据。
3.集群
3.1 特点
-
多个redis节点网络互联,数据共享
-
所有的节点都是一主一从(可以是多个从),其中从不提供服务,仅作为备用
-
不支持同时处理多个键(如mset/mget),因为redis需要把键均匀分布在各个节点上,并发量很高的情况下同时创建键值会降低性能并导致不可预测的行为。
-
支持在线增加、删除节点
-
客户端可以连任何一个主节点进行读写
redis.conf
port 7001 #端口
cluster-enabled yes #启用集群模式
cluster-config-file nodes.conf
cluster-node-timeout 5000 #超时时间
appendonly yes
daemonize yes #后台运行
protected-mode no #非保护模式
pidfile /var/run/redis_7001.pid
JedisPoolConfig poolConfig = new JedisPoolConfig();
Set<HostAndPort> nodes = new HashSet<HostAndPort>();
HostAndPort hostAndPort = new HostAndPort("192.168.1.99", 7000);
HostAndPort hostAndPort1 = new HostAndPort("192.168.1.99", 7001);
HostAndPort hostAndPort2 = new HostAndPort("192.168.1.99", 7002);
HostAndPort hostAndPort3 = new HostAndPort("192.168.1.99", 7003);
HostAndPort hostAndPort4 = new HostAndPort("192.168.1.99", 7004);
HostAndPort hostAndPort5 = new HostAndPort("192.168.1.99", 7005);
nodes.add(hostAndPort);
nodes.add(hostAndPort1);
nodes.add(hostAndPort2);
nodes.add(hostAndPort3);
nodes.add(hostAndPort4);
nodes.add(hostAndPort5);
JedisCluster jedisCluster = new JedisCluster(nodes, poolConfig);//JedisCluster中默认分装好了连接池.
// redis内部会创建连接池,从连接池中获取连接使用,然后再把连接返回给连接池
String string = jedisCluster.get("a");
System.out.println(string);
集群优点: 1.主从备份,防止主机宕机。 2.读写分离,分担master的任务。 3.任务分离,分担工作与计算。 ------------------------------------------------------------ 方案: 1.master下面有两个孩子,两个孩子直接指向master(树). 2.线性,第一个孩子指向master,第二个孩子指向第一个孩子.
原理: 主从通信:slave启动之后链接master,slave发出同步命令,save拿到dump出的rdb 快照,之后再拿缓冲aof队列中的日志数据进行数据填充。
---master配置: 1.关闭rdb快照(备份工作交给slave) 2.可以开启aof
slave配置: 1.声明slave-of (slave-of localhsot 6379) 2.如果master有密码,slave也要有密码 3.打开rdb快照 4.配置是否只读[salve-read-only]
---主; passwordrequire 密码 --slave masterauth 密码
=============主从缺点: 每次slave断开后,无论是主动断开还是网络故障,再链接master, 都要master全部dump导出rdb再aof. 所以,启动slave要一个一个启动,且有时间间隔。要不然 master主IO剧增。
4.注册中心
使用 Redis 的 Key/Map 结构存储数据结构:
-
主 Key 为服务名和类型
-
Map 中的 Key 为 URL 地址
-
Map 中的 Value 为过期时间,用于判断脏数据,脏数据由监控中心删除 3
使用 Redis 的 Publish/Subscribe 事件通知数据变更:
-
通过事件的值区分事件类型:register, unregister, subscribe, unsubscribe
-
普通消费者直接订阅指定服务提供者的 Key,只会收到指定服务的 register, unregister 事件
-
监控中心通过 psubscribe 功能订阅 /dubbo/,会收到所有服务的所有变更事件*
调用过程:
1.服务提供方启动时,向 Key:/dubbo/com.foo.BarService/providers 下,添加当前提供者的地址
2.并向 Channel:/dubbo/com.foo.BarService/providers 发送 register 事件
3.服务消费方启动时,从 Channel:/dubbo/com.foo.BarService/providers 订阅 register 和 unregister 事件
4.并向 Key:/dubbo/com.foo.BarService/providers 下,添加当前消费者的地址
5.服务消费方收到 register 和 unregister 事件后,从 Key:/dubbo/com.foo.BarService/providers 下获取提供者地址列表
6.服务监控中心启动时,从 Channel:/dubbo/* 订阅 register 和 unregister,以及 subscribe和unsubsribe事件
7.服务监控中心收到 register 和 unregister 事件后,从 Key:/dubbo/com.foo.BarService/providers下获取提供者地址列表
8.服务监控中心收到 subscribe 和 unsubsribe 事件后,从 Key:/dubbo/com.foo.BarService/consumers 下获取消费者地址列
*/
5.redis消息队列
6.分布式锁
分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁。本篇博客将介绍第二种方式,基于Redis实现分布式锁。虽然网上已经有各种介绍Redis分布式锁实现的博客,然而他们的实现却有着各种各样的问题,为了避免误人子弟,本篇博客将详细介绍如何正确地实现Redis分布式锁。
6.1 可靠性
首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
-
互斥性。在任意时刻,只有一个客户端能持有锁。
-
不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
-
具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
-
解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
加锁
public class RedisTool {
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
/**
* 尝试获取分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @param expireTime 超期时间
* @return 是否获取成功
*/
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
可以看到,我们加锁就一行代码:jedis.set(String key, String value, String nxxx, String expx, int time)
,这个set()方法一共有五个形参:
-
第一个为key,我们使用key来当锁,因为key是唯一的。
-
第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用
UUID.randomUUID().toString()
方法生成。 -
第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;
-
第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。
-
第五个为time,与第四个参数相呼应,代表key的过期时间。
总的来说,执行上面的set()方法就只会导致两种结果:
-
当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。
-
已有锁存在,不做任何操作。
心细的童鞋就会发现了,我们的加锁代码满足我们可靠性里描述的三个条件。首先,set()加入了NX参数,可以保证如果已有key存在,则函数不会调用成功,也就是只有一个客户端能持有锁,满足互斥性。其次,由于我们对锁设置了过期时间,即使锁的持有者后续发生崩溃而没有解锁,锁也会因为到了过期时间而自动解锁(即key被删除),不会发生死锁。最后,因为我们将value赋值为requestId,代表加锁的客户端请求标识,那么在客户端在解锁的时候就可以进行校验是否是同一个客户端。由于我们只考虑Redis单机部署的场景,所以容错性我们暂不考虑。
解锁
private static final Long RELEASE_SUCCESS = 1L;
/**
* 释放分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @return 是否释放成功
*/
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
可以看到,我们解锁只需要两行代码就搞定了!第一行代码,我们写了一个简单的Lua脚本代码,上一次见到这个编程语言还是在《黑客与画家》里,没想到这次居然用上了。第二行代码,我们将Lua代码传到jedis.eval()
方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。
就是在eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。
Redission
概述
https://blog.csdn.net/u014042066/article/details/72778440
分布式系统有一个著名的理论CAP,指在一个分布式系统中,最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三项中的两项。所以在设计系统时,往往需要权衡,在CAP中作选择。当然,这个理论也并不一定完美,不同系统对CAP的要求级别不一样,选择需要考虑方方面面。
在微服务系统中,一个请求存在多级跨服务调用,往往需要牺牲强一致性老保证系统高可用,比如通过分布式事务,异步消息等手段完成。但还是有的场景,需要阻塞所有节点的所有线程,对共享资源的访问。比如并发时“超卖”和“余额减为负数”等情况。
本地锁可以通过语言本身支持,要实现分布式锁,就必须依赖中间件,数据库、redis、zookeeper等。
-
互斥:互斥好像是必须的,否则怎么叫锁。
-
死锁: 如果一个线程获得锁,然后挂了,并没有释放锁,致使其他节点(线程)永远无法获取锁,这就是死锁。分布式锁必须做到避免死锁。
-
性能: 高并发分布式系统中,线程互斥等待会成为性能瓶颈,需要好的中间件和实现来保证性能。
-
锁特性:考虑到复杂的场景,分布式锁不能只是加锁,然后一直等待。最好实现如Java Lock的一些功能如:锁判断,超时设置,可重入性等。
Redission锁总结
1,加锁机制
为了实现原子操作,通过执行一段Lua脚本进行加锁。首先锁定key, 先判断key是否已经锁定,未锁定则通过hset语句进行锁定
第一步 使用exist判断key是否存在
第二步 hset 第一个参数是key,第二个参数是value,第三个参数是1 。表示创建一个散列key, field域的值是 1
第三步 设置key过期时间
第四步 使用hexists判断散列,field域的值是否为客户端id
第五步 如果是,则filed域的值加一
第六步 重新设置key过期时间
2,互斥机制
客户端2去加锁,通过第二条语句判断,如果散列field域不是客户端2,则返回pttl剩余的时间,客户端2会进入一个while循环
3, watch dog自动延期
一个后台线程,发现客户端1还持有锁,会不断延长key的时间
4, 可重入加锁机制
第四步判断成功,则会通过hincrby增加域的值
网友评论