美文网首页
2019-05-08

2019-05-08

作者: 晚秋月色 | 来源:发表于2019-05-08 16:04 被阅读0次

    Java总结

    [图片上传失败...(image-e8f21-1557302654041)]

    选择profiles,注意实勾和虚勾有区别的

    2.jetty服务器默认开启的是8080端口,通过使用-Djetty.port=9090 开启服务端口

    3.集合排序规则return a-b,升序排序;return b-a,降序排序;

    eg:a-b 等同于 a>b return 1; a<b return -1;

    b-a 等同于 a>b return -1; a<b return 1;

    返回1 代表把哪位放后面,也就是升序

    4.网络上有人提出用atomikos开源项目实现JTA分布式事务处理

    5.violate和atomic实现共享变量的可见性和原子性

    class VolatileNoAtomic extends Thread {

    // private static volatile int count = 0;

    private static AtomicInteger atomicInteger=new AtomicInteger(0);

    @Override

    public void run() {

    for (int i = 0; i < 1000; i++) {

    // count++;

    atomicInteger.incrementAndGet(); //count++;

    }

    System.out.println(getName() + "-----" + atomicInteger);

    }

    }

    public class VolatileNoAtomicDemo {

    public static void main(String[] args) {

    // 初始化10个线程

    VolatileNoAtomic[] volatileNoAtomic= new VolatileNoAtomic[10];

    for (int i = 0; i < volatileNoAtomic.length; i++) {

    //创建每一个线程

    volatileNoAtomic[i]=new VolatileNoAtomic();

    }

    for (int i = 0; i < volatileNoAtomic.length; i++) {

    //启动每一个线程

    volatileNoAtomic[i].start();

    }

    }

    }

    6.$ lsof -i -P | grep redis查看pid和端口映射

    $ lsof -n -P | grep zoo

    distinct 会将所有NULL合并为一项

    count(distinct)会将NULL除去。

    null不参与count运算,同样适用于sum, avg。

    distinct时,所有null作为一个值。

    8.为啥 redis 单线程模型也能效率这么高?

    • 纯内存操作
    • 核心是基于非阻塞的 IO 多路复用机制
    • 单线程反而避免了多线程的频繁上下文切换问题

    9.一般采取删除缓存,而不是更新缓存是一种lazy思想,在读的时候才缓存而不是更新的时候缓存,更新的时候建议删除缓存.

    10.MySQL 主从同步延时问题(精华)

    以前线上确实处理过因为主从同步延时问题而导致的线上的 bug,属于小型的生产事故。

    是这个么场景。有个同学是这样写代码逻辑的。先插入一条数据,再把它查出来,然后更新这条数据。在生产环境高峰期,写并发达到了 2000/s,这个时候,主从复制延时大概是在小几十毫秒。线上会发现,每天总有那么一些数据,我们期望更新一些重要的数据状态,但在高峰期时候却没更新。用户跟客服反馈,而客服就会反馈给我们。

    我们通过 MySQL 命令:

    show status

    查看 Seconds_Behind_Master,可以看到从库复制主库的数据落后了几 ms。

    一般来说,如果主从延迟较为严重,有以下解决方案:

    • 分库,将一个主库拆分为多个主库,每个主库的写并发就减少了几倍,此时主从延迟可以忽略不计。
    • 打开 MySQL 支持的并行复制,多个库并行复制。如果说某个库的写入并发就是特别高,单库写并发达到了 2000/s,并行复制还是没意义。
    • 重写代码,写代码的同学,要慎重,插入数据时立马查询可能查不到。
    • 如果确实是存在必须先插入,立马要求就查询到,然后立马就要反过来执行一些操作,对这个查询设置直连主库。不推荐这种方法,你这么搞导致读写分离的意义就丧失了。

    11.一致性 hash 算法

    一致性 hash 算法将整个 hash 值空间组织成一个虚拟的圆环,整个空间按顺时针方向组织,下一步将各个 master 节点(使用服务器的 ip 或主机名)进行 hash。这样就能确定每个节点在其哈希环上的位置。

    来了一个 key,首先计算 hash 值,并确定此数据在环上的位置,从此位置沿环顺时针“行走”,遇到的第一个 master 节点就是 key 所在位置。

    在一致性哈希算法中,如果一个节点挂了,受影响的数据仅仅是此节点到环空间前一个节点(沿着逆时针方向行走遇到的第一个节点)之间的数据,其它不受影响。增加一个节点也同理。

    燃鹅,一致性哈希算法在节点太少时,容易因为节点分布不均匀而造成缓存热点的问题。为了解决这种热点问题,一致性 hash 算法引入了虚拟节点机制,即对每一个节点计算多个 hash,每个计算结果位置都放置一个虚拟节点。这样就实现了数据的均匀分布,负载均衡。

    [图片上传失败...(image-3f0d94-1557302654041)]

    12.redis cluster 的 hash slot 算法

    redis cluster 有固定的 16384 个 hash slot,对每个 key 计算 CRC16 值,然后对 16384 取模,可以获取 key 对应的 hash slot。

    redis cluster 中每个 master 都会持有部分 slot,比如有 3 个 master,那么可能每个 master 持有 5000 多个 hash slot。hash slot 让 node 的增加和移除很简单,增加一个 master,就将其他 master 的 hash slot 移动部分过去,减少一个 master,就将它的 hash slot 移动到其他 master 上去。移动 hash slot 的成本是非常低的。客户端的 api,可以对指定的数据,让他们走同一个 hash slot,通过 hash tag 来实现。

    任何一台机器宕机,另外两个节点,不影响的。因为 key 找的是 hash slot,不是机器。

    [图片上传失败...(image-876808-1557302654041)]

    redis cluster 的高可用与主备切换原理

    redis cluster 的高可用的原理,几乎跟哨兵是类似的

    判断节点宕机

    如果一个节点认为另外一个节点宕机,那么就是 pfail,主观宕机。如果多个节点都认为另外一个节点宕机了,那么就是 fail,客观宕机,跟哨兵的原理几乎一样,sdown,odown。

    在 cluster-node-timeout 内,某个节点一直没有返回 pong,那么就被认为 pfail。

    如果一个节点认为某个节点 pfail 了,那么会在 gossip ping 消息中,ping 给其他节点,如果超过半数的节点都认为 pfail 了,那么就会变成 fail。

    从节点过滤

    对宕机的 master node,从其所有的 slave node 中,选择一个切换成 master node。

    检查每个 slave node 与 master node 断开连接的时间,如果超过了 cluster-node-timeout * cluster-slave-validity-factor,那么就没有资格切换成 master。

    从节点选举

    每个从节点,都根据自己对 master 复制数据的 offset,来设置一个选举时间,offset 越大(复制数据越多)的从节点,选举时间越靠前,优先进行选举。

    所有的 master node 开始 slave 选举投票,给要进行选举的 slave 进行投票,如果大部分 master node(N/2 + 1)都投票给了某个从节点,那么选举通过,那个从节点可以切换成 master。

    从节点执行主备切换,从节点切换为主节点。

    与哨兵比较

    整个流程跟哨兵相比,非常类似,所以说,redis cluster 功能强大,直接集成了 replication 和 sentinel 的功能。

    13.如何保证消息的有序性

    • RabbitMQ:一个 queue,多个 consumer。比如,生产者向 RabbitMQ 里发送了三条数据,顺序依次是 data1/data2/data3,压入的是 RabbitMQ 的一个内存队列。有三个消费者分别从 MQ 中消费这三条数据中的一条,结果消费者2先执行完操作,把 data2 存入数据库,然后是 data1/data3。这不明显乱了。

    [图片上传失败...(image-ad8469-1557302654041)]

    • Kafka:比如说我们建了一个 topic,有三个 partition。生产者在写的时候,其实可以指定一个 key,比如说我们指定了某个订单 id 作为 key,那么这个订单相关的数据,一定会被分发到同一个 partition 中去,而且这个 partition 中的数据一定是有顺序的。

    消费者从 partition 中取出来数据的时候,也一定是有顺序的。到这里,顺序还是 ok 的,没有错乱。接着,我们在消费者里可能会搞多个线程来并发处理消息。因为如果消费者是单线程消费处理,而处理比较耗时的话,比如处理一条消息耗时几十 ms,那么 1 秒钟只能处理几十条消息,这吞吐量太低了。而多个线程并发跑的话,顺序可能就乱掉了。

    [图片上传失败...(image-4972d4-1557302654041)]

    解决方案

    RabbitMQ

    拆分多个 queue,每个 queue 一个 consumer,就是多一些 queue 而已,确实是麻烦点;或者就一个 queue 但是对应一个 consumer,然后这个 consumer 内部用内存队列做排队,然后分发给底层不同的 worker 来处理。

    [图片上传失败...(image-203ea9-1557302654041)]

    Kafka

    • 一个 topic,一个 partition,一个 consumer,内部单线程消费,单线程吞吐量太低,一般不会用这个。
    • 写 N 个内存 queue,具有相同 key 的数据都到同一个内存 queue;然后对于 N 个线程,每个线程分别消费一个内存 queue 即可,这样就能保证顺序性。

    14.dubbo服务启动过程

    l) 将初始化后的Server返回协议层

    i. 创建好的Server返回,此Server对象很重,缓存

    m) 协议层将暴露后的Exporter返回给注册中心协议层,最后返回给ServiceConfig进行缓存

    i. 协议层成功返回Exporter给注册中心协议层,标志着服务暴露完毕,接下来是与注册中心的操作,逻辑上看,注册中心负责服务的注册与发现,所有提供者向注册中心注册服务,并订阅重载配置,所有消费者向注册中心注册服务,并订阅服务,消费者通过注册中心自动发现所有服务提供者,且本地缓存提供者类表,注册中心宕机也不影响消费者调用;程序上看,注册中心也是普通的RPC服务,所有消费者、提供者与注册中心都是长连接。Dubbo目前注册中心服务有MulticastRegistry、ZookeeperRegistry等,关于注册中心的详细介绍待后期奉上。

    ii. RegistryProtocol.getRegistry();

    iii. 得到具体注册中心服务且连接注册中心,此时提供者作为消费者引用注册中心核心服务RegistryService

    iv. Registry.register(url);

    v. 调用远端注册中心的register()方法进行服务注册,且若有消费者订阅此服务,则推送消息让消费者引用此服务,注册中心缓存了所有提供者注册的服务以供消费者发现。

    vi. Registry.subscribe(url,listener);

    vii. 提供者向注册中心订阅所有注册服务的覆盖配置,当注册中心有此服务的覆盖配置注册进来时,推送消息给提供者,让它重新暴露服务,这由管理页面完成。

    viii. 返回暴露后的Exporter给上层ServiceConfig进行缓存,便于后期撤销暴露。

    ix. 总结:Exporter的创建、Server的创建、服务的注册与订阅,这些逻辑都是分离的,它们是通过URL联系在一起的。一般情况下,同JVM同协议下的服务共享同一个Server,且消费端的引用这些服务的也可共享一个Client,从而实现多个服务共享同一个通道进行通信,且是基于长连接下,减少了通信的握手次数,高效率通信,另外,远程调用层与信息交换层及网络传输层是Dubbo的核心。

    15.你真的需要服务化吗?

    服务化,SOA,绝对是一个火热了N年的词,再加上各大互联网公司在讲各自的技术架构演进时,基本都会提到服务化,这个诱惑的很多人都想对系统做服务化的改造,但你真的需要服务化吗?

    所谓的服务化,是指根据业务的职责划分为多个系统,系统之间的交互以服务的方式进行,这样的好处看起来就是系统的职责变得非常清晰。

    但其实呢,服务化并不仅仅是一个纯粹的技术改造,服务化就意味着业务是由多个系统构成,这个时候首先会产生的第一个核心问题是需要有相应的人员来维护,在服务化之前,通常来说模式都是一个系统,所有的开发共同维护一个系统,而服务化拆成多个系统后,就不可能所有的开发再共同维护了,因此做服务化之前,首先要做的第一点是组织结构的调整要对应的准备好,所以其实如果开发人员不多的话,显然是没必要做服务化的,否则连开发的人都不够分,Jeff Dean在有一次的topic上讲到服务化带来的一个好处:easier to have many engineering offices around the world.

    另外,有很多想做服务化的原因是觉得服务化后职责清晰,开发效率更高,但事实上是,服务化后变成了多个系统,很多时候会出现为了实现一个需求,需要改多个系统(尽管服务化本来是希望能减少这种现象,但事实上很难做到),协调多个团队,而在一个系统的情况下,通常是每个开发都为整个系统负责,所以在实现需求时是可以一个开发搞定,这样的效率其实通常是更高的,所以其实我始终认为,服务化了以后整个开发效率是一定程度下降的,但好处是它可以支撑很大规模的开发团队。

    所以从上面大家可以看到,服务化是在发展到一定情况下才需要做的,我自己觉得触发要做服务化的主要原因会是这三个:

    1. 底层例如数据库连接到达上限

    系统共用的底层资源在随着机器增加的越来越多时,一定会成为瓶颈,这种情况下服务化会带来帮助,当系统逻辑变简单的情况下,吞吐量自然也比较容易上升,这个时候使用底层资源的机器数自然可以大幅下降。

    2. 开发人员规模庞大

    当开发人员规模变大(100个左右)时,众多人共同维护一个系统会变得不可行,这个时候服务化可以产生巨大帮助,也会成为必须做的事情。

    3. 业务多元化,共享的业务逻辑多

    当业务朝多元化方向发展时,各个业务可能会有很多共享的业务逻辑出现,这个时候服务化会带来帮助,不过通常业务多元化也会自然带来开发人员规模庞大,不过这个方式在逐步发展中也会出现一些问题,但总体来说这种情况下服务化还是必须做的。

    服务化改造还会带来其他一些问题,感兴趣的可以参见我以前写过的一篇《服务框架演变过程》,可点击阅读原文查看。

    如果没有上面的那些原因的话,我的建议都是能不做服务化的话还是尽量不要做。

    所以一个架构师在做架构改造的决定时,最重要的是要考虑全面(各种平衡),很有可能架构改造并不仅仅是技术层面的事,并且需要判断架构改造发生的合适时间点。

    16 并发锁错误案例

    try {

    if (RedisLockUtils.lock(key, 5, 3)) { // 1

    info = queryByPk(RedstarDataRegion.getInstance(), info.getId());

    if (info.getStatus() == JobInfoStatusEnum.OVERDUE.getCode() || info.getStatus() == JobInfoStatusEnum.STOP.getCode()) {

    return;

    }

    //派发任务

    jobAssignDetailService.insertJobAssign(info);

    //更新任务状态

    updateStatusById(info, JobInfoStatusEnum.CONFIRM_PUBLISH.getCode(), JobInfoStatusEnum.TRIGGERED.getCode());

    } else {

    throw new Exception("获取锁失败");

    }

    } catch (Exception e) {

    logger.error("派发任务失败,error:{}", e);

    } finally {

    RedisLockUtils.unlock(key);

    }

    如上1 处代码,如果在try体中,会出现多次并发导致错误的执行finally块,解锁正在执行的获取锁逻辑,所以应该将这块代码放到try外

    17.如何对相同用户id的并发控制

    创建对象池,取得对象的标志就是你提到的用户ID,用用户ID从对象池中取得对象作为同步用的锁对象就可以了,只要保证用户ID一样(参考equals, hashCode)取得得是同一个锁对象(这里用==比较为true)就可以了。

    Java code

    ?

    <colgroup><col style="width: 70px;"><col style="width: 864px;"></colgroup>
    |

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    |

    public class LockerPool {

    Map<String, String> lockers = new HashMap<String, String>(); // Value可以是任意得对象
    
    public String getLocker(String id) {
    
        if (!lockers.getKeys().contains(id)) {
    
            lockers.put(id, "Lock" + id);
    
        }
    
        return lockers.get(id);
    
    }
    
    // 实现单例
    

    }

    同步代码块

    synchronized(LockerPool.getInstance().getLocker(userId)) {

    // your code
    

    }

    |

    上面的几个问题:

    1.Map容量会随用户量的增长无限增长

    2.增长到最大用户,就是所方法而不是锁用户

    3.可以考虑用有界队列的方式,清楚过期的用户id

    17.mysql读写所问题

    解决写冲突两种方案:

    1.select * ... for update select * from banner for update ;

    2. update banner set status = status+1 where id = 1;

    start transaction;

    select * from banner for update ;

    update banner set status = status+1 where id = 1;

    rollback ;

    commit ;

    18.MQ消息丢失情况

    [图片上传失败...(image-584ec5-1557302654040)]

    RabbitMQ 弄丢了数据

    就是 RabbitMQ 自己弄丢了数据,这个你必须开启 RabbitMQ 的持久化,就是消息写入之后会持久化到磁盘,哪怕是 RabbitMQ 自己挂了,恢复之后会自动读取之前存储的数据,一般数据不会丢。除非极其罕见的是,RabbitMQ 还没持久化,自己就挂了,可能导致少量数据丢失,但是这个概率较小。

    设置持久化有两个步骤:

    • 创建 queue 的时候将其设置为持久化

    这样就可以保证 RabbitMQ 持久化 queue 的元数据,但是它是不会持久化 queue 里的数据的。

    • 第二个是发送消息的时候将消息的 deliveryMode 设置为 2

    就是将消息设置为持久化的,此时 RabbitMQ 就会将消息持久化到磁盘上去。

    必须要同时设置这两个持久化才行,RabbitMQ 哪怕是挂了,再次重启,也会从磁盘上重启恢复 queue,恢复这个 queue 里的数据。

    注意,哪怕是你给 RabbitMQ 开启了持久化机制,也有一种可能,就是这个消息写到了 RabbitMQ 中,但是还没来得及持久化到磁盘上,结果不巧,此时 RabbitMQ 挂了,就会导致内存里的一点点数据丢失。

    所以,持久化可以跟生产者那边的 confirm 机制配合起来,只有消息被持久化到磁盘之后,才会通知生产者 ack了,所以哪怕是在持久化到磁盘之前,RabbitMQ 挂了,数据丢了,生产者收不到 ack,你也是可以自己重发的。

    [图片上传失败...(image-d55d77-1557302654040)]

    总结:异步持久化还是同步持久化的问题

    19 Copy-On-Write

    在了解了CopyOnWriteArrayList之后,不知道大家会不会有这样的疑问:他的add/remove等方法都已经加锁了,还要copy一份再修改干嘛?多此一举?同样是线程安全的集合,这玩意和Vector有啥区别呢?

    Copy-On-Write简称COW,是一种用于程序设计中的优化策略。其基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容Copy出去形成一个新的内容然后再改,这是一种延时懒惰策略。

    CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。

    CopyOnWriteArrayList中add/remove等写方法是需要加锁的,目的是为了避免Copy出N个副本出来,导致并发写。

    但是,CopyOnWriteArrayList中的读方法是没有加锁的。

    public E get(int index) { return get(getArray(), index); }

    这样做的好处是我们可以对CopyOnWrite容器进行并发的读,当然,这里读到的数据可能不是最新的。因为写时复制的思想是通过延时更新的策略来实现数据的最终一致性的,并非强一致性。

    所以CopyOnWrite容器是一种读写分离的思想,读和写不同的容器。而Vector在读写的时候使用同一个容器,读写互斥,同时只能做一件事儿。

    20 Cglib是什么

    Cglib是一个强大的、高性能的代码生成包,它广泛被许多AOP框架使用,为他们提供方法的拦截。下图是我网上找到的一张Cglib与一些框架和语言的关系:

    [图片上传失败...(image-5f4bc8-1557302654040)]

    对此图总结一下:

    • 最底层的是字节码Bytecode,字节码是Java为了保证“一次编译、到处运行”而产生的一种虚拟指令格式,例如iload_0、iconst_1、if_icmpne、dup等
    • 位于字节码之上的是ASM,这是一种直接操作字节码的框架,应用ASM需要对Java字节码、Class结构比较熟悉
    • 位于ASM之上的是CGLIB、Groovy、BeanShell,后两种并不是Java体系中的内容而是脚本语言,它们通过ASM框架生成字节码变相执行Java代码,这说明在JVM中执行程序并不一定非要写Java代码----只要你能生成Java字节码,JVM并不关心字节码的来源,当然通过Java代码生成的JVM字节码是通过编译器直接生成的,算是最“正统”的JVM字节码
    • 位于CGLIB、Groovy、BeanShell之上的就是Hibernate、Spring AOP这些框架了,这一层大家都比较熟悉
    • 最上层的是Applications,即具体应用,一般都是一个Web项目或者本地跑一个程序

    相关文章

      网友评论

          本文标题:2019-05-08

          本文链接:https://www.haomeiwen.com/subject/jgqvoqtx.html