redis应用实战

Redis Java客户端介绍

已有的客户端支持

Redis Java客户端有很多的开源产品比如Redission、Jedis、lettuce

差异

Jedis是Redis的Java实现的客户端,其API提供了比较全面的Redis命令的支持;
Redisson实现了分布式和可扩展的Java数据结构,和Jedis相比,功能较为简单,不支持字符串操作,不支持排序、事务、管道、分区等Redis特性。Redisson主要是促进使用者对Redis的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上。
lettuce是基于Netty构建的一个可伸缩的线程安全的Redis客户端,支持同步、异步、响应式模式。多个线程可以共享一个连接实例,而不必担心多线程并发问题;

jedis-sentinel原理分析

原理

客户端通过连接到哨兵集群,通过发送Protocol.SENTINEL_GET_MASTER_ADDR_BY_NAME 命令,从哨兵机器中询问master节点的信息,拿到master节点的ip和端口号以后,再到客户端发起连接。连接以后,需要在客户端建立监听机制,当master重新选举之后,客户端需要重新连接到新的master节点

源码分析

private HostAndPort initSentinels(Set<String> sentinels, final String masterName) {
 HostAndPort master = null;
 boolean sentinelAvailable = false;
 log.info("Trying to find master from available Sentinels...");
// 有多个sentinels,遍历这些个sentinels
 for (String sentinel : sentinels) {
// host:port表示的sentinel地址转化为一个HostAndPort对象。
  final HostAndPort hap = HostAndPort.parseString(sentinel);
  log.fine("Connecting to Sentinel " + hap);
  Jedis jedis = null;
  try {
// 连接到sentinel
   jedis = new Jedis(hap.getHost(), hap.getPort());
// 根据masterName得到master的地址,返回一个list,host= list[0], port =// list[1]
   List<String> masterAddr = jedis.sentinelGetMasterAddrByName(masterName);
   // connected to sentinel...
   sentinelAvailable = true;
   if (masterAddr == null || masterAddr.size() != 2) {
    log.warning("Can not get master addr, master name: " + masterName + ".
Sentinel: " + hap
      + ".");
    continue;
  }
// 如果在任何一个sentinel中找到了master,不再遍历sentinels
   master = toHostAndPort(masterAddr);
   log.fine("Found Redis master at " + master);
   break;
 } catch (JedisException e) {
   // resolves #1036, it should handle JedisException there's another chance
   // of raising JedisDataException
   log.warning("Cannot get master address from sentinel running @ " + hap + ".
Reason: " + e
     + ". Trying next one.");
 } finally {
   if (jedis != null) {
    jedis.close();
  }
 }
}
// 到这里,如果master为null,则说明有两种情况,一种是所有的sentinels节点都down掉了,一种是master节
点没有被存活的sentinels监控到
 if (master == null) {
  if (sentinelAvailable) {
   // can connect to sentinel, but master name seems to not
   // monitored
   throw new JedisException("Can connect to sentinel, but " + masterName
     + " seems to be not monitored...");
 } else {
   throw new JedisConnectionException("All sentinels down, cannot determine where is
"
     + masterName + " master is running...");
 }
}
//如果走到这里,说明找到了master的地址
 log.info("Redis master running at " + master + ", starting Sentinel listeners...");
//启动对每个sentinels的监听
为每个sentinel都启动了一个监听者MasterListener。MasterListener本身是一个线程,它会去订阅sentinel
上关于master节点地址改变的消息。
 for (String sentinel : sentinels) {
  final HostAndPort hap = HostAndPort.parseString(sentinel);
  MasterListener masterListener = new MasterListener(masterName, hap.getHost(),
hap.getPort());
  // whether MasterListener threads are alive or not, process can be stopped
  masterListener.setDaemon(true);
  masterListeners.add(masterListener);
  masterListener.start();
}
 return master;
}

从哨兵节点获取master信息的方法

public List<String> sentinelGetMasterAddrByName(String masterName) {
 client.sentinel(Protocol.SENTINEL_GET_MASTER_ADDR_BY_NAME, masterName);
 final List<Object> reply = client.getObjectMultiBulkReply();
 return BuilderFactory.STRING_LIST.build(reply);
}

Jedis-cluster原理分析

连接方式

Set<HostAndPort> hostAndPorts=new HashSet<>();
HostAndPort hostAndPort=new HostAndPort("192.168.11.153",7000);
HostAndPort hostAndPort1=new HostAndPort("192.168.11.153",7001);
HostAndPort hostAndPort2=new HostAndPort("192.168.11.154",7003);
HostAndPort hostAndPort3=new HostAndPort("192.168.11.157",7006);
hostAndPorts.add(hostAndPort);
hostAndPorts.add(hostAndPort1);
hostAndPorts.add(hostAndPort2);
hostAndPorts.add(hostAndPort3);
JedisCluster jedisCluster=new JedisCluster(hostAndPorts,6000);
jedisCluster.set("mic","hello");

原理分析

程序启动初始化集群环境
1)、读取配置文件中的节点配置,无论是主从,无论多少个,只拿第一个,获取redis连接实例
2)、用获取的redis连接实例执行clusterNodes()方法,实际执行redis服务端cluster nodes命令,获取主从配置信息
3)、解析主从配置信息,先把所有节点存放到nodes的map集合中,key为节点的ip:port,value为当前节点的jedisPool
4)、解析主节点分配的slots区间段,把slot对应的索引值作为key,第三步中拿到的jedisPool作为value,存储在slots的map集合中
就实现了slot槽索引值与jedisPool的映射,这个jedisPool包含了master的节点信息,所以槽和几点是对应的,与redis服务端一致

从集群环境存取值

1)、把key作为参数,执行CRC16算法,获取key对应的slot值
2)、通过该slot值,去slots的map集合中获取jedisPool实例

3)、通过jedisPool实例获取jedis实例,最终完成redis数据存取工作

Redisson客户端的操作方式

redis-cluster连接方式

Config config=new Config();
config.useClusterServers().setScanInterval(2000).
    addNodeAddress("redis://192.168.11.153:7000",
        "redis://192.168.11.153:7001",
        "redis://192.168.11.154:7003","redis://192.168.11.157:7006");
RedissonClient redissonClient= Redisson.create(config);
RBucket<String> rBucket=redissonClient.getBucket("mic");
System.out.println(rBucket.get());

常规操作命令

getBucket-> 获取字符串对象;
getMap -> 获取map对象
getSortedSet->获取有序集合
getSet -> 获取集合
getList ->获取列表

redis实战

分布式锁的实现

关于锁,其实我们或多或少都有接触过一些,比如synchronized、 Lock这些,这类锁的目的很简单,在多线程环境下,对共享资源的访问造成的线程安全问题,通过锁的机制来实现资源访问互斥。那么什么是分布式锁呢?或者为什么我们需要通过Redis来构建分布式锁,其实最根本原因就是Score(范围),因为在分布式架构中,所有的应用都是进程隔离的,在多进程访问共享资源的时候我们需要满足互斥性,就需要设定一个所有进程都能看得到的范围,而这个范围就是Redis本身。所以我们才需要把锁构建到Redis中。
Redis里面提供了一些比较具有能够实现锁特性的命令,比如SETNX(在键不存在的情况下为键设置值),那么我们可以基于这个命令来去实现一些简单的锁的操作

Redisson实现分布式锁

Redisson它除了常规的操作命令以外,还基于redis本身的特性去实现了很多功能的封装,比如分布式锁、原子操作、布隆过滤器、队列等等。我们可以直接利用这个api提供的功能去实现

Config config=new Config();
    config.useSingleServer().setAddress("redis://192.168.11.152:6379");
RedissonClient redissonClient=Redisson.create(config);
RLock rLock=redissonClient.getLock("updateOrder");
    //最多等待100秒、上锁10s以后自动解锁
    if(rLock.tryLock(100,10,TimeUnit.SECONDS)){
      System.out.println("获取锁成功");
   }

原理分析

trylock

tryAcquire

tryLockInnerAsync

通过lua脚本来实现加锁的操作
\1. 判断lock键是否存在,不存在直接调用hset存储当前线程信息并且设置过期时间,返回nil,告诉客户端直接获取
到锁。
\2. 判断lock键是否存在,存在则将重入次数加1,并重新设置过期时间,返回nil,告诉客户端直接获取到锁。
\3. 被其它线程已经锁定,返回锁有效期的剩余时间,告诉客户端需要等待。

unlock

\1. 如果lock键不存在,发消息说锁已经可用,发送一个消息
\2. 如果锁不是被当前线程锁定,则返回nil
\3. 由于支持可重入,在解锁时将重入次数需要减1
\4. 如果计算后的重入次数>0,则重新设置过期时间
\5. 如果计算后的重入次数<=0,则发消息说锁已经可用

管道模式

Redis服务是一种C/S模型,提供请求-响应式协议的TCP服务,所以当客户端发起请求,服务端处理并返回结果到客户端,一般是以阻塞形式等待服务端的响应,但这在批量处理连接时延迟问题比较严重,所以Redis为了提升或弥补这个问题,引入了管道技术:可以做到服务端未及时响应的时候,客户端也可以继续发送命令请求,做到客户端和服务端互不影响,服务端并最终返回所有服务端的响应,大大提高了C/S模型交互的响应速度上有了质的提高

使用方法

Jedis jedis=new Jedis("192.168.11.152",6379);
Pipeline pipeline=jedis.pipelined();
for(int i=0;i<1000;i++){
  pipeline.incr("test");
}
pipeline.sync();

Redis的应用架构

对于读多写少的高并发场景,我们会经常使用缓存来进行优化。比如说支付宝的余额展示功能,实际上99%的时候都是查询,1%的请求是变更(除非是土豪,每秒钟都有收入在不断更改余额),所以,我们在这样的场景下,可以加入缓存,用户->余额

Redis缓存与数据一致性问题

那么基于上面的这个出发点,问题就来了,当用户的余额发生变化的时候,如何更新缓存中的数据,也就是说。
\1. 我是先更新缓存中的数据再更新数据库的数据;
\2. 还是修改数据库中的数据再更新缓存中的数据
这就是我们经常会在面试遇到的问题,数据库的数据和缓存中的数据如何达到一致性?首先,可以肯定的是,redis中的数据和数据库中的数据不可能保证事务性达到统一的,这个是毫无疑问的,所以在实际应用中,我们都是基于当前的场景进行权衡降低出现不一致问题的出现概率

更新缓存还是让缓存失效
更新缓存表示数据不但会写入到数据库,还会同步更新缓存; 而让缓存失效是表示只更新数据库中的数据,然后删
除缓存中对应的key。那么这两种方式怎么去选择?这块有一个衡量的指标。
\1. 如果更新缓存的代价很小,那么可以先更新缓存,这个代价很小的意思是我不需要很复杂的计算去获得最新的
余额数字。
\2. 如果是更新缓存的代价很大,意味着需要通过多个接口调用和数据查询才能获得最新的结果,那么可以先淘汰
缓存。淘汰缓存以后后续的请求如果在缓存中找不到,自然去数据库中检索。
先操作数据库还是先操作缓存?
当客户端发起事务类型请求时,假设我们以让缓存失效作为缓存的的处理方式,那么又会存在两个情况,
\1. 先更新数据库再让缓存失效
\2. 先让缓存失效,再更新数据库

前面我们讲过,更新数据库和更新缓存这两个操作,是无法保证原子性的,所以我们需要根据当前业务的场景的容忍性来选择。也就是如果出现不一致的情况下,哪一种更新方式对业务的影响最小,就先执行影响最小的方案

最终一致性的解决方案

关于缓存雪崩的解决方案

当缓存大规模渗透在整个架构中以后,那么缓存本身的可用性讲决定整个架构的稳定性。那么接下来我们来讨论下缓存在应用过程中可能会导致的问题

缓存雪崩
缓存雪崩是指设置缓存时采用了相同的过期时间,导致缓存在某一个时刻同时失效,或者缓存服务器宕机宕机导致缓存全面失效,请求全部转发到了DB层面,DB由于瞬间压力增大而导致崩溃。缓存失效导致的雪崩效应对底层系统的冲击是很大的。
解决方式
\1. 对缓存的访问,如果发现从缓存中取不到值,那么通过加锁或者队列的方式保证缓存的单进程操作,从而避免失效时并发请求全部落到底层的存储系统上;但是这种方式会带来性能上的损耗
\2. 将缓存失效的时间分散,降低每一个缓存过期时间的重复率
\3. 如果是因为缓存服务器故障导致的问题,一方面需要保证缓存服务器的高可用、另一方面,应用程序中可以采用多级缓存

缓存穿透
缓存穿透是指查询一个根本不存在的数据,缓存和数据源都不会命中。出于容错的考虑,如果从数据层查不到数据则不写入缓存,即数据源返回值为 null 时,不缓存 null。缓存穿透问题可能会使后端数据源负载加大,由于很多后端数据源不具备高并发性,甚至可能造成后端数据源宕掉

解决方式
\1. 如果查询数据库也为空,直接设置一个默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库,这种办法最简单粗暴。比如,”key” , “&&”。
在返回这个&&值的时候,我们的应用就可以认为这是不存在的key,那我们的应用就可以决定是否继续等待继续访问,还是放弃掉这次操作。如果继续等待访问,过一个时间轮询点后,再次请求这个key,如果取到的值不再是&&,则可以认为这时候key有值了,从而避免了透传到数据库,从而把大量的类似请求挡在了缓存之中。
\2. 根据缓存数据Key的设计规则,将不符合规则的key进行过滤
采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的BitSet中,不存在的数据将会被拦截掉,从而避免了对底层存储系统的查询压力

布隆过滤器

布隆过滤器是Burton Howard Bloom在1970年提出来的,一种空间效率极高的概率型算法和数据结构,主要用来判断一个元素是否在集合中存在。因为他是一个概率型的算法,所以会存在一定的误差,如果传入一个值去布隆过滤器中检索,可能会出现检测存在的结果但是实际上可能是不存在的,但是肯定不会出现实际上不存在然后反馈存在的结果。因此,Bloom Filter不适合些“零错误”的应用场合。而在能容忍低错误率的应用场合下,Bloom Filter通过极少的错误换取了存储空间的极大节省。

bitmap

布隆过滤器(Bloom Filter)是一种节省空间的概率数据结构,由Burton Howard Bloom在1970年提出,用来测试一个元素是否在一个集合里。有可能”误报“,但肯定不会”错报“:对布隆过滤器的一次查询要么返回“可能在集合中“,要么”肯定不在集合里“。

判断一个元素是不是在一个集合里,一般想到的是将所有元素保存起来,然后通过比较来确定。链表、平衡二叉树、散列表,或者是把元素放到数组或链表里,都是这种思路。以上三种结构的检索时间复杂度分别为O(n), O(logn), O(n/k),O(n),O(n)。而布隆过滤器(Bloom Filter)也是用于检索一个元素是否在一个集合中,它的空间复杂度是固定的常数O(m),而检索时间复杂度是固定的常数O(k)。相比而言,有1%误报率和最优值k的布隆过滤器,每个元素只需要9.6个比特位--无论元素的大小。这种优势一方面来自于继承自数组的紧凑性,另外一方面来自于它的概率性质。1%的误报率通过每个元素增加大约4.8比特,就可以降低10倍。

布隆过滤器的原理

布隆过滤器是一种多哈希函数映射的快速查找算法。它可以判断出某个元素肯定不在集合里或者可能在集合里,即它不会漏报,但可能会误报。通常应用在一些需要快速判断某个元素是否属于集合,但不严格要求100%正确的场合。

基本原理

一个空的布隆过滤器是一个m位的位数组,所有位的值都为0。定义了k个不同的符合均匀随机分布的哈希函数,每个函数把集合元素映射到位数组的m位中的某一位。

添加一个元素:

先把这个元素作为k个哈希函数的输入,拿到k个数组位置,然后把所有的这些位置置为1。

查询一个元素(测试这个元素是否在集合里):

把这个元素作为k个哈希函数的输入,得到k个数组位置。这些位置中只要有任意一个是0,元素肯定不在这个集合里。如果元素在集合里,那么这些位置在插入这个元素时都被置为1了。如果这些位置都是1,那么要么元素在集合里,要么所有这些位置是在其他元素插入过程中被偶然置为1了,导致了一次“误报”。

一个布隆过滤器的例子见下图,代表了集合{x,y,z}。带颜色的箭头表示了集合中每个元素映射到位数组中的位置。元素w不在集合里,因为它哈希后的比特位置中有一个值为0的位置。在这个图里,m=18,k=3。

360px_Bloom_filter_svg

一个布隆过滤器的例子

 

简单的布隆过滤器不支持删除一个元素,因为“漏报”是不允许的。一个元素映射到k位,尽管设置这k位中任意一位为0就能够删除这个元素,但也会导致删除其他可能映射到这个位置的元素。因为没办法决定是否有其他元素也映射到了需要删除的这一位上。

通过好几个哈希函数来共同判断这个元素是否在集合里,比只用一次哈希带来冲突的可能性要低很多。暴雪的MPQ归档文件中使用的哈希算法跟布隆过滤器也有异曲同工之妙。

误判率

误判率就是在插入n个元素后,某元素被判断为“可能在集合里”,但实际不在集合里的概率,此时这个元素哈希之后的k个比特位置都被置为1。

假设哈希函数等概率地选择每个数组位置,即哈希后的值符合均匀分布,那么每个元素等概率地哈希到位数组的m个比特位上,与其他元素被哈希到哪些位置无关(独立事件)。设定数组总共有m个比特位,有k个哈希函数。在插入一个元素时,一个特定比特没有被某个哈希函数置为1的概率是:

1

插入一个元素后,这个比特没有被任意哈希函数置为1的概率是:

2

在插入了n个元素后,这个特定比特仍然为0的概率是:

3

所以这个比特被置为1的概率是:

4

现在检测一个不在集合里的元素。经过哈希之后的这k个数组位置任意一个位置都是1的概率如上。这k个位置都为1的概率是:

5

哈希函数个数的最优解

对于给定的m和n,让“误报率”最小的k值为:

6

此时“误报率”为:

7

可以简化为:

9

在leveldb中,设定的误判率<=1%,所以m/n是9.6,即10个比特,此时k=6.72,即7bit,即需要7次hash,每个元素占7bit,总共需要m=n*9.6个比特作为布隆过滤器的位数组数据。

优点

  1. 存储空间和插入/查询时间都是常数,远远超过一般的算法
  2. Hash函数相互之间没有关系,方便由硬件并行实现
  3. 不需要存储元素本身,在某些对保密要求非常严格的场合有优势

缺点

  1. 有一定的误识别率
  2. 删除困难

应用

  1. 搜索引擎中的海量网页去重
  2. leveldb等数据库中快速判断元素是否存在,可以显著减少磁盘访问
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值