Redis 是如何实现分布式锁的

以 Redisson 为例, 实现方案为 lua 脚本 + Watch Dog , Redis 内部使用的数据结构为 Hash. Hash 内部有一对变量, 分别是客户进程 ID 和重入次数. Watch Dog 是在默认锁 30s 超时设置的时候添加的, 自定义超时时间没有看门狗.

Redisson 也是 Redis 官方推荐分布式锁实现方案,实现起来较为简单。

分布式锁的要求:

  • 互斥性:在任意时刻,只能有一个进程持有锁。
  • 防死锁:即使有一个进程在持有锁的期间崩溃而未能主动释放锁,要有其他方式去释放锁从而保证其他进程能获取到锁。
  • 加锁和解锁的必须是同一个进程。
  • 锁的续期问题。

IO 模型一共有五种, 是哪五种

  • 阻塞
  • 非阻塞
    • 同步
      • 一对一
      • 多对多
    • 异步

通常情况下,用 select 进行 I/O 多路复用的程序设置的文件描述符数目限制是 1024。这个限制的来源是系统内核参数的限制,在 Linux 内核中叫做 FD_SETSIZE,其默认值为 1024。

而为什么要设置成 1024 呢?它是一个传统的数字,在很早以前就已经被定下来了。当时,1024 是一个相对较大的数字,因此大多数程序都不会因为文件描述符数目的限制而受到限制。

但是,随着硬件的提高,现在的系统可以打开的文件数目已经远大于 1024 了,因此,在使用 select 的程序中,需要注意文件描述符数目的限制。

线程池的参数:

核心线程数, 最大线程数, 任务队列, keepAlivedTime+时间单位, 线程工厂, 拒绝策略

任务队列:

  • ArrayBlockingQueue: 出队入队同一个锁
  • LinkedBlockQueue: 出对入队两个锁
  • PriorityQueue: 实现 campareTo() 方法
  • DelayQueue: 入队元素只有等到延迟期过了才能被获取
  • SynchronousQueue: 没有容量, 即入即出
  • LinkedTransferQueue: 先匹配再分配
  • LinkedBlockingDuque: 双向链表

拒绝策略:

  • CallerRunsPolicy - 当触发拒绝策略,只要线程池没有关闭的话,则使用调用线程直接运行任务。一般并发比较小,性能要求不高,不允许失败。但是,由于调用者自己运行任务,如果任务提交速度过快,可能导致程序阻塞,性能效率上必然的损失较大
  • AbortPolicy - 丢弃任务,并抛出拒绝执行 RejectedExecutionException 异常信息。线程池默认的拒绝策略。必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行。
  • DiscardPolicy - 直接丢弃,其他啥都没有
  • DiscardOldestPolicy - 当触发拒绝策略,只要线程池没有关闭的话,丢弃阻塞队列 workQueue 中最老的一个任务,并将新任务加入

自旋锁(spinlock):

是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。 获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成busy-waiting。

重写 equals() 方法:

第一步: 重写 hashCode() 方法, 对一些不同的参数类型的 hashCode 的计算方法如下:

public int hashCode() {
        int result = 17;
        result = 31 * result + mInt; // Int
        result = 31 * result + (mBoolean ? 1 : 0); // Boolean
        result = 31 * result + Float.floatToIntBits(mFloat); // float
        result = 31 * result + (int)(mLong ^ (mLong >>> 32)); // long
        long mDoubleTemp = Double.doubleToLongBits(mDouble); 
        result =31 * result + (int)(mDoubleTemp ^ (mDoubleTemp >>> 32)); // Double
        result = 31 * result + (mString == null ? 0 : mString.hashCode()); // String
        result = 31 * result + (mObj == null ? 0 : mObj.hashCode()); // Object
        return result;
    }

第二步: 重写 equals(Object obj) 方法:

判断步骤:

  • 是否同一个引用:

    if(this == obj) return true;
    if(obj==null) return false;
    
  • 是否是匹配的类: 通过 instanceof 和 getClass 来判断:

    • instanceof 判断可以是子类

      String str = "";
      str instanceof Object; // true
      
    • getClass() 要求全类名必须相同, 并且不看声明, 只看构造

      Object o1 = new Object(); //class java.lang.Object
      Object o2 = new String("abc"); // class java.lang.String
      o1.getClass() == o2.getClass(); // false
      
  • 是否成员都相等:

    Object.equals(mem1,obj.mem1) && Object.equals(mem2,obj.mem2) ...
    

AOP

AOP(面向切面编程 Aspect,)是一种编程范式,用于在不修改原始代码的情况下向现有应用程序添加新功能。这种编程方式将应用程序分成许多独立的部分,称为切面。这些切面可以在应用程序的不同位置进行编写和维护,从而提高了应用程序的可重用性和可维护性

最主要的是为了避免对原来的代码逻辑的破坏和侵入式操作, 使用 AOP 代理完成鉴权, 日志, 解密, 流量统计等操作.

两种方式, JDK 编译时织入, 只支持接口实现的, CGLIB 运行时织入, 通过继承的方式实现一个代理子类完成 AOP. 两个都只能在方法级织入

类加载的过程

--> 加载 = (双亲委派)
--> 验证 = (文件格式验证, 元数据验证, 字节码验证, 符号引用验证)
--> 准备 = (分配内存, 赋值默认值0, null)
--> 解析 = (符号引用到直接引用)
--> 初始化 = (对静态变量赋值, 完成类加载最后的过程)

Spring 创造 Bean

getBean() -> 实例化(相对应的工厂放入三级缓存) -> 属性注入 -> 初始化 -> 放入一级缓存(单例池) -> 结束

Spring循环依赖

Spring通过三级缓存解决了循环依赖,其中一级缓存为单例池(singletonObjects),二级缓存为早期曝光对象earlySingletonObjects,三级缓存为早期曝光对象工厂(singletonFactories)。当A、B两个类发生循环引用时,在A完成实例化后,就使用实例化后的对象去创建一个对象工厂,并添加到三级缓存中,如果A被AOP代理,那么通过这个工厂获取到的就是A代理后的对象,如果A没有被AOP代理,那么这个工厂获取到的就是A实例化的对象。当A进行属性注入时,会去创建B,同时B又依赖了A,所以创建B的同时又会去调用getBean(a)来获取需要的依赖,此时的getBean(a)会从缓存中获取,第一步,先获取到三级缓存中的工厂;第二步,调用对象工工厂的getObject方法来获取到对应的对象,得到这个对象后将其注入到B中。紧接着B会走完它的生命周期流程,包括初始化、后置处理器等。当B创建完后,会将B再注入到A中,此时A再完成它的整个生命周期。至此,循环依赖结束!

MySQL 时间范围查询的效率
  • 对于 InnoDB 引擎,没有索引的情况下(不建议),效率从高到低:int > UNIXTIMESTAMP(timestamp) > datetime(直接和时间比较) > timestamp(直接和时间比较)> UNIXTIMESTAMP(datetime)。
  • 对于 InnoDB 引擎,建立索引的情况下,效率从高到低:int > datetime(直接和时间比较) > timestamp(直接和时间比较)> UNIXTIMESTAMP(timestamp) > UNIXTIMESTAMP(datetime)。
  • 一句话,对于 MyISAM 引擎,采用 UNIX_TIMESTAMP(timestamp) 比较;对于InnoDB 引擎,建立索引,采用 int 或 datetime直接时间比较。