0%

索引

概念:索引是为了加快数据查询的一种数据结构

索引类型

类型分类
存储结构维度划分 B + Tree索引、B-Tree索引、Hash索引
应用层次维度划分 主键索引,普通索引、唯一索引、全文索引、空间索引
索引键值类型维度划分 主键索引、辅助索引(二级索引)
数据存储和索引键值逻辑关系维度划分 聚集索引(聚簇索引)、非聚集索引(非聚簇索引)
索引组成维度划分 组合索引(复合索引)、单一索引
阅读全文 »

Redis 常见面试题

1. 什么是Redis?它主要用来什么的?

Redis,英文全称是Remote Dictionary Server(远程字典服务),是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。

与MySQL数据库不同的是,Redis的数据是存在内存中的。它的读写速度非常快,每秒可以处理超过10万次读写操作。因此redis被广泛应用于缓存,另外,Redis也经常用来做分布式锁。除此之外,Redis支持事务、持久化、LUA 脚本、LRU 驱动事件、多种集群方案。

阅读全文 »

Redisson锁

Redis 实现分布式锁主要步骤

  1. 指定一个 key 作为锁标记,存入 Redis 中,指定一个 唯一的用户标识 作为 value。
  2. 当 key 不存在时才能设置值,确保同一时间只有一个客户端进程获得锁,满足 互斥性 特性。
  3. 设置一个过期时间,防止因系统异常导致没能删除这个 key,满足 防死锁 特性。
  4. 当处理完业务之后需要清除这个 key 来释放锁,清除 key 时需要校验 value 值,需要满足 只有加锁的人才能释放锁

特别注意:以上实现步骤考虑到了使用分布式锁需要考虑的互斥性、防死锁、加锁和解锁必须为同一个进程等问题,但是锁的续期无法实现。所以,博主采用 Redisson 实现 Redis 的分布式锁,借助 Redisson 的 WatchDog 机制 能够很好的解决锁续期的问题,同样 Redisson 也是 Redis 官方推荐分布式锁实现方案,实现起来较为简单。

高效分布式锁

当我们在设计分布式锁的时候,我们应该考虑分布式锁至少要满足的一些条件,同时考虑如何高效的设计分布式锁,这里我认为以下几点是必须要考虑的。

互斥

在分布式高并发的条件下,我们最需要保证,同一时刻只能有一个线程获得锁,这是最基本的一点。

防止死锁

在分布式高并发的条件下,比如有个线程获得锁的同时,还没有来得及去释放锁,就因为系统故障或者其它原因使它无法执行释放锁的命令,导致其它线程都无法获得锁,造成死锁。

所以分布式非常有必要设置锁的有效时间,确保系统出现故障后,在一定时间内能够主动去释放锁,避免造成死锁的情况。

性能

对于访问量大的共享资源,需要考虑减少锁等待的时间,避免导致大量线程阻塞。

所以在锁的设计时,需要考虑两点。

  • 锁的颗粒度要尽量小。比如你要通过锁来减库存,那这个锁的名称你可以设置成是商品的ID,而不是任取名称。这样这个锁只对当前商品有效,锁的颗粒度小。
  • 锁的范围尽量要小。比如只要锁2行代码就可以解决问题的,那就不要去锁10行代码了。

重入

我们知道ReentrantLock是可重入锁,那它的特点就是:同一个线程可以重复拿到同一个资源的锁。重入锁非常有利于资源的高效利用。

阅读全文 »

高可用

主从模式

  • 不具备自动容错和恢复功能,需要等机器重启或者手动切换前端IP才能恢复
  • 较难支持在线扩容
  • 存在数据丢失,以及全量复制时会降低性能

