2019秋招面试复习 项目重点提问

1. 使用redis作为分布式锁的注意事项

来源 https://www.cnblogs.com/gxyandwmm/p/9588383.html

Redis分布式锁实现的三个核心要素

1. 加锁

setnx(key,1)当一个线程执行setnx返回1,说明key原本不存在,该线程成功得到了锁,当其他线程执行setnx返回0,说明key已经存在,该线程抢锁失败。

2.解锁

当得到锁的线程执行完任务,需要释放锁,以便其他线程可以进入。释放锁的最简单方式是执行del指令,del(key)释放锁之后,其他线程就可以继续执行setnx命令来获得锁。

3.锁超时

如果一个得到锁的线程在执行任务的过程中挂掉,来不及显式地释放锁,这块资源将会永远被锁住,别的线程再也别想进来。

所以,setnx的key必须设置一个超时时间,以保证即使没有被显式释放,这把锁也要在一定时间后自动释放。setnx不支持超时参数,所以需要额外的指令,expire(key, 30)

if(setnx(key,1) == 1){
    expire(key,30try {
        do something ......
    }catch()
  {
  }
  finally {
       del(key)
    }

}

问题 1 setnx和expire的非原子性

设想一个极端场景,当某线程执行setnx,成功得到了锁:setnx刚执行成功,还未来得及执行expire指令,该节点挂了。这样一来,这把锁就没有设置过期时间,变得“长生不老”,别的线程再也无法获得锁了。

解决:

setnx指令本身是不支持传入超时时间的,Redis 2.6.12以上版本为set指令增加了可选参数,伪代码如下:set(key,1,30,NX,这样就可以取代setnx指令

问题 2 超时后使用del 导致误删其他线程的锁:

假如某线程成功得到了锁,并且设置的超时时间是30秒。

如果某些原因导致线程B执行的很慢很慢,过了30秒都没执行完,这时候锁过期自动释放,线程B得到了锁。随后,线程A执行完了任务,线程A接着执行del指令来释放锁。但这时候线程B还没执行完,线程A实际上删除的是线程B加的锁

解决:

可以在del释放锁之前做一个判断,验证当前的锁是不是自己加的锁。

至于具体的实现,可以在加锁的时候把当前的线程ID当做value,并在删除之前验证key对应的value是不是自己线程的ID。

 1 加锁:
 2 String threadId = Thread.currentThread().getId()
 3 set(key,threadId ,30,NX)
 4 
 5 doSomething.....
 6  
 7 解锁:
 8 if(threadId .equals(redisClient.get(key))){
 9     del(key)
10 }

但是,这样做又隐含了一个新的问题,if判断和释放锁是两个独立操作,不是原子性的

可以通过使用Lua脚本 来实现两个语句的原子性。

 

可能出现的问题及解决方法:

1. 在执行业务时,多个并发的请求,导致同个资源被访问被多次执行加锁
2. 加锁后,业务并发访问这个线程大部分都返回了失败,只有少部分获取到锁的线程才能处理,这种情况下是非常不通情理的(使用reddison已经内部实现的自旋锁来进行锁的等待,获取不到锁就while循环一直尝试加锁,知道锁的获取成功才返回结果,但是这是悲观锁)
3. 加完锁后,每次获取完锁时就对一个特定值+1,执行完后对特定值进行释放,但是线程拿到锁后,抛出异常时,无法执行最后释放锁操作加finally块执行释放锁操作
4. 加了finally块后,这个块中的业务失败了,或者程序挂了,redis连接失败了,无法释放锁(对锁加超时时间
5. 加了超时时间后,在持有锁这个业务中的执行时间比超时时间长,在业务执行的时候,锁超时释放了,这时,其他的请求的线程就能获取到这个锁了,出现了线程的重复进入
对redis锁的key值用一个UUID来设计,同个线程内获取这个锁都需要生成一个唯一的id,释放时只能让同个线程内存放的id匹配释放
,第二个线程执行了业务,而且这个业务执行后,比第一个线程快,并且锁超时解锁解了第一个锁
获取到这个锁的时候,另外开辟一个线程,比如超时时间是10秒,这个线程就每5秒就查询一下这个锁是否失效,如果没有失效就增加5秒中,保持这个锁的有效性

 

 

高并发设计思路

参考 https://blog.csdn.net/qiuchaoxi/article/details/81012384

http连接池+NIO+线程池(多生产者多消费者)(反向代理服务器,一致性哈希算法)+阻塞队列+数据库连接池+缓存(主从、集群)+数据库(集群、分库主从)。

细节: 

1、设置http连接池,可以降低延迟,提高客户端响应时间。还可以连接池复用,支持更大的并发量。

2、把一些静态资源先加载到浏览器缓存里面,减少服务器端的压力

3、可以对服务器端的数据进行压缩

4、反向代理服务器可以保护服务器的安全,来自互联网的请求必需经过代理服务器。所以也可以在代理服务器放一些静态数据,当用户第一次访问静态内容时,静态内容就被缓存在反向代理服务器上,其他用户请求进来时,就可以直接返回,减轻web服务器负载压力。

5、NIO模型(是在Linux还是Windows系统下,Windows建议用AIO,Linux系统下AIO的底层也是基于epoll多路复用,差别不大,LF的区别)

6、线程池(根据线程池处理不同性质的任务,要有不同性质的线程池,IO密集型,CPU*2。CPU密集型,CPU+1.多生产者多消费这模型)线程池还需要考虑:a.先设置一个最大线程数量和最小线程数量,进行性能评估,压测。b.线程池阻塞队列的大小要有界,否则服务器压力过大。c.须考虑线程池的失败策略,失败后的补偿。d.后台批处理服务须与线上面向用户的服务进行分离。

7、阻塞队列,因为NIO第二个阶段会引起用户线程的阻塞,比如可能等待JDBC连接数据库,因此在这里用一个阻塞队列,线程把请求放到阻塞队列里面,这个线程就可以回归线程池,处理别的事情了。是一个生产者消费者模型

8、建一个数据库连接池,主要是为了减少资源的消耗、减少延迟。

9、数据存储部分,1)根据实际情况设置索引和优化SQL语句。2)幂等、乐观、悲观。3)防止SQL注入攻击。4)一个事务当中操作不要过多,可能会阻塞,进而累积造成数据库的故障。5)数据量太大,查询的时间利用limit关键字进行分页处理,防止结果集太大,让应用OOM。

10、使用缓存,减少数据库的访问次数,提高并发量。1)缓存的结构,LRU,链表(集合类存放超时对象),大小,时间。2)核心业务和非核心业务进行分离,减少相互影响的可能性,不要使用共享缓存。3)不常用的数据不要使用缓存。4)夜间查询一天之类搜索频率比较高的词汇,结合AI进行预测,预测的结果预先放到缓存里面。5)考虑分布式缓存数据库:Redis、memcached,防止本地缓存内存溢出。Redis的主从同步,读写分离、负载均衡。主从+一级二级缓存+哨兵。哨兵是Redis 的高可用性解决方案:由一个或多个哨兵实例 组成的哨兵 系统可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器。

11、主从有瓶颈,会有延迟、主服务器压力过大。考虑集群、分库分表。考虑用一致性哈希算法实现分布式缓存数据库。数据迁移量小,引入虚拟节点、防止数据倾斜。

12、 若有重复数据,布隆过滤器去重。

13、数据库存储文件名之类的,服务器保存实际的数据。

保持数据一致性的方法:1. 消息队列  2. 用同一个数据库,A.B,C用同一个数据库。 3。 用Redis缓存,把一些数据放到缓存。

 

2. 高并发系统设计2

来源 https://blog.csdn.net/qixiang_chen/article/details/88945241

一、系统架构扩展
系统的扩展性可以提供系统的性能。代表系统能够容纳更高的负载、更大的数据集,并且系统是可维护的。扩展可以分为两种:

垂直扩展(stade up),提高单一的机器性能配置,如添加内存、更换更强的处理器等等。
2.水平扩展(out),横向添加新机器。

水平扩展比垂直扩展有更强大的扩展性,但水平扩展也来了更高的维护成本。实践中需要根据具体情况来寻求一个平衡点。

二、静态化技术
采用预处理方式将页面静态化,存储在磁盘,不需要连接数据库读取数据,可以提高服务端性能

三、使用缓存服务器减少IO
使用Redis,Memache内存服务器,在内存中存储数据,减少磁盘IO读取量

四、引入微服务器框架Dubbo,SpringCloud
将业务模块切分为多个微服务,托管在微服务框架中,提高负载均衡与容错处理

五、使用数据库集群技术
数据库层面采用主从复制模式、集群模式、采用分区表将数据平均分配到多个磁盘控制器。采用读写分离设计模式

六、图片存储在分布文件系统或CDN服务中
静态资源(图片、视频、网页)采用分布式存储,或者使用CDN服务分发,系统需要设计为前后端分离。

七、业务处理采用NIO技术
采用非阻塞IO技术,借鉴Dubbo使用Netty框架。

八、应用服务调优(Tomcat,Weblogic,Websphere)
通用的配置是设置JVM参数,内存各分区大小,垃圾回收线程多少。再根据不同应用服务器的配置参数,优化应用服务器。

九、负载均衡
使用了水平扩展之如何将大量的请求“均衡”到我们的扩展机器上

两种负载均衡模式:有状态(如有携带session)和无状态

两种负载均衡方式:硬件均衡和软件均衡

硬件均衡比较简单,通常接入一个设备即可,之后的均衡和故障检测等等都由硬件自动完成。成本较高。

软件均衡则是通过软件来转发各种请求,更加容易的定义转发规则,有较多的开源产品选择。

第四层和第七层

经常在负载均衡中看到第四层和第七层这两个名词。它们实际上是指它们各自工作时所处理的网络协议的层数(使用ISO模型)。

第四层是数据传输层,包括TCP和UDP,第七层则是应用层,通常web中为HTTP应用。如Apache、nginx等支持第七层的均衡,而且可配置性都相当强大,能够适应较复杂的应用。例如可以简单的将流量分担到各个负载机上,也可以定义一套业务规则,将应用划分为不同的池,每个池处理某些固定规则的URL。

对比软件均衡与硬件均衡,可以发现它们各自的优缺点:

硬件均衡成本比较高,软件均衡多数可以使用免费的开源软件来实现。

硬件均衡对于故障检测比软件均衡更加强大、快速。在采用硬件均衡时,一旦某台机器出现故障,马上就可以检测出来并立即屏蔽。

硬件均衡可以快速的添加机器(接入硬件接口即可),而软件均衡除了添加机器外还要添加一些配置信息,以将某些流量导入到新的机器。

软件均衡可以定义非常复杂的业务规则,而硬件均衡在这方面相对较弱。

多数的硬件均衡方案都有捆绑附加的一些服务如HTTPS加速、DOS防火墙等等。

十、操作系统优化
虚拟内存调优、可用文件句柄多少配置,

 

posted @ 2019-09-20 16:30  lllunaticer  阅读(209)  评论(0编辑  收藏  举报