java架构知识点-中间件(学习笔记)

一、缓存

为什么要使用缓存
(一)性能 我们在碰到需要执行耗时特别久,且结果不频繁变动的SQL,就特别适合将运行结果放入 缓存。这样,后面的请求就去缓存中读取,使得请求能够迅速响应

(二)并发 如下图所示,在大并发的情况下,所有的请求直接访问数据库,数据库会出现连接异常。这个时候,就需要使用redis做一个缓冲操作,让请求先访问到redis,而不是直接访问数据库。
优秀的缓存系统Redis
Redis是完全开源免费的,用C语言编写的,遵守BSD协议,是一个高性能的(key/value)分布式内存数据库,基于内存运行并支持持久化的NoSQL数据库,是当前最热门的NoSql数据库之一,也被人们称为数据结构服务器
Redis相比同类的其他产品,具有如下优点:
Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用
Redis不仅仅支持简单的key-value类型的数据,同时还提供listsetzsethash等数据结构的存储 Redis支持数据的备份,即master-slave模式的数据备份
redis为什么这么快
主要是以下三点
1.纯内存操作
2.单线程操作,避免了频繁的上下文切换
3.采用了非阻塞I/O多路复用机制

我们的redis-client在操作的时候,会产生具有不同事件类型的socket。在服务端,有
一段I/0多路复用程序,将其置入队列之中。然后,文件事件分派器,依次去队列中取,转发到不同的事件处理器 中。 需要说明的是,这个I/O多路复用机制,redis还提供了selectepollevportkqueue等多路复用函数库,
 
redis的数据类型,以及每种数据类型的使用场景
()String 这个其实没啥好说的,最常规的set/get操作,value可以是String也可以是数字。一般做
一些复杂的计数
功能的缓存。
(二)hash 这里value存放的是结构化的对象,比较方便的就是操作其中的某个字段。博主在做单点登录的时候,就 是用这种数据结构存储用户信息,以cookieId作为key,设置30分钟为缓存过期时间,能很好的模拟出类似session 的效果。
()list 使用List的数据结构,可以做简单的消息队列的功能。另外还有一个就是,可以利用lrange命令,做基于 redis的分页功能,性能极佳,用户体验好。欢迎关注公众号:老男孩的架构路,后
()set 因为set堆放的是一堆不重复值的集合。所以可以做全局去重的功能。为什么不用JVM自带的Set进行去重?
因为我们的系统一般都是集群部署,使用JVM自带的Set,比较麻烦,难道为了一个做一个全局去重,再起一个公共 服务,太麻烦了。 另外,就是利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的**喜好等功能**
()sorted set
sorted set多了一个权重参数score,集合中的元素能够按score进行排列。可以做排行榜应用,取TOP N操作。另外,参照另一篇《分布式之延时任务方案解析》,该文指出了sorted set可以用来做延时任务。最后一个应用就是可以做范围查找
redis的过期策略以及内存淘汰机制
分析:这个问题其实相当重要,到底redis有没用到家,这个问题就可以看出来。比如你redis只能存5G数据,可是你写了10G,那会删5G的数据。怎么删的,这个问题思考过么?还有,你的数据已经设置了过期时间,但是时间到了,内存占用率还是比较高,有思考过原因么? 回答: redis采用的是定期删除+惰性删除策略。
为什么不用定时删 除策略?
定时删除,用一个定时器来负责监视key,过期则自动删除。虽然内存及时释放,但是十分消耗CPU资源。在大并发请求下,CPU要将时间应用在处理请求,而不是删除key,因此没有采用这一策略.
定期删除+惰性删除是如何工作的呢?
定期删除,redis默认每个100ms检查,是否有过期的key,有过期key则删除。需要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis岂不是卡 死)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。 于是,惰性删除派上用场。也就是说在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删 除。
采用定期删除+惰性删除就没其他问题了么? 不是的,如果定期删除没删除key。然后你也没即时去请求key, 也就是说惰性删除也没生效。这样,redis的内存会越来越高。那么就应该采用内存淘汰机制。 在redis.conf中有一 行配置
# maxmemory-policy volatile-lru
该配置就是配内存淘汰策略的(什么,你没配过?好好反省一下自己)
1noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。应该没人用吧。
2allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最 近最少使用的key推荐使用,目前项目在用这种。
3allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key应该也没人用吧,你不删最少使用Key,去随机删。
4volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key这种情况一般是把redis既当缓存,又做持久化存储的时候才用。不推荐
5volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key依然不推荐
6volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。不推荐 ps:如果没有设置 expire key, 不满足先决条件 (prerequisites); 那么 volatile-lru, volatile-random volatile-ttl 策略的行为, noeviction(不删除) 基本上一致。
渐进式ReHash
渐进式rehash的原因
整个rehash过程并不是一步完成的,而是分多次、渐进式的完成。如果哈希表中保存着数量巨大的键值对时,若一次进行rehash,很有可能会导致服务器宕机。
渐进式rehash的步骤
ht[1]分配空间,让字典同时持有ht[0]ht[1]两个哈希表
维持索引计数器变量rehashidx,并将它的值设置为0,表示rehash开始 每次对字典执行增删改查时,将ht[0]rehashidx索引上的所有键值对rehashht[1],将rehashidx+1
ht[0]的所有键值对都被rehashht[1]中,程序将rehashidx的值设置为-1,表示rehash操作完成
注:渐进式rehash的好处在于它采取分为而治的方式,将rehash键值对的计算均摊到每个字典增删改查操作,避免了集中式rehash的庞大计算量。
缓存穿透
概念访问一个不存在的key,缓存不起作用,请求会穿透到DB,流量大时DB会挂掉。
解决方案
采用布隆过滤器,使用一个足够大的bitmap,用于存储可能访问的key,不存在的key直接被过滤;
访问key未在DB查询到值,也将空值写进缓存,但可以设置较短过期时间。
缓存雪崩
大量的key设置了相同的过期时间,导致在缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩。
解决方案
可以给缓存设置过期时间时加上一个随机值时间,使得每个key的过期时间分布开来,不会集中在同一时刻失效;
采用限流算法,限制流量;
采用分布式锁,加锁访问。
                                                                二、消息队列
消息队列中间件是分布式系统中重要的组件,主要解决应用耦合,异步消息,流量削锋等问题
实现高性能,高可用,可伸缩和最终一致性架构 使用较多的消息队列有ActiveMQRabbitMQZeroMQKafkaMetaMQRocketMQ
消息队列应用场景
以下介绍消息队列在实际应用中常用的使用场景。异步处理,应用解耦,流量削锋和消息通讯四个场景
异步处理
场景说明:用户注册后,需要发注册邮件和注册短信。传统的做法有两种 1.串行的方式;2.并行方式
1)串行方式:将注册信息写入数据库成功后,发送注册邮件,再发送注册短信。以上三个任务全部完成后,返回给客户端
 
2)并行方式:将注册信息写入数据库成功后,发送注
 
假设三个业务节点每个使用50毫秒钟,不考虑网络等其他开销,则串行方式的时间是150毫秒,并行的时间可能是
100毫秒。
因为CPU在单位时间内处理的请求数是一定的,假设CPU1秒内吞吐量是100次。则串行方式1秒内CPU可处理的请 求量是7次(1000/150)。并行方式处理的请求量是10次(1000/100

小结:如以上案例描述,传统的方式系统的性能(并发量,吞吐量,响应时间)会有瓶颈。如何解决这个问题呢?
 
按照以上约定,用户的响应时间相当于是注册信息写入数据库的时间,也就是50毫秒。注册邮件,发送短信写入消 息队列后,直接返回,因此写入消息队列的速度很快,基本可以忽略,因此用户的响应时间可能是50毫秒。因此架构改变后,系统的吞吐量提高到每秒20 QPS。比串行提高了3倍,比并行提高了两倍
应用解耦
场景说明:用户下单后,订单系统需要通知库存系统。传统的做法是,订单系统调用库存系统的接口。

流量削锋
流量削锋也是消息队列中的常用场景,一般在秒杀或团抢活动中使用广泛
应用场景:秒杀活动,一般会因为流量过大,导致流量暴增,应用挂掉。为解决这个问题,一般需要在应用前端加入消息队列。 可以控制活动的人数可以缓解短时间内高流量压垮应用
用户的请求,服务器接收后,首先写入消息队列。假如消息队列长度超过最大数量,则直接抛弃用户请求或跳转到错误页面秒杀业务根据消息队列中的请求信息,再做后续处理
日志处理
日志处理是指将消息队列用在日志处理中,比如Kafka的应用,解决大量日志传输的问题。
 
日志采集客户端,负责日志数据采集,定时写受写入Kafka队列 Kafka消息队列,负责日志数据的接收,存储和转发 日志处理应用:订阅并消费kafka队列中的日志数据
以下是新浪kafka日志处理应用案例:转自(http://cloud.51cto.com/art/201507/484338.htm

 
消息的幂等处理
由于网络原因,生产者可能会重复发送消息,因此消费者方必须做消息的幂等处理,常用的解决方案有:
1. 查询操作:查询一次和查询多次,在数据不变的情况下,查询结果是一样的。select是天然的幂等操作;
2. 删除操作:删除操作也是幂等的,删除一次和多次删除都是把数据删除。(注意可能返回结果不一样,删除的 数据不存在,返回0,删除的数据多条,返回结果多个)
3. 唯一索引,防止新增脏数据。比如:支付宝的资金账户,支付宝也有用户账户,每个用户只能有一个资金账户,怎么防止给用户创建资金账户多个,那么给资金账户表中的用户ID加唯一索引,所以一个用户新增成功 一个资金账户记录。要点:唯一索引或唯一组合索引来防止新增数据存在脏数据(当表存在唯一索引,并发 时新增报错时,再查询一次就可以了,数据应该已经存在了,返回结果即可);
4. token机制,防止页面重复提交。业务要求: 页面的数据只能被点击提交一次;发生原因: 由于重复点击或者网络重发,或者nginx重发等情况会导致数据被重复提交;解决办法: 集群环境采用tokenredis(redis单线程的,处理需要排队);单JVM环境:采用tokenredistokenjvm内存。处理流程:
1. 数据提交前要向服务的申请tokentoken放到redisjvm内存,token有效时间;
2. 提交后后台校验token,同时删除 token,生成新的token返回。token特点:要申请,一次有效性,可以限流。注意:redis要用删除操作来判 断token,删除成功代表token校验通过,如果用select+delete来校验token,存在并发问题,不建议使用;
5. 悲观锁——获取数据的时候加锁获取。select * from table_xxx where id='xxx' for update; 注意:id字段一 定是主键或者唯一索引,不然是锁表,会死人的悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,根据实际情况选用;
6. 乐观锁——乐观锁只是在更新数据那一刻锁表,其他时间不锁表,所以相对于悲观锁,效率更高。乐观锁的实现方式多种多样可以通过version或者其他状态条件:1. 通过版本号实现update table_xxx set
name=#name#,version=version+1 where version=#version#如下图(来自网上)2. 通过条件限制 update
table_xxx set avai_amount=avai_amount-#subAmount# where avai_amount-#subAmount# >= 0要求: quality-#subQuality# >= ,这个情景适合不用版本号,只更新是做数据安全校验,适合库存模型,扣份额和回滚份额,性能更高;
update table_xxx set name=#name#,version=version+1 where id=#id# and version=#version#
update table_xxx set avai_amount=avai_amount-#subAmount# where id=#id# and avai_amount-
#subAmount# >= 0
7.分布式锁——还是拿插入数据的例子,如果是分布是系统,构建全局唯一索引比较困难,例如唯一性的字段没法确定,这时候可以引入分布式锁,通过第三方的系统(rediszookeeper),在业务系统插入数据或者更新数据,获取分布式锁,然后做操作,之后释放锁,这样其实是把多线程并发的锁的思路,引入多多个系统,也就是分布式系统中得解决思路。要点:某个长流程处理过程要求不能并发执行,可以在流程执行之前根据某个标志(用户ID+后缀等)获取分布式锁,其他流程执行时获取锁就会失败,也就是同一时间该流程只能有一个能执行成功,执行完成
后,释放分布式锁(分布式锁要第三方系统提供)
8.select + insert——并发不高的后台系统,或者一些任务JOB,为了支持幂等,支持重复执行,简单的处理方法是,先查询下一些关键数据,判断是否已经执行过,在进行业务处理,就可以了。注意:核心高并发流程不要用这种方法;
消息的按序处理
同上,消息的按序也不能完全依靠于TCP在说到消息中间件的时候,我们通常都会谈到一个特性:消息的顺序消费问题。这个问题看起来很简单:Producer 发送消息1, 2, 3。。。 Consumer1, 2, 3。。。顺序消费。
但实际情况却是:无论RocketMQ,还是Kafka,缺省都不保证消息的严格有序消费!
这个特性看起来很简单,但为什么缺省他们都不保证呢?
严格的顺序消费有多么困难
下面就从3个方面来分析一下,对于一个消息中间件来说,严格的顺序消费有多么困难,或者说不可能。
发送端
发送端不能异步发送,异步发送在发送失败的情况下,就没办法保证消息顺序。
比如你连续发了123。 过了一会,返回结果1失败,2, 3成功。你把1再重新发送1遍,这个时候顺序就乱掉 了。
存储端
对于存储端,要保证消息顺序,会有以下几个问题:
1)消息不能分区。也就是1topic,只能有1个队列。在 Kafka中,它叫做partition;在RocketMQ中,它叫做queue。 如果你有多个队列,那同1topic的消息,会分散到多个分区里面,自然不能保证顺序。
2)即使只有1个队列的情况下,会有第2个问题。该机器挂了之后,能否切换到其他机器?也就是高可用问题。
比如你当前的机器挂了,上面还有消息没有消费完。此时切换到其他机器,可用性保证了。但消息顺序就乱掉了。 要想保证,一方面要同步复制,不能异步复制;另1方面得保证,切机器之前,挂掉的机器上面,所有消息必须消 费完了,不能有残留。很明显,这个很难!!!
接收端
对于接收端,不能并行消费,也即不能开多线程或者多个客户端消费同1个队列。
总结
 
从上面的分析可以看出,要保证消息的严格有序,有多么困难!
发送端和接收端的问题,还好解决一点,限制异步发送,限制并行消费。但对于存储端,机器挂了之后,切换的问 题,就很难解决了。
你切换了,可能消息就会乱;你不切换,那就暂时不可用。这2者之间,就需要权衡了。
业务需要全局有序吗?
通过上面分析可以看出,要保证一个topic内部,消息严格的有序,是很困难的,或者说条件是很苛刻的。 那怎么办呢?我们一定要使出所有力气、用尽所有办法,来保证消息的严格有序吗?
这里就需要从另外一个角度去考虑这个问题:业务角度。正如在下面这篇博客中所说的:
http://www.jianshu.com/p/453c6e7ffff81c
实际情况中: (1)不关注顺序的业务大量存在; (2) 队列无序不代表消息无序。
三、搜索引擎
概述
全文搜索就是对文本数据的一种搜索方式,文本数据的都多,可以分为顺序搜索法和索引搜索法,,全文检索使用的是索引搜索法
特点(优势):
做了相关度排序
对文本中的关键字做了高亮显示
摘要截取
只关注文本,不考虑语义
搜索效果更加精确——基于单词搜索,比如搜索Java的时候找不到JavaScript,因为它们是不同的两个单词
使用场景:
替换数据库的模糊查询,提高查询速度,降低数据库压力,增强了查询效率
数据库模糊查询缺点:查询速度慢,左模糊和全模糊会使索引失效,没有相关度排序,没有对文本中关键字
做高亮显示,搜索效果不好 全文检索是搜索引擎的基础 只对“指定领域的网站进行索引和搜索,即垂直搜索 可以在wordpdf等各种各样的数据格式中检索内容 其他场合,比如输入法等
倒排索引
正向索引的结构如下:
文档1”ID > 单词1:出现次数,出现位置列表;单词2:出现次数,出现位置列表;…………
文档2”ID > 此文档出现的关键词列表。

 
当用户在主页上搜索关键词华为手机时,假设只存在正向索引(forward index),那么就需要扫描索引库中的
所有文档,找出所有包含关键词华为手机的文档,再根据打分模型进行打分,排出名次后呈现给用户。因为互联
网上收录在搜索引擎中的文档的数目是个天文数字,这样的索引结构根本无法满足实时返回排名结果的要求。
所以,搜索引擎会将正向索引重新构建为倒排索引,即把文件ID对应到关键词的映射转换为关键词到文件ID的映
,每个关键词都对应着一系列的文件,这些文件中都出现这个关键词。
得到倒排索引的结构如下:
关键词1”文档1”ID文档2”ID…………
关键词2”:带有此关键词的文档ID列表。
 
将原文档传给分次组件(Tokenizer)
分词组件(Tokenizer)会做以下几件事情( 此过程称为Tokenize)
1. 将文档分成一个一个单独的单词。
2. 去除标点符号。
3. 去除停词(Stop word)
所谓停词(Stop word)就是一种语言中最普通的一些单词,由于没有特别的意义,因而大多数情况下不能成为搜索
的关键词,因而创建索引时,这种词会被去掉而减少索引的大小。
英语中挺词(Stop word)如:“the”,“a”“this”等。
对于每一种语言的分词组件(Tokenizer),都有一个停词(stop word)集合。
经过分词(Tokenizer) 后得到的结果称为词元(Token)
在我们的例子中,便得到以下词元(Token)
“Students”“allowed”“go”“their”“friends”“allowed”“drink”“beer”“My”“friend”“Jerry”“went
“school”“see”“his”“students”“found”“them”“drunk”“allowed”
将得到的词元(Token)传给语言处理组件(Linguistic Processor)
语言处理组件(linguistic processor)主要是对得到的词元(Token)做一些同语言相关的处理。
对于英语,语言处理组件(Linguistic Processor) 一般做以下几点:
1. 变为小写(Lowercase)
2. 将单词缩减为词根形式,如“cars ”“car ”等。这种操作称为:stemming
3. 将单词转变为词根形式,如“drove ”“drive ”等。这种操作称为:lemmatization
Stemming lemmatization的异同:
相同之处:Stemminglemmatization都要使词汇成为词根形式。
两者的方式不同:
Stemming采用的是缩减的方式:“cars”“car”“driving”“drive”
Lemmatization采用的是转变的方式:“drove”“drove”“driving”“drive”
两者的算法不同:
Stemming主要是采取某种固定的算法来做这种缩减,如去除“s”,去除“ing”“e”,将“ational”
“ate”,将“tional”变为“tion”
Lemmatization主要是采用保存某种字典的方式做这种转变。比如字典中
“driving”“drive”“drove”“drive”“am, is, are”“be”的映射,做转变时,只要查字典就可以了。
Stemminglemmatization不是互斥关系,是有交集的,有的词利用这两种方式都能达到相同的转换。
语言处理组件(linguistic processor)的结果称为词(Term)
在我们的例子中,经过语言处理,得到的词(Term)如下:
“student”“allow”“go”“their”“friend”“allow”“drink”“beer”“my”“friend”“jerry”“go”“schoo
l”“see”“his”“student”“fifind”“them”“drink”“allow”
也正是因为有语言处理的步骤,才能使搜索drove,而drive也能被搜索出来。
将得到的词(Term)传给索引组件(Indexer)

分词器
WhitespaceAnalyzer 仅仅是去掉了空格,没有其他任何操作,不支持中文。
SimpleAnalyzer 讲除了字母以外的符号全部去除,并且讲所有字符变为小写,需要注意的是这个分词器同样把数据也去除了,同样 不支持中文。
StopAnalyzer 这个和SimpleAnalyzer类似,不过比他增加了一个的是,在其基础上还去除了所的
stop words,比如the, a, this 这些。这个也是不支持中文的。
StandardAnalyzer 英文方面的处理和StopAnalyzer一样的,对中文支持,使用的是单字切割。
CJKAnalyzer 这个支持中日韩,前三个字母也就是这三个国家的缩写。这个对于中文基本上不怎么用吧,对中文的支持很烂,它 是用每两个字作为分割,分割方式个人感觉比较奇葩,我会在下面比较举例。
SmartChineseAnalyzer 中文的分词。比较标准的中文分词,对一些搜索处理的并不是很好

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值