哨兵模式

  • sentinal,哨兵,主要负责:
    (1) 集群监控,负责监控redis master和salve进程是否正常工作
    (2) 消息通知,如果某个redis有故障,哨兵负责发送消息作为报警通知管理员
    (3) 故障转移,如果master挂掉,自动转移到salve 节点上
    (4) 配置中心.如果故障转移发生,通知客户端新的master地址

  • 哨兵本身也可以是分布式的,作为一个哨兵集群,这样当部分哨兵节点挂掉,集群依然可以正常工作

  • 数据丢失

    • 主从复制导致丢失

      • 在master节点数据还没同步到slave的时候,master宕机了
    • 集群脑裂导致丢失

      • 某个master所在机器突然脱离了正常的网络,跟其他slave节点不能连接,但实际上master还运行着,此时哨兵可能会认为master宕机,重新选举了master,此时集群里会存在两个master

      • 在某个slave选举成为master之后,客户端还没来得及切换到新的master上面,继续向旧的master写入数据,此时旧master恢复正常,会作为一个slave挂到新的master上面,自己的数据就会被清空,重新复制数据

    • 解决办法
      min-salves-to-write
      min-slaves-max-lag
      设置要求最少数量的slave,复制和同步最大延迟不能超过多长时间.超过min-slaves-max-lag指定的时间后未收到ack则master拒绝写请求

  • sdown和odown失败状态
    - sdown是主观宕机,即就一个哨兵自己觉得master宕机了.如果一个哨兵ping一个master,超过is-master-after-milliseconds指定的时间后,没有收到回复则主观认为master宕机了
    - odowm,客观宕机,如果quorum数量的哨兵都觉得master宕机了.如果一个哨兵指定时间内,收到了quorum指定数量的其他哨兵也认为master宕机了,那就是客观宕机

  • 自动发现
    - 哨兵间的相互发现是通过redis的pub/sub机制实现的
    每个哨兵都会往 sentinel:hello这个channel发送消息,这个时候其他哨兵可以订阅这个消息感知其他哨兵的存在
    - 每隔两秒钟,哨兵就会往自己监控的某个master+slaves对应的 sentinel:hello channel里发送一个消息,内容是自己的host ip runid 和对这个master的监控配置

  • 选举算法
    - 如果一个master被认为odown了,而且majority哨兵都允许主备涉黄,那么某个哨兵就会执行主备切换操作
    - 首先需要quorum数量的哨兵认为odowm了,然后选举出来一个哨兵来做切换,这个哨兵需得到majority哨兵的授权
    如quorum < majority ,比如有5个哨兵,majority就是3,quorum设置为2,那么需要3个哨兵授权才能执行切换
    如quorum >= majority, 那么必须quorum数量的哨兵授权才能执行
    - 执行切换的那个哨兵会从切换到的新master哪里得到一个 configuration epoch(一个版本包version),进行传播

  • 优点
    - 基于主从模式,具有主从的有点
    - 故障可以转移,可用性好

  • 缺点
    - 配置复杂
    - 在线扩容较难

Cluster集群

  • 16384个槽
  • crc16 对 16384 取余
  • 有任意一个master挂掉且没有可用的slaves时,集群进入fail状态,不可用
阅读全文 »

数据类型

  • 基本类型
    • String(字符串)
    • Hash(哈希)
    • List(列表)
    • Set(集合)
    • Zset(有序集合)
  • 特殊的数据结构类型
    • Geospatial
    • Hyperloglog
    • Bitmap
阅读全文 »

Redis持久化方案

AOF

  • Append only File,在redis执行写命令的时,会追加该命令到日志文件中。当Redis故障重启后,读取该日志文件的命令即可恢复数据

配置:

  • 默认不开启,需在配置文件中打开 appendonly = yes

策略:

image

  • Redis先执行写命令之后才将命令记录到AOF日志中(主进程执行命令写入内存-Redis数据,再写日志到AOF缓冲,缓冲后落盘)

    • 优势:

      • 避免额外的检查开销。因为如果先记录日志在执行命令的话,如果当前命令语法有问题,那么如果不进行命令语法检查,那么错误的命令就会被记录到日志,Redis在使用日志恢复时可能或出错。
      • 不阻塞当前写操作命令的执行。因为写命令执行成功后才会记录到AOF日志中。
    • 风险(AOF日志回写硬盘的时机有关):

      • 数据丢失风险

        执行写操作命令和记录日志是两个过程,当Redis还没来得及将命令写入到硬盘时,服务器发生宕机,那么数据就会丢失

      • 给下一个命令带来阻塞风险

        由于写命令执行成功后才会记录AOF日志,所以不会阻塞当前写操作的命令执行,当会给下一个命令带来阻塞风险。写日志时,服务器的硬盘IO压力太大就会导致写硬盘速度很慢进而阻塞住

阅读全文 »

Redis缓存更新方案:

先更新数据库,再更新缓存

策略:

并发请求A、B。

A请求先更新数据库的值为1,然后在更新缓存前,请求B将数据库的值更新为2,紧接着也把缓存更新为2,然后A请求更新缓存为1。于是出现数据库缓存不一致,数据库为2,缓存为1

问题:存在并发更新同一条数据时,可能会出现数据库缓存不一致现象

先更新缓存,再更新数据库

策略:

并发请求A、B。

A请求先将缓存数据更新为1,然后在更新数据库前,B请求将数据库的值更新为2,紧接着将数据库更新为2,此时A请求将数据库的数据更新为1。于是出现了数据库缓存数据不一致

问题:存在并发更新同一条数据时,可能会出现数据库缓存不一致现象

先删除缓存,再删除数据库(Cache Aside 旁路更新策略①)

策略:

并发请求A、B。

请求A要将数据库值更新为21,所以请求A会删除缓存,此时请求B要读取该值,它查询缓存后没有命中,会从数据库中读取原值20,并写入到缓存中,然后请求A继续更改数据值为21。此时依然会出现数据库缓存不一致的现象

问题:

不更新缓存,而是删除缓存,然后后读取数据时,发现缓存中没有数据,在从数据库中读取数据更新到缓存中。分为【读策略】和【写策略】
写策略步骤:更新数据库数据,删除缓存中的数据
读策略步骤:如果读取的数据命中缓存,则直接返回数据;如果没有命中则从数据库中读取数据,然后更新缓存并返回

先更新数据库,再删除缓存(Cache Aside 旁路更新策略①)

策略:

并发请求A、B。

请求A查询某个值(假设缓存中不存在),此时请求A从数据中查询到值为20,在未写入缓存时请求B更新该值,先将数据库更新为21,并删除缓存。这是请求A把从数据库中读取到的数据20写入缓存。最终还是可能出现数据库缓存不一致的现象。

概率较低,缓存速度理论上不会出现该问题,但若删除缓存失败会导致缓存不一致。

问题:

不更新缓存,而是删除缓存,然后后读取数据时,发现缓存中没有数据,在从数据库中读取数据更新到缓存中。分为【读策略】和【写策略】
写策略步骤:更新数据库数据,删除缓存中的数据
读策略步骤:如果读取的数据命中缓存,则直接返回数据;如果没有命中则从数据库中读取数据,然后更新缓存并返回

问题解决方案:

原因:

两步操作,没有并发控制,因此遇到并发场景时,由于执行顺序不同导致数据不一致

方案:

  • 分布式锁

    同一时间同一数据只由一个请求操作更新缓存

  • 过期时间

    数据不一致 兜底方案

  • 保证数据库与缓存两个操作均成功

    • 重试(如@Retryable)
    • 订阅MySQL binlong,异步操作缓存(Canal,MQ)
注解:

①Cache Aside 旁路更新策略(不保证数据一致性)

释义:更新数据时,不更新缓存,而是删除缓存中的数据,在读取数据时,发现缓存中没有,再从数据库中读取数据,更新到缓存中。

来源:https://www.jianshu.com/p/1f7fa8f6cd59

缓存雪崩

原因:

大量Key同时过期或Redis宕机,请求直接访问数据库造成巨大压力,影响数据库性能及系统稳定性

解决方案:

  • 大量Key同时过期

    • 过期时间设置策略,避免Key同时全部失效
    • 互斥锁:保证同一时刻只有一个请求构建缓存,构建后是否锁,其他请求此刻阻塞或返回空
    • 双Key策略:设置主从Key,主Key设置过期时间,备用Key永不过期,主Key过期时返回备Key数据,主Key构建完数据时同步更新备Key数据
    • 后台缓存更新:将数据同步功能解耦,通过MQ或Cannal方式更新数据
  • redis宕机

    • 服务熔断或请求限流:降低请求量或直接返回空环境服务器压力
    • 构建集群:Redis集群

缓存击穿

原因:

某个热点数据过期,大量请求访问无法直接在缓存获取数据,导致直接访问数据库

解决方案:

  • 互斥锁或后台更新:不设置过期时间或快过期时通知后台线程更新

缓存穿透

原因:

访问Redis与Database中都不存在的数据,无法根据当前请求构建缓存数据,造成应用压力

解决方案:

  • 限制非法请求
    • 避免恶意请求专门访问增加服务器压力,通过接口API参数判断请求参数合理性
  • 缓存指定默认值或设置空值
    • 直接设置访问请求的Result为null并进行定时缓存,使后面请求可访问缓存数据
  • 布隆过滤器
    • 可快速查询数据是否存在,避免通过查询Database判断增加数据库压力。落库时使用布隆过滤器记录,确保缓存失效时查询的数据是否真实存在
    • 注:存在误判情况,结果不准确

Spring Bean初始化及@Bean销毁

  • @PostConstruct
  • @Bean(initMethod = “X”)
  • @InitilizingBean#afterPropertiesSet

@PostConstruct

1
2
3
4
@PostConstruct
public void init(){
System.out.println("PostConstruct init.......");
}

@Bean(initMethod = “X”)

  • 初始化
1
2
3
4
5
6
7
8
/**
* 显式指定bean的名称,初始化调用方法即销毁调用方法
* @return
*/
@Bean(name = "orderBean",initMethod = "init",destroyMethod = "destroy")
public static OrderAllServiceImpl orderBeanFactory(){
return new OrderAllServiceImpl();
}
  • 销毁方式-1:在 OrderAllServiceImpl 实现类中写 init 方法和 destroy 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class OrderAllServiceImpl implements  OrderAllService{
public void init(){
log.info("bean init start...");
}

@Override
public List<Product> getProductList(String orderCode) {
//TODO
return new ArrayList<>();
}

public void destroy(){
log.info("bean destory...");
}}
  • 销毁方式-2:使用方法注解 @PostConstruct @PreDestroy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class OrderAllServiceImpl implements OrderAllService {
@PostConstruct
public void init(){
System.out.println("bean init");
}
@Override
public List<Product> getProductNameByOrderCode(String code) {
//TODO
return new ArrayList<>();
}
@PreDestroy
public void destroy(){
System.out.println("bean desory");
}
}

@InitilizingBean#afterPropertiesSet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Component
public class OrderProcessors implements InitializingBean {

@Autowired
private ApplicationContext applicationContext;

private static OrderAllService OrderAllService;

private static final List<OrderProcessor> PROCESSOR_LIST = new ArrayList<>();

@Autowired
public void setOrderAllService(OrderAllService OrderAllService) {
OrderProcessors.OrderAllService = OrderAllService;
}

@Override
public void afterPropertiesSet() throws Exception {
log.info("init processors");
// 按顺序添加处理器
PROCESSOR_LIST.add(applicationContext.getBean(FirstProcessor.class));
PROCESSOR_LIST.add(applicationContext.getBean(SecondProcessor.class));

}}

初始化顺序

  • 1.@PostConstruct
  • 2.@InitializingBean#afterPropertiesSet
  • 3.@Bean(initMethod=”X”)

@Bean初始化与销毁方法和 bean 中方法执行的顺序

  • 类的构造函数执行 —》自定义初始化方法执行 —》自定义 destroy 方法 —》 bean 销毁。
来源:

https://github.com/mifunc/Spring-BeanInitialization

IOC

Inversion of Control 控制反转

  • IOC是一种思想,实现程序的可插拔的核心理念就是控制反转(IoC:Inversion of Control),所谓的控制反转就是将代码的调用权(控制权)从调用放转移给被调用方(服务提供方,容器)。

    所谓的”控制反转”就是对组件对象控制权的转移,从程序代码本身转移到了外部容器,由容器来创建对象并管理对象之间的依赖关系。

    image

阅读全文 »