余声-个人博客


  • 首页

  • 分类

  • 归档

  • 标签

redis 常见性能问题和解决方案

发表于 2025-02-11 | 更新于 2025-06-22 | 分类于 面试
字数统计 | 阅读时长

👌redis常见性能问题和解决方案?

口语化回答

好的,面试官,redis 常见性能问题主要有内存空间不足,大 key 问题,阻塞操作等等。像内存空间问题,主要发生在大规模的数据量下产生,针对这种我们可以采取数据结构层面的优化,或者集群模式的水平扩。大 key 问题一般就是最初设计的时候,没有考虑清楚,导致业务日积月累,一个小的 key 变成了大 key。会导致我们的性能下降,耗时增加,这种需要从根上进行业务的梳理和处理拆分。还有就是阻塞,如果执行一些 keys 命令会大致阻塞,生产要避免进行这些操作。以上

题目解析

这道题大家主要是从以下的几个问题中,选取常见的 3 个即可。建议大家选择空间不足,大 key 和阻塞,这三点比较好回答,也好解释和扩展。

面试得分点

内存不足,大 key,阻塞,网络延迟,慢查询,持久化性能

题目详细答案

问题一、redis 内存空间不足

****由于Redis的数据存储在内存中,当数据量增大时,可能会出现内存不足的情况,导致性能下降或服务不可用。

解决方案:

内存优化:使用更高效的数据结构(如哈希表、压缩列表)来存储数据,减少内存占用。

水平扩展:使用Redis集群模式,将数据分片存储在多个节点上,扩展内存容量。

问题二、redis 的大 key

****某些键可能存储了大量数据(如大列表、大哈希表),操作这些大键可能导致阻塞,影响性能。

解决方案:

拆分大键:将大键拆分成多个小键,减少单个键的操作时间。

分批处理:对于需要迭代处理的大键,使用SCAN、SSCAN、HSCAN、ZSCAN等命令进行分批处理,避免单次操作时间过长。

监控和预警:定期监控Redis中的大键,及时发现并处理。

问题三、阻塞操作

某些Redis命令(如KEYS、FLUSHALL、SAVE等)会阻塞服务器,导致其他操作无法执行。

解决方案:

避免阻塞命令:尽量避免使用阻塞命令,使用非阻塞的替代命令(如SCAN代替KEYS)。

异步操作:对于需要执行的阻塞操作,尽量使用异步方式(如FLUSHALL ASYNC)。

问题四、网络延迟

Redis是基于TCP协议的网络服务,高网络延迟会影响Redis的性能。

解决方案:

本地部署:尽量将Redis服务器部署在与应用服务器同一内网,减少网络延迟。

连接池:使用连接池来复用Redis连接,减少连接建立和关闭的开销。

问题五、慢查询

****某些复杂的查询或数据操作可能会导致Redis响应变慢,影响整体性能。

解决方案:

慢查询日志:启用Redis的慢查询日志功能,定期检查慢查询并优化。

索引优化:合理使用Redis的数据结构和索引,优化查询性能。

问题六、主从复制延迟

****在主从复制架构中,从服务器可能会因为网络或负载问题导致复制延迟,影响数据一致性。

解决方案:

优化网络:确保主从服务器之间的网络连接稳定,带宽充足。

调整复制参数:优化Redis的复制参数(如repl-backlog-size、repl-timeout等),减少复制延迟。

监控复制状态:定期监控主从复制状态,及时发现并处理延迟问题。

问题七、持久化性能问题

****Redis的持久化操作(如RDB快照和AOF日志)可能会影响性能,尤其是在大数据量或高并发情况下。

解决方案:

合理配置持久化策略:根据业务需求配置合理的持久化策略,平衡性能和数据安全性。

异步持久化:使用异步持久化方式(如AOF的fsync策略),减少对主线程的影响。

redis 内存用完之后会发生什么

发表于 2025-02-11 | 更新于 2025-06-22 | 分类于 面试
字数统计 | 阅读时长

👌redis的内存用完了会发生什么?

口语化回答

好的,面试官。redis 内存用完之后发生的现象主要取决于我们配置的内存回收策略。默认是noeviction,这个策略不会删除任何的键,当内存不足的时候,就会报错。这种策略,我们一般不使用。常见使用的就是 lru,回收最近最少使用的有过期时间的键。其他的策略还比如 randow,可以回收随机的键。ttl 按照最短的过期时间来进行回收。以上。

题目解析

这道题还算是比较常问的一道题,大家需要理解其中的策略,然后答题的时候,能说上几种就可以了。最后再说一下方案的选择优点就可以了。

面试得分点

lru,lfu,random,无过期

题目详细答案

当Redis的内存用完时,会根据配置的内存回收策略采取不同的措施。可以在内存达到限制时决定如何处理新的写请求。主要的策略有如下 8 种。

内存回收策略

  1. noeviction:不删除任何键,当内存不足时返回错误。这是默认策略。

当内存达到限制时,Redis将不再接受任何写请求,并返回错误。例如,客户端尝试设置新键时,会收到类似以下的错误信息:

1
(error) OOM command not allowed when used memory > 'maxmemory'.
  1. allkeys-lru:使用最近最少使用(LRU)算法回收所有键。
  2. volatile-lru:使用最近最少使用(LRU)算法回收设置了过期时间的键。

Redis将根据LRU算法选择最近最少使用的键进行删除,以腾出空间存储新的数据。allkeys-lru会在所有键中选择,volatile-lru只会在设置了过期时间的键中选择。

  1. allkeys-random:随机回收所有键。
  2. volatile-random:随机回收设置了过期时间的键。

Redis会随机选择一些键进行删除,以腾出空间。allkeys-random会在所有键中选择,volatile-random只会在设置了过期时间的键中选择。

  1. volatile-ttl:回收那些剩余生存时间(TTL)最短的键。

Redis将选择那些剩余生存时间(TTL)最短的键进行删除。

  1. volatile-lfu:使用最长时间没有被使用(LFU)算法回收设置了过期时间的键。
  2. allkeys-lfu:使用最长时间没有被使用(LFU)算法回收所有键。

Redis将根据LFU算法选择最近最少使用的键进行删除。volatile-lfu只会在设置了过期时间的键中选择,allkeys-lfu会在所有键中选择。

配置内存回收策略的方式

redis.conf文件中配置内存回收策略,例如:

1
2
maxmemory 100mb
maxmemory-policy allkeys-lru

也可通过命令行参数设置:

1
redis-server --maxmemory 100mb --maxmemory-policy allkeys-lru

redis 同步机制

发表于 2025-02-11 | 更新于 2025-06-22 | 分类于 面试
字数统计 | 阅读时长

👌redis的同步机制是什么?

口语化回答

好的,面试官。redis 的同步机制主要是主从同步,一开始从服务器发送同步命令,主服务器接收到之后,就会生成一个 rdb 的文件,然后传输给从服务器,从服务器接收到之后,立马进行数据的恢复。然后当主服务器再次接收到写命令的时候,会发给从服务器。这个过程是一个异步复制,主服务器不会等待结果。这样就完成了主从复制,主要的核心步骤就是这些。如果同步机制发生问题的话,从服务器可以进行断线重连。还可以做集群、哨兵,来自动切换。以上。

题目解析

主要还是考主从同步原理和如何进行配置,还可以带一点故障的处理。面试官主要是想看看你对集群有没有一定的了解,redis 主从的数据复制有没有了解。

面试得分点

rdb 快照,增量同步,故障机制

题目详细答案

redis的同步机制主要涉及主从复制,主从复制机制允许一个服务器(主服务器)将数据复制到一个或多个服务器(从服务器)。从服务器可以是只读的,也可以接受写操作,但这些写操作不会被同步回主服务器。

初次同步

当从服务器第一次连接到主服务器时,或者当从服务器与主服务器的连接中断后重新连接时,会触发一次全量同步过程。

  1. 从服务器发送SYNC命令:从服务器向主服务器发送SYNC命令,请求进行同步。
  2. 主服务器生成RDB快照:主服务器接收到SYNC命令后,会生成一个RDB(Redis Database)快照文件,并在生成过程中将所有新写入的命令记录到一个缓冲区中。
  3. 传输RDB文件:主服务器将生成的RDB文件发送给从服务器。从服务器接收到RDB文件后,会清空自身的数据库并加载这个RDB文件。
  4. 传输缓冲区中的命令:主服务器将缓冲区中的所有写命令发送给从服务器,从服务器依次执行这些命令,以确保数据完全同步。

增量同步

在初次同步完成后,主从服务器会保持连接状态,主服务器会将后续的所有写命令实时发送给从服务器,从服务器执行这些命令以保持数据的一致性。

同步机制如何配置

主服务器配置

主服务器的配置通常不需要特别设置,只需要确保其能够接受从服务器的连接请求。

从服务器配置

在从服务器的配置文件中,需要指定主服务器的IP地址和端口号:

1
2
replicaof <master-ip> <master-port>
replicaof 192.168.1.100 6379

同步机制的故障处理

断线重连

当从服务器与主服务器的连接中断时,从服务器会自动尝试重连。在重连成功后,从服务器会根据情况选择进行全量同步或增量同步。

主从切换

在高可用环境中,可以使用Redis Sentinel或Redis Cluster来实现自动主从切换。当主服务器发生故障时,Sentinel或Cluster会自动选举一个新的主服务器,并通知其他从服务器进行同步。

redis 高级数据类型有哪些

发表于 2025-02-11 | 更新于 2025-06-22 | 分类于 面试
字数统计 | 阅读时长

👌redis的高级数据类型有哪些?

口语化回答

好的,面试官,面对一些复杂的场景,redis提供了一些高级数据类型,来进行了功能的扩展。主要有四种,bitmaps,hyperloglog,geo,stream。stream 不是非常常用,主要是用来实现消息队列功能。常用的就是 bitmap,bitmap 的 0,1 特性,非常实用于签到,或者存在,不存在这种类型判断,以及在大量数据下,快速统计是否结果。bitmap 非常节省空间,相比于传统的存储数据后,在 mysql 等层面统计,bitmap 更加适用。其次就是hyperloglog 主要是用于一些数量的统计,不过要允许误差,他不会存具体的内容,会帮助我们进行数据的统计,像常见的网站访问统计,就非常适合这个数据结构。geo 主要是做地理位置的计算,通过经度和纬度来定位位置,经过运算可以得到距离,附近范围的坐标等等。像比如美团外卖的附近商家,地图的距离测算,都可以通过 geo 的结构来进行实现,以上。

题目解析

这道题问的比较少,如果在问你基础数据类型的时候,你补了一句,还有三种高级类型,如果面试官感兴趣的话,会继续的追问你。不过三种里面最常用的就是 bitmap,其他用的比较少,重点关注 bitmap 即可。hyperloglog,geo 都不常见,无需关注。作为了解即可。

面试得分点

bitmap,二进制位统计,签到功能,hyperloglog,大数据量统计,geo,地理位置,经纬度,附近的人

题目详细答案

一、 Bitmaps

位图就是一个用二进制位(0和1)来表示数据的结构。可以把它想象成一排开关,每个开关只能是开(1)或者关(0)。这些开关排成一行,从左到右编号,编号从0开始。

目的就是操作某一个位置的数据变成 1 或者 0。

主要操作命令

1
SETBIT jichi 4 1

按照上图,我们其实就是把 4 位设置成了 1。

1
GETBIT jichi 4
1
BITCOUNT jichi  //获取bitmap里面有多少个1

举个例子

基于上面我们按照大家常见的比如用户签到系统,来做一个例子的说明。

假设我们有一个用户签到系统,我们可以用 bitmap 来记录每个用户每天是否签到。比如,一个月有30天,我们可以用30个位来表示这个月的签到情况,我们就可以如此设计。

第1天签到:第0位设为1。第2天没签到:第1位设为0。第3天签到:第2位设为1。以此类推…

这个例子就用上面三个命令即可完成,setbit 设置签到位置,getbit 判断某一天有没有签到,bitcount 获取总共签了多少次到。

假设用户在第1天和第3天签到,那么 bitmap 的值就是下面这样的:

1
101000000000000000000000000000

为什么用 bitmap

类似签到,活跃情况,这些场景,假设我们用数据库存储,可能是一条一条的,统计起来也费时和麻烦,如果使用 bitmap,可以进行非常快速的统计,并且 bitmap 每个位只是二进制位,非常节省空间。

扩展起来,其实比如判断用户有没有权限,假设把某个权限作为一个位置,新增作为 1,删除作为 2,那么这种场景也是可以很快知道用户是否有权限的一种方式。

总之涉及单位置判断的,是否的场景,bitmap 比较靠谱。


二、HyperLogLog

HyperLogLog 用于计算数据集中不重复元素的数量,是 Redis 提供的一种基数统计的数据结构。当我们需要统计大量数据中有多少不同的元素时,直接存储所有元素会占用大量内存。例如,统计一个网站一天内有多少不同的IP地址访问。如果直接存储所有IP地址,内存消耗会非常大。HyperLogLog通过巧妙的数学方法,可以在很小的内存占用下,提供一个非常接近的估算值。在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基 数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。

什么是基数??

比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素)为5。 基数估计就是在误差可接受的范围内,快速计算基数。

常用命令

HyperLogLog 在 Redis 中以字符串的形式存在,但是只能作为计数器来使用,并不能获取到集合的原始数据。

主要涉及三个命令:

添加元素:

1
2
PFADD key element1 element2 ...
例如:PFADD jichihll jichi jitui

估算基数:

1
2
3
4
PFCOUNT key
PFCOUNT jichihll

返回的就是 2

合并多个HyperLogLog:

1
PFMERGE destkey sourcekey1 sourcekey2 ...

应用场景

凡是大量的数据下,统计不同数据的数量的情况都可以使用,非常的方便,同时要接受误差的场景。比如

网站访问统计:估算鸡翅 club 网站每天有多少独立访客。

日志分析:估算日志文件中有多少不同的错误类型。

三、 Geospatial Indexes

Geo数据指的是与地理位置相关的数据。简单来说,就是关于“东西在哪里”的数据。它可以描述物体的位置、形状和关系,比如城市的坐标、商店的位置、路线的路径等等。

有主要的三个要素,经度,纬度,和位置名称。

比如鸡哥所在的位置

1
GEOADD jichi 16.281231 37.1231241 jd

常用命令

添加地理位置:

1
2
GEOADD key longitude latitude member [longitude latitude member ...]
GEOADD cities 116.4074 39.9042 "Beijing"

获取地理位置:

1
2
3
4
5
GEOPOS key member
GEOPOS cities "Beijing"
会返回
116.4074
39.9042

计算距离:

1
2
GEODIST key member1 member2 [unit]
GEODIST cities "Beijing" "Shanghai" km(计算北京和上海之间的距离,单位为公里)

查找附近的位置:

1
2
GEORADIUS key longitude latitude radius [unit]
GEORADIUS cities 116.4074 39.9042 100 km(查找北京附近100公里内的所有城市)

查找某个位置附近的位置:

1
2
GEORADIUSBYMEMBER key member radius [unit]
GEORADIUSBYMEMBER cities "Beijing" 100 km(查找北京附近100公里内的所有城市)

georadius 以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。

georadiusbymember 和 GEORADIUS 命令一样, 都可以找出位于指定范围内的元素, 但是 georadiusbymember 的中心点是由给定的位置元素决定的, 而不是使用经度和纬度来决定中心点。

应用场景

附近的人:比如类似微信的附近的人,以自己为中心,找其他的人,这种场景,就可以使用GEORADIUS 。

基于地理位置推荐:比如推荐某个位置附近的餐厅,都可以实现

计算距离:大家会遇到这种场景,比如当你购物的时候,美团外卖会告诉你商家距您多远,也可以通过 geo 来进行实现。

四、Stream(不是重点)

stream 是 redis5.0 版本后面加入的。比较新,以至于很多老八股题目,都没有提到这个类型。还有就是本身应用度的场景真的不多,类似 mq,但是如果 mq 的场景,大家一般会选择正宗的 rokcetmq 或者 rabbit 或者 kafka,所以这种类型,大家稍微知道即可。

Redis中的流结构用来处理连续不断到达的数据。你可以把它想象成一条流水线,数据像流水一样源源不断地流过来,我们可以在流水线的不同位置对这些数据进行处理。

主要目的是做消息队列,在此之前 redis 曾经使用发布订阅模式来做,但是发布订阅有一个缺点就是消息无法持久化。非常脆弱,redis 宕机,断开这些,都会产生造成丢失。stream 提供了持久化和主备同步机制。

概念解析

消息(Message):流中的每一条数据。每条消息都有一个唯一的ID和一组字段和值。

流(Stream):存储消息的地方。可以把它看作一个消息队列。

消费者组(Consumer Group):一个或多个消费者组成的组,用来处理流中的消息。

消费者(Consumer):处理消息的终端,可以是应用程序或服务。

应用场景

如果需要轻量级,很轻很轻,没有 mq 的情况下,可以使用 redis 来做,适合处理需要实时处理和快速响应的数据。比如做成用户消息实时发送和接收、服务器日志实时记录和分析、传感器数据实时收集和处理。

不过需要注意的是,正常来说 mq,mqtt 等等在各自场景有比较好的应用。

常见命令

添加消息到流:

1
2
3
XADD stream-name * field1 value1 [field2 value2 ...]
XADD mystream * user jichi message "Hello, world!"
他会向流mystream添加一条消息,消息内容是user: jichi, message: "Hello, world!"。

读取消息:

1
2
3
XREAD COUNT count STREAMS stream-name ID
XREAD COUNT 2 STREAMS mystream 0
会从流mystream中读取前两条消息,也就是读取到jichi 的hello world

创建消费者组:

1
2
3
XGROUP CREATE stream-name group-name ID
XGROUP CREATE mystream mygroup 0
会为流mystream创建一个名为mygroup的消费者组。

消费者组读取消息:

1
2
3
XREADGROUP GROUP group-name consumer-name COUNT count STREAMS stream-name ID
XREADGROUP GROUP mygroup consumer1 COUNT 2 STREAMS mystream >
会让消费者组mygroup中的消费者consumer1读取流mystream中的前两条消息。

确认消息处理完成:

消费者处理完成,应该进行 ack。

1
2
3
XACK stream-name group-name ID
XACK mystream mygroup 1526569495631-0
确认消费者组mygroup已经处理完了ID为1526569495631-0的消息。

redis 过期策略有哪些

发表于 2025-02-11 | 更新于 2025-06-22 | 分类于 面试
字数统计 | 阅读时长

👌redis过期策略有哪些?

口语化回答

好的,面试官,过期策略主要分为主动和被动,主动又分为定时、定期,被动就是常说的惰性清理。先说结论,redis 采取的方案是定期+惰性配合的方式来进行实现。定期策略主要是通过周期性执行的函数来扫描即将过期的键,立马将其进行失效操作。这种方式比较消耗 cpu。于是产生了定期操作,没隔多少 ms 来进行执行,这种减少了 cpu 的消耗。也能比较准时的删除过期的键。算是定时的一种优化,比较难的点就是寻求平衡。最后就是拖性删除,所有的 key 即使过期了也不会立马删除,当这个键过期之后,下一次访问的时候,才会被删除,容易造成内存泄漏的问题。最后 oom 就会触发内存淘汰策略了,优点就是大大减轻了 cpu 的压力。以上两种方式配合,能达到一个平衡。

题目解析

常考题,很多人把过期策略和淘汰策略混在一起。二者既不同,当惰性删除的时候,又有联系。大家要注意多层面来回答,注意辩证 cpu 性能的问题处理。

面试得分点

定期删除,定时删除,惰性删除,主动于被动

题目详细答案

从行为上,我们可以把过期策略分为两大点。主动删除,被动删除。主动删除又分为定时删除和定期删除。

主动删除

定时删除

当设置键的过期时间时,Redis会为该键创建一个定时器,当过期时间到达时自动删除该键。redis.c 下的 activeExpireCycle 函数实现了定期删除粗略,配合 Redis的服务器的 serverCron函数,在服务器周期执行serverCron 的时候,activeExpireCycle函数就会被调用,在一定的时间内,分多次遍历 redis 中的数据库,从数据库的expires字典中检查一部分键的过期时间,此操作是随机性的,然后删除其中的过期键。

优点:删除操作会在数据到期时立即进行,确保内存及时释放。

缺点:定时器的管理会消耗系统资源,特别是在大量键设置过期时间的情况下,删除 key 会对响应时间和吞吐量产生影响。

定期删除

Redis会定期扫描数据库中的键,并删除其中已过期的键。通过随机抽取一定数量的键,并检查它们是否过期,如果过期就删除,Redis默认每隔100ms(可以通过配置文件中的hz参数进行调整)就执行一次过期扫描任务。

配置redis.conf的hz选项,默认为10,1s刷新的频率。即1秒执行10次,相当于100ms执行一次,hz值越大,说明刷新频率越快,Redis性能损耗也越大

优点:通过限制删除操作执行的时长和频率来减少删除操作对CPU的影响,同时能有效释放过期键占用的内存。

缺点:难以确定删除操作执行的时长和频率,如果执行的太频繁,会对CPU造成负担,就变成了定时删除;如果执行的太少,则过期键长时间占用的内存没有及时释放,造成内存浪费。

内存不足

当Redis的内存达到最大限制时,还会触发内存淘汰策略,策略不同决定哪些数据会被删除以腾出空间。
no eviction:禁止淘汰,达到内存限制时拒绝新的写请求。
allkeys-lru:从所有键中淘汰最近最少使用的键。
volatile-lru:从设置了过期时间的键中驱逐最近最少使用的键。
allkeys-random:从所有键中随机驱逐键。
volatile-random:从设置了过期时间的键中随机驱逐键。
volatile-ttl:从设置了过期时间的键中驱逐剩余时间最短的键。

被动删除

惰性删除

Redis不会在键过期时立即删除它,而是在下一次访问这个键时检查其是否过期,然后删除过期的键。假设这个键已经过期,但是后面一直没有被访问,则会永远存在。不会被删除,这就是惰性删除。

惰性删除策略由db.c/expireIfNeeded函数实现,所有读写数据库的Redis命令在执行之前都会调用expireIfNeeded函数对输入键进行检查。如果输入键已经过期,那么expireIfNeeded函数将输入键从数据库中删除;如果输入键未过期,那么expireIfNeeded函数不做动作。

优点:惰性删除不会增加额外的系统开销,不浪费 cpu,只在访问时进行检查。

缺点:如果某个键永远不会被访问,即使设置了过期时间,它也不会被自动删除,造成内存泄漏问题。

Redis 实际使用的是定期删除+惰性删除的方式!定期删除减少 cpu 消耗和浪费,配合惰性删除,二次检查保险。

Mysql中的索引

发表于 2024-11-20 | 更新于 2025-06-22 | 分类于 面试
字数统计 | 阅读时长

一、MySQL 为什么使用 B+ 树?

与B+ 树相比,平衡二叉树、红黑树在同等数据量下,高度更高,性能更差,而且它们会频
繁执行再平衡过程,来保证树形结构平衡。
与B+ 树相比,跳表在极端情况下会退化为链表,平衡性差,而数据库查询需要一个可预期
的查询时间,并且跳表需要更多的内存
与B+ 树相比,B 树的数据存储在全部节点中,对范围查询不友好;非叶子节点存储了数
据,导致内存中难以放下全部非叶子节点,可能需要磁盘IO;

二、MySQL 对 NULL 值的索引支持特点如下:

索引会存储并支持 NULL 值
查询条件 IS NULL 和 IS NOT NULL 可以利用索引。

  • 与其他数据库相比,MySQL 的索引对 NULL 的支持更完善且优化更好。

索引的优化

优化方面:sql本身优化、服务器/引擎(配置)优化、操作系统优化、硬件资源问题

1、sql优化达到的目的:

减少磁盘IO:避免全面扫描、使用索引(覆盖索引)
减少内存cpu消耗:尽可能减少排序、分组、去重之类的操作
修改索引或者说表定义变更的核心问题是数据库会加表锁,直到修改完成

示例:

1、使用覆盖索引和索引下推减少回表; 索引下推中范围查询排序的字段使用不当,使用了index where,增加字段走索引下推,优化 ORDER BY 将排序列加入索引。

2、用 WHERE 替换 HAVING(注意sql的执行顺序);如果不是使用聚合函数来作为过滤条件,最好还是将过滤条件优先写到WHERE 里面。

HashMap

发表于 2024-11-15 | 更新于 2025-06-22 | 分类于 面试
字数统计 | 阅读时长

1、HashMap 线程不安全 key和value能存null值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

HashMap<Object, Object> map = new HashMap<>();
map.put(1,1);
map.put(1,1);
map.put(null,2);
map.put(null,"12");
System.out.println(map.get(1));
````
### HashMap 能存null值 不会出现空指针的原因是:HashMap 的key如果是null 底层在去hashCode 的时候是默认赋值为0;

```java
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

2、ConcurrentHashMap 线程安全是因为加了锁 key和value不能存null值

1
2
ConcurrentHashMap<Object, Object> objectObjectConcurrentHashMap = new ConcurrentHashMap<>();
objectObjectConcurrentHashMap.put(null, null);

key 如果是null,key.hashCode() 会报空指针

  • 部分原码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) {}
}
}
}

Mysql数据库调优

发表于 2024-11-13 | 更新于 2025-06-22 | 分类于 数据库
字数统计 | 阅读时长

数据库调优到底调什么?

  • 调SQL语句:根据需求创建结构良好的SQL语句【实现同一个需求,SQL语句写法很多】
  • 调索引:索引创建原则
  • 调数据库表结构
  • 调MySQL配置:最大连接数,连接超时,线程缓存,查询缓存,排序缓存,连接查询缓存…
  • 调MySQL宿主机OS:TCP连接数,打开文件数,线程栈大小…
  • 调服务器硬件:更多核CPU、更大内存
  • MySQL客户端:连接池(MaxActive,MaxWait),连接属性

前置工作:

1、数据库压力测试

  • 配置数据库驱动
  • 配置线程组
  • 配置 JDBC 连接池
  • 添加 JDBC 请求
  • 添加结果监听器

2、客户端-连接池

连接池参数设置

  • MaxWait 参数表示从连接池获取连接的超时等待时间,单位毫秒。
  • MaxActive

正式调优

一、SQL语句优化

1、查看SQL执行计划【Explain】

  • id:SELECT识别符,这是SELECT查询序列号。
  • select_type:表示单位查询的查询类型,比如:普通查询、联合查询(union、union all)、子查询等复杂查询。
  • table:表示查询的表
  • partitions:使用的哪些分区(对于非分区表值为null)。
  • type(重要)表示表的连接类型。
  • possible_keys:此次查询中可能选用的索引
  • key:查询真正使用到的索引
  • key_len:显示MySQL决定使用的索引size
  • ref:哪个字段或常数与 key 一起被使用
  • rows:显示此查询一共扫描了多少行,这个是一个估计值,不是精确的值。
  • filtered: 表示此查询条件所过滤的数据的百分比
  • Extra:额外信息

2、索引优化

3、深分页LIMIT优化

4、子查询优化 (减少子查询,多用join)

5、其他查询优化

  • 小表驱动大表
  • 避免全表扫描
  • WHERE条件中尽量不要使用not in语句,建议使用not exists
  • 利用慢查询日志、explain执行计划查询、show profile查看SQL执行时的资源使用情况

6、SQL语句性能分析

二、数据库优化

1、慢查询日志

2、连接数max_connections

3、线程使用情况

4、数据库优化-结构优化

  • 将字段很多的表分解为多个表
  • 增加中间表
  • 增加冗余字段

三、服务器层面的优化

1、缓冲区优化

  • 修改buffer_pool

2、减少磁盘写入次数

3、MySQL数据库配置优化

4、服务器硬件优化

Mysql笔记

发表于 2024-11-13 | 更新于 2025-06-22 | 分类于 数据库
字数统计 | 阅读时长

1、隔离级别

为什么mysql默认使用RR;

级别太高会影响并发度,太低会出现脏读现象;
最重要的是主从同步的问题;
当binlog的格式为statement时,binlog 里面记录的就是 SQL 语句的原文
为了兼容历史上的那种statement格式的bin log。
在RC情况下,主库因为隔离级别没有问题,但是从库会发生数据不一致的问题;
在RR中不会出现这种问题,是因为在其中存在间隙锁和临建锁,确保一个事务提交以后才能执行下一个事务;

2、RR和RC区别

只有这两个才会使用快照读;
在 RC 中,每次读取都会重新生成一个快照,总是读取行的最新版本

在数据库的 RC 这种隔离级别中,还支持”半一致读” ,一条update语句,如果 where 条件匹配到的记录已经加锁,那么InnoDB会返回记录最近提交的版本,由MySQL上层判断此是否需要真的加锁。

在 RC 中,只会对索引增加Record Lock,不会添加Gap Lock和Next-Key Lock。
在 RR 中,为了解决幻读的问题,在支持Record Lock的同时,还支持Gap Lock和Next-Key Lock;
所以 RC并发更好,减少锁的问题;

在RC 中 读取到别的事务修改的值其实问题不太大的,只要修改的时候的不基于错误数据就可以了,所以我们都是在核心表中增加乐观锁标记,更新的时候都要带上锁标记进行乐观锁更新

Innodb的RR到底有没有解决幻读?

间隙锁解决了部分当前读的幻读问题;
MVCC解决了快照读的幻读问题;

MVCC

  • 时机
    在 RC 中,每次读取都会重新生成一个快照,总是读取行的最新版本。
    在 RR 中,快照会在事务中第一次SELECT语句执行时生成,只有在本事务中对数据进行更改才会更新快照。

在同一个事务里面,如果既有快照读,又有当前读,那是会产生幻读的

MVCC只是解决了快照读中的欢度问题,但是对于当前读还是会有幻读的问题;
在RR中,如果本事务中发生了数据的修改,那么就会更新快照,那么最后一次查询的结果也就发生了变化。

间隙锁是导致死锁的一个重要原因

MVCC理解

并发问题:MVCC解决是读-写并发的问题;

快照读是MVCC实现的基础,而当前读是悲观锁实现的基础。

  • undo log是Mysql中比较重要的事务日志之一,顾名思义,undo log是一种用于回退的日志,在事务没提交之前,MySQL会先记录更新前的数据到 undo log日志文件里面,当事务回滚时或者数据库崩溃时,可以利用 undo log来进行回退。
  • 针对一条记录的多个快照,通过隐藏主键+回滚指针生成一个快照链表
  • Read View 主要来帮我们解决可见性的问题的, 即他会来告诉我们本次事务应该看到哪个快照,不应该看到哪个快照。

本地缓存

发表于 2024-11-12 | 更新于 2025-06-22 | 分类于 缓存
字数统计 | 阅读时长

多级缓存

多级缓存是通过在数据访问路径的不同层级上部署缓存来提高数据访问效率的技术。通常包括:

  1. 本地缓存:位于应用服务器本地,访问速度非常快,但容量有限。常用的本地缓存框架有Caffeine和Guava,它们提供了缓存过期策略、缓存项管理等高级功能。

  2. 分布式缓存:通常部署在多台服务器上,容量大,适合存储热点数据。常用的分布式缓存框架有Redis和Memcached。分布式缓存通过网络访问,速度比本地缓存慢,但提供了更高的可用性和可扩展性。

查询逻辑

在多级缓存系统中,查询数据的逻辑通常是:

  1. 首先查询本地缓存。
  2. 如果本地缓存未命中,则查询分布式缓存。
  3. 如果分布式缓存命中,则将结果存入本地缓存(通常称为“回写”或“预热”缓存)。
  4. 如果分布式缓存也未命中,则可能需要查询数据库或其他持久化存储。

代码示例

您提供的代码示例中有几个问题和遗漏,下面是修正后的版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public String query(String key) {
// 查询本地缓存
String localResult = localCache.get(key);
if (localResult != null) {
// 本地缓存命中,直接返回结果
return localResult;
}

// 本地缓存未命中,查询分布式缓存
String remoteResult = remoteCache.get(key);
if (remoteResult != null) {
// 分布式缓存命中,将结果存入本地缓存
localCache.put(key, remoteResult); // 注意:这里需要指定key和value
return remoteResult;
}

// 分布式缓存也未命中,这里可能需要处理,例如查询数据库
// 注意:此示例未包含该逻辑

// 如果没有其他数据源或查询失败,返回null或适当的默认值
return null;
}

特殊场景:黑名单与Bloom Filter

对于某些特殊场景,如黑名单检查,可以使用Bloom Filter来进一步优化。Bloom Filter是一种空间效率很高的概率型数据结构,用于判断一个元素是否在一个集合中。它允许一定程度的假阳性(即判断为在集合中但实际上不在),但不会有假阴性(即判断为不在集合中但实际上在)。

在使用Bloom Filter作为本地缓存的场景中,如果Bloom Filter判断某个元素可能在集合中(即可能命中黑名单),则需要再次查询分布式缓存或数据库以确认。如果确认命中,则可以进行相应的处理;如果未命中,则可以直接返回结果。

这种组合使用Bloom Filter和分布式缓存的方法可以在保持高性能的同时,降低对分布式缓存的访问频率和带宽消耗。

你提出的问题涉及到了本地缓存一致性的保证方法,这是一个在使用本地缓存时经常需要考虑的问题。以下是对你问题的详细解答:

如何保证本地缓存的一致性?

本地缓存的一致性问题主要是由于数据在多个节点(或进程)间的不同步更新导致的。为了解决这个问题,我们可以采取以下几种策略:

  1. 使用版本号或时间戳:

    • 当本地缓存更新时,将新的数据及其版本号或时间戳存储到数据库中。
    • 其他节点在访问本地缓存时,先检查数据库中的版本号或时间戳,如果发现自己本地缓存的版本较旧,则从数据库中更新本地缓存。
  2. 借助配置中心:

    • 当本地缓存更新时,将变更通知到配置中心。
    • 配置中心将变更推送到所有相关节点,节点监听配置变化并更新本地缓存。
  3. 使用消息队列(MQ):

    • 当本地缓存更新时,发送一个广播消息到消息队列。
    • 所有订阅了该消息的节点接收到消息后,更新自己的本地缓存。
  4. 设置合理的失效时长:

    • 根据业务对数据一致性的需求,设置本地缓存的失效时长。
    • 在失效时长内,本地缓存的数据是有效的,但可能不是最新的。失效后,查询将触发从分布式缓存或数据库中更新本地缓存。
  5. 使用自动更新策略:

    • 一些缓存库(如Caffeine)支持自动更新策略。
    • 可以配置定时刷新策略,让缓存库在后台定期从分布式缓存或数据库中更新数据。

注意事项:

  • 评估数据变化频率:频繁更新的数据不适合放在本地缓存中。
  • 评估业务一致性需求:根据业务需求决定是否使用本地缓存,以及能接受的不一致时长。
  • 选择合适的缓存库:一些缓存库提供了丰富的配置选项,可以帮助更好地管理本地缓存。

RocketMQ消息分发:

RocketMQ支持两种消息模式:广播消费和集群消费。

  • 广播消费:消息会发送给集群内的所有消费者,确保每个消费者都能收到消息。这种模式适用于需要向所有消费者广播消息的场景。
  • 集群消费:消息只会被发送到集群中的一个消费者(根据负载均衡算法选择)。这种模式适用于需要处理大量消息且不需要每个消费者都收到所有消息的场景。

总结:

保证本地缓存的一致性是一个复杂的问题,需要根据业务需求和数据变化频率来选择合适的策略。在实际应用中,通常会结合多种策略来确保数据的一致性和系统的性能。同时,也需要关注缓存库的选择和配置,以充分利用其提供的特性来优化系统性能。

高并发下的缓存问题

发表于 2024-11-12 | 更新于 2025-06-22 | 分类于 高并发
字数统计 | 阅读时长

在高并发的情况下,特别是当 本地缓存(如 Guava, Caffeine 等)接到每秒 2000 万(QPS)的请求时,系统面临的挑战主要有两个方面:

缓存的读写性能瓶颈:即如何高效地处理大量的读写请求。
缓存的内存管理问题:如何合理利用内存,以应对大量数据的存储需求,并防止内存泄漏或过载。
为了应对这些挑战,可以采取以下几种优化和设计策略:

  1. 选择合适的本地缓存库
    对于高并发场景,选择一个高效的本地缓存库至关重要。常见的库有:

Caffeine:一个高性能的 Java 本地缓存库,基于 Google 的 Guava,通过 基于时间的过期策略 和 大小限制 来管理缓存,并提供 异步加载 和 弱引用缓存 支持。
Guava:较为成熟的缓存库,但对于高并发处理可能稍逊色于 Caffeine。
Caffeine 的性能通常优于 Guava,尤其在处理大量并发请求时,它在 缓存淘汰算法(如 LRU)和 内存管理(如使用弱引用、自动过期等)上做了很多优化。

  1. 内存和缓存的容量管理
    本地缓存的一个关键问题是如何管理 缓存容量,尤其是在高并发时。可以通过以下方式进行优化:

设置合理的缓存大小限制:根据应用的内存容量设置合理的缓存大小,避免缓存占用过多内存导致的内存溢出。常见的策略是 按大小(maximumSize)或按时间(expireAfterWrite)限制缓存大小。

LRU(Least Recently Used)策略:通过限制缓存条目的数量,当缓存的条目数超过限制时,自动淘汰最久未使用的数据。

自动过期(TTL):为缓存数据设置一个过期时间(例如 expireAfterWrite 或 expireAfterAccess),避免缓存占用过多内存。

批量过期机制:在高并发环境下,可以通过批量清除缓存或定期刷新缓存来减少单个请求的压力。

  1. 缓存穿透、雪崩和击穿的处理
    在高并发情况下,避免 缓存穿透、缓存雪崩 和 缓存击穿 是非常重要的:

缓存穿透:是指查询的数据在缓存和数据库中都不存在。为了避免这种情况,可以使用 布隆过滤器(Bloom Filter)来快速判断某个数据是否存在,避免无效查询。

缓存雪崩:是指缓存中的大量数据在同一时刻过期,导致大量请求直接访问数据库。可以通过以下方式避免:

设置 不同的过期时间(例如,随机化过期时间,避免所有缓存同时过期)。
使用 后台异步更新 机制,确保缓存能够及时更新。
缓存击穿:是指某一时刻大量并发请求访问同一缓存条目,导致缓存失效后直接访问数据库。可以通过以下方式避免:

使用 锁机制(例如 ReentrantLock、synchronized 等)来确保只有一个请求能去加载数据,其他请求等待加载完成后共享缓存结果。
使用 队列或 信号量 来限制并发请求对数据库的访问。
4. 使用异步加载和缓存预加载
在高并发场景下, 异步加载 缓存可以显著提高性能,避免同步加载带来的性能瓶颈。

异步加载:通过异步方式加载缓存数据,使得当缓存数据不存在时,其他线程可以并发等待数据的加载结果,而不是阻塞。

缓存预加载:对于一些访问频繁的数据,可以提前预加载到缓存中,避免高并发时大量缓存未命中的情况。

  1. 多级缓存策略
    在高并发的情况下,采用 多级缓存 策略非常有效。例如:

本地缓存 + 分布式缓存:对于极其频繁的查询,使用本地缓存(如 Caffeine)进行快速响应;对于数据量较大、频率较低的查询,则使用 分布式缓存(如 Redis)来保证数据的一致性和持久性。

本地缓存与二级缓存结合:当本地缓存未命中时,可以查询 Redis 等分布式缓存。若 Redis 中也没有,则最终回退到数据库。

  1. 限流与请求排队
    在高并发环境中,可以使用 限流 和 请求排队 技术来避免请求过载,减少对本地缓存和数据库的压力。

限流:可以通过 令牌桶 或 漏桶算法 实现请求限流,确保系统不会因为过载而崩溃。
请求排队:对于高并发请求,可以通过队列对请求进行排队处理,保证系统的稳定性。
7. 监控与预警
在高并发情况下,必须监控缓存的使用情况、命中率、缓存大小和过期情况等,以便及时发现和处理性能瓶颈。

监控 缓存命中率 和 缓存填充率,确保缓存系统的健康运行。
对 缓存容量、加载时间、过期时间、缓存清除策略 等进行实时监控,及时调整系统参数。
8. 分布式缓存的结合
在极高 QPS 的情况下,单机本地缓存 可能无法承载如此大的流量,此时可以考虑使用 分布式缓存(如 Redis、Memcached 等)作为备份或辅助缓存。

分布式缓存 提供了跨机器的缓存能力,能有效扩展缓存容量和处理高并发的请求。
与本地缓存结合,采用 分层缓存 模型,将常用的数据放在本地缓存中,其他数据存储在 Redis 等分布式缓存中。

小结

当面对每秒 2000 万(QPS)的请求时,本地缓存和分布式缓存的设计需要特别注意:

使用 高效的缓存库(如 Caffeine)来处理高并发的缓存读写。
合理设置缓存的 大小限制 和 过期策略,避免内存溢出和缓存过载。
通过 多级缓存、异步加载、缓存预加载 和 限流机制 来减轻系统负载,确保系统的稳定性和性能。
缓存穿透、缓存雪崩 和 缓存击穿 的问题也需要特别注意,通过布隆过滤器、过期时间随机化、加锁等机制来应对。
如果有更多关于高并发缓存设计的具体问题,随时可以提问!

AtomicInteger的实现方式及场景

发表于 2024-11-12 | 更新于 2025-06-22 | 分类于 面试
字数统计 | 阅读时长

👌AtomicInteger的实现方式及场景

题目详细答案

AtomicInteger是 Java 中提供的一种用于在多线程环境下进行原子操作的类。它属于java.util.concurrent.atomic包,提供了一种无锁的线程安全方式来操作整数值。AtomicInteger基于底层的硬件原子操作(例如 CAS 操作)实现,确保在多线程环境中进行高效的数值更新。

AtomicInteger的实现方式

AtomicInteger通过使用 CAS(Compare-And-Swap)操作来实现原子性。CAS 是一种硬件级别的原子操作,能够确保在多线程环境下对变量进行无锁的更新。

  1. 底层变量:使用volatile关键字声明一个int类型的变量,确保变量的可见性。
  2. CAS 操作:通过Unsafe类提供的原子操作方法来实现无锁更新。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerExample {
private final AtomicInteger atomicInteger = new AtomicInteger(0);

public void increment() {
atomicInteger.incrementAndGet();
}

public int getValue() {
return atomicInteger.get();
}

public static void main(String[] args) {
AtomicIntegerExample example = new AtomicIntegerExample();

Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
};

Thread t1 = new Thread(task);
Thread t2 = new Thread(task);

t1.start();
t2.start();

try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println("Final value: " + example.getValue());
}
}

在这个示例中,我们使用AtomicInteger实现了一个简单的计数器,并在两个线程中并发地对计数器进行自增操作。最终的结果是线程安全的。

AtomicInteger的主要方法

get(): 获取当前值。

set(int newValue): 设置当前值。

getAndSet(int newValue): 获取当前值,并设置为新值。

compareAndSet(int expect, int update): 如果当前值等于预期值,则将其设置为新值。

getAndIncrement(): 获取当前值,并自增。

incrementAndGet(): 自增,并获取自增后的值。

getAndDecrement(): 获取当前值,并自减。

decrementAndGet(): 自减,并获取自减后的值。

getAndAdd(int delta): 获取当前值,并加上指定的值。

addAndGet(int delta): 加上指定的值,并获取加后的值。

使用场景

  1. 计数器:在多线程环境中用于计数,例如统计请求数、用户数等。
  2. 序列生成器:生成全局唯一的序列号。
  3. 并发控制:用于实现并发控制机制,如限流器、资源池等。
  4. 状态管理:用于管理多线程环境下的共享状态,确保状态更新的原子性。
  5. 锁的替代:在某些情况下,可以使用AtomicInteger来替代传统的锁机制,减少锁竞争,提高性能。

优缺点

优点

  1. 高效:基于硬件级别的原子操作,性能高于使用锁的方式。
  2. 无锁:避免了锁竞争和上下文切换,减少了开销。
  3. 简单易用:提供了丰富的方法,简化了并发编程。

缺点

  1. 适用范围有限:适用于简单的数值更新操作,对于复杂的数据结构或操作,仍需要使用锁。
  2. CAS 操作失败重试:在高并发情况下,CAS 操作可能会频繁失败,需要多次重试,影响性能。

原文: https://www.yuque.com/jingdianjichi/xyxdsi/vpbqzzcb5t0ur8hm

CAS与Synchronized的使用情景?

发表于 2024-11-12 | 更新于 2025-06-22 | 分类于 面试
字数统计 | 阅读时长

👌CAS与Synchronized的使用情景?

题目详细答案

CAS(Compare-And-Swap)和synchronized是两种不同的并发控制机制,适用于不同的使用情景。

CAS(Compare-And-Swap)

特点:

无锁操作:CAS 是一种无锁的并发控制机制,不需要显式地获取锁。

高性能:由于不需要锁定资源,CAS 的性能通常比锁机制更高,尤其在高并发场景下。

乐观锁:CAS 基于乐观锁的思想,假设竞争不频繁,只有在检测到冲突时才会重试。

原子操作:CAS 操作是原子的,通常由硬件指令支持(如 x86 架构的cmpxchg指令)。

使用场景:

  1. 高并发场景:适用于需要高并发访问的场景,如计数器、自旋锁、无锁队列等。
  2. 轻量级操作:适用于操作简单且执行时间短的场景,因为 CAS 操作本身是原子的,但如果操作复杂,可能会导致频繁重试。
  3. 避免锁竞争:在锁竞争激烈的场景下,CAS 可以避免线程阻塞,提高系统的吞吐量。

示例:

使用AtomicInteger类实现 CAS 的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.util.concurrent.atomic.AtomicInteger;

public class CASExample {
private AtomicInteger counter = new AtomicInteger(0);

public void increment() {
int oldValue, newValue;
do {
oldValue = counter.get();
newValue = oldValue + 1;
} while (!counter.compareAndSet(oldValue, newValue));
}

public int getCounter() {
return counter.get();
}

public static void main(String[] args) {
CASExample example = new CASExample();
example.increment();
System.out.println(example.getCounter()); // 输出: 1
}
}

synchronized

特点:

互斥锁:synchronized是一种互斥锁机制,确保同一时刻只有一个线程可以执行被锁定的代码块。

简单易用:synchronized是 Java 语言内置的关键字,使用简单,易于理解和维护。

阻塞操作:被锁定的线程会进入阻塞状态,直到获取到锁。

内存可见性:synchronized确保锁释放后,修改的变量对其他线程可见。

使用场景:

  1. 复杂操作:适用于需要对共享资源进行复杂操作的场景,确保操作的原子性和一致性。
  2. 低并发场景:适用于并发度不高的场景,因为synchronized会导致线程阻塞,进而影响性能。
  3. 需要内存可见性:适用于需要确保变量修改对其他线程立即可见的场景。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class SynchronizedExample {
private int counter = 0;

public synchronized void increment() {
counter++;
}

public synchronized int getCounter() {
return counter;
}

public static void main(String[] args) {
SynchronizedExample example = new SynchronizedExample();
example.increment();
System.out.println(example.getCounter()); // 输出: 1
}
}

CAS适用于高并发、轻量级操作和避免锁竞争的场景,具有高性能的优势,但可能会导致重试。synchronized适用于复杂操作、低并发场景和需要内存可见性的场景,使用简单,但会导致线程阻塞和性能下降。

原文: https://www.yuque.com/jingdianjichi/xyxdsi/ehiutd9peq73al3g

Mysql事务

发表于 2024-11-10 | 更新于 2025-06-22 | 分类于 数据库
字数统计 | 阅读时长

一、事务

事务指的是逻辑上的一组操作,组成这组操作的各个单元要么全都成功,要么全都失败。
事务作用:保证在一个事务中多次SQL操作要么全都成功,要么全都失败。

1、特性

  • 原子性:事务的操作要么都发生,要么都不发生;
  • 一致性:事务前后数据的完整性必须保持一致;
  • 隔离性:一个用户的事务不能被其他用户的事务干扰;多个并发事务之间的数据相互隔离;隔离性由隔离级别保障!
  • 持久性:事务一旦提交,对数据的修改时永久性的;

2、事务并发问题

  • 脏读:一个事务读到了另一个事务未提交的数据
  • 不可重复读:一个事务读到了另一个事务已经提交(update)的数据。引发事务中的多次查询结果不
    一致
  • 虚读 /幻读:一个事务读到了另一个事务已经插入(insert)的数据。导致事务中多次查询的结果不一
    致
  • 丢失更新的问题!

3、隔离级别

  • read uncommitted 读未提交【RU】,一个事务读到另一个事务没有提交的数据
    存在:3个问题(脏读、不可重复读、幻读)。
  • read committed 读已提交【RC】,一个事务读到另一个事务已经提交的数据
    存在:2个问题(不可重复读、幻读)。
    解决:1个问题(脏读)
  • repeatable read:可重复读【RR】,在一个事务中读到的数据始终保持一致,无论另一个事务是
    否提交
    解决:3个问题(脏读、不可重复读、幻读)
  • serializable 串行化,同时只能执行一个事务,相当于事务中的单线程
    解决:3个问题(脏读、不可重复读、幻读)

二、事务底层

1、丢失更新问题

  • 两个事务针对同一个数据进行修改操作时会丢失更新!
  • 解决方案:
    • 基于锁并发控制LBCC
    • 基于版本并发控制MVCC

三、MVCC

核心思想是读不加锁,读写不冲突

MVCC 实现原理关键在于数据快照,不同的事务访问不同版本的数据快照,从而实现事务下对数据的隔离级别

MVCC,全称Multiversion Concurrency Control,即多版本并发控制,是数据库领域中一种用于管理并发数据访问的机制。与数据库锁相似,MVCC也是一种并发控制的解决方案,但它侧重于通过维护数据的多个版本来避免读写冲突,从而提高并发性能。

MVCC的基本原理

在数据库中,对数据的操作主要分为读和写两种。在并发场景下,会出现读-读并发、读-写并发和写-写并发三种情况。其中,读-读并发通常不会引发问题,写-写并发则常通过加锁来解决,而读-写并发则可以通过MVCC机制来高效处理。

MVCC的核心思想是,对于同一份数据,每个事务在读取时都会看到一个特定的、一致的数据版本,这个版本是在该事务开始时刻生成的。这样,即使有其他事务在修改数据,也不会影响到当前事务的读取结果。

快照读与当前读

MVCC的实现依赖于快照读的概念。快照读是指读取的是快照数据,即快照生成时的数据状态。在MySQL中,普通的SELECT语句(不加锁)通常就是快照读。与快照读相对应的是当前读,它读取的是最新数据,通常用于加锁的SELECT操作或数据的增删改操作。

Undo Log与快照

Undo Log是MySQL中用于回退的事务日志。在事务提交之前,MySQL会先记录更新前的数据到Undo Log中。这些“更新前的数据”实际上就是快照数据。因此,Undo Log是MVCC实现的重要手段。

每当一条记录发生变更时,MySQL都会先将其快照存储到Undo Log中,并更新记录中的隐式字段。这些隐式字段包括:

  • db_row_id:隐藏主键,用于创建聚簇索引。
  • db_trx_id:对这条记录做了最新一次修改的事务的ID。
  • db_roll_ptr:回滚指针,指向这条记录的上一个版本(即Undo Log中的上一个快照的地址)。

这样,每个快照都通过db_trx_id和db_roll_ptr字段形成了一个快照链表。

Read View与可见性

然而,即使有了Undo Log和快照链表,我们仍然需要确定在当前事务中应该读取哪个快照。这时,就需要用到Read View了。

Read View是InnoDB中一个至关重要的概念,它是实现MVCC的基础。Read View主要用来解决可见性问题,即它会告诉当前事务应该看到哪个版本的数据。具体来说,Read View会根据当前事务的ID和其他活跃事务的ID来构建一个视图,然后基于这个视图来确定哪些数据版本对当前事务是可见的。

通过Read View,InnoDB能够确保每个事务在读取数据时都能看到一个一致的快照,从而避免了读写冲突,提高了并发性能。

综上所述,MVCC通过维护数据的多个版本、利用快照读和Undo Log以及Read View等机制,实现了高效的并发控制。这使得数据库能够在高并发环境下保持数据的一致性和完整性,同时提高了系统的性能和吞吐量。

小结

  • MVCC指在使用RC、RR隔离级别下,使不同事务的 读-写 、 写-读 操作并发执行,提升系统性能
  • MVCC核心思想是读不加锁,读写不冲突。
  • RC、RR这两个隔离级别的一个很大不同就是生成 ReadView 的时机不同
  • RC在每一次进行普通 SELECT 操作前都会生成一个 ReadView
  • RR在第一次进行普通 SELECT 操作前生成一个 ReadView ,之后的查询操作都重复这个ReadView

Mysql事务

发表于 2024-11-10 | 更新于 2025-06-22 | 分类于 数据库
字数统计 | 阅读时长

什么是索引下推

官方索引条件下推:

Index Condition Pushdown,简称ICP。是MySQL5.6对使用索引从表中检索行的
一种优化。ICP可以减少存储引擎必须访问基表的次数以及MySQL服务器必须访问存储引擎的次数。可
用于 InnoDB 和 MyISAM 表,对于InnoDB表ICP仅用于辅助索引。
可以通过参数optimizer_switch控制ICP的开始和关闭。

索引下推和覆盖索引是减少回表的一种手段

  • ICP可以有效减少回表查询次数和返回给服务层的记录数,从而
    减少了磁盘IO次数和服务层与存储引擎的交互次数。

问题

以InnoDB的辅助索引为例,来讲解ICP的作用:MySQl在使用组合索引在检索数据时是使用最左前缀原
则来定位记录,左侧前缀之后不匹配的后缀,MySQL会怎么处理?

配置

#optimizer_switch优化相关参数开关
mysql> show VARIABLES like ‘optimizer_switch’;
#关闭ICP
SET optimizer_switch = ‘index_condition_pushdown=off’;
#开启ICP
SET optimizer_switch = ‘index_condition_pushdown=on’;

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CREATE TABLE `test1`  (
`id` int NOT NULL,
`id1` int NULL DEFAULT NULL,
`id2` int NULL DEFAULT NULL,
`id3` int NULL DEFAULT NULL,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `id1`(`id1`, `id2`, `id3`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of test1
-- ----------------------------
INSERT INTO `test1` VALUES (1, 2, 3, 4, 'zhangsan ');
INSERT INTO `test1` VALUES (2, 2, 3, 5, 'lisi');
INSERT INTO `test1` VALUES (3, 2, 4, 5, 'wangwu ');

SET FOREIGN_KEY_CHECKS = 1;
1
2

EXPLAIN SELECT * FROM `test1` WHERE id1 = 1 AND id2 > 1 AND id3 = 3;

如果使用了索引下推,Extra 中是Using index condition

注意!!!

如果你的 EXPLAIN 输出显示:

1
2
3
Extra: Using where; Using index
Using where:表示部分条件是在 MySQL 层处理的,而不是完全依赖索引。
Using index:说明查询是覆盖索引的(即所有查询的数据都可以从索引中获取,避免回表)。

这说明,虽然联合索引 (id1, id2, id3) 被使用,但优化器可能认为使用 索引覆盖扫描 比触发索引下推更高效。

volatile笔记

发表于 2024-11-10 | 更新于 2025-06-22 | 分类于 笔记
字数统计 | 阅读时长

volatile能保证原子性吗?

volatile本身不是锁

一、原子性

原子性是指一个操作或者多个操作要么全部执行,要么全部不执行,中间不会被其他线程的操作打断。在Java中,原子性通常通过同步机制(如synchronized)或者原子类(如AtomicInteger、AtomicLong等)来保证。

为什么需要原子性

在并发编程中,多个线程可能会同时访问和修改同一个共享资源。如果没有适当的同步机制,就可能会出现数据不一致、数据丢失等问题。原子性操作可以确保这些共享资源在并发访问时的正确性和一致性。

如何实现原子性操作

  1. 使用synchronized关键字:
    synchronized关键字可以用来修饰方法或代码块,以确保同一时间只有一个线程可以执行被修饰的代码。这是保证原子性的一种常见方式。

  2. 使用原子类:
    Java提供了一些原子类(如AtomicInteger、AtomicLong、AtomicReference等),这些类内部使用了高效的机制来确保操作的原子性。这些类通常用于实现计数器、状态标志等需要原子性操作的场景。

  3. 使用锁(Lock):
    Java的java.util.concurrent.locks包提供了一些高级的锁机制(如ReentrantLock、ReadWriteLock等),这些锁提供了比synchronized更灵活的同步控制。

  4. 使用低级别的原子操作:
    在某些情况下,开发者可能需要直接使用Java的Unsafe类或者JNI(Java Native Interface)来调用操作系统的原子操作指令。这种方式通常用于实现高性能的并发数据结构或算法。

示例:使用AtomicInteger实现原子性自增

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.util.concurrent.atomic.AtomicInteger;

public class Test {
private AtomicInteger number = new AtomicInteger(0);

public void increase() {
number.incrementAndGet(); // 原子性自增操作
}

public static void main(String[] args) {
Test atomicDemo = new Test();
for (int j = 0; j < 10; j++) {
new Thread(() -> {
for (int i = 0; i < 1000; i++) {
atomicDemo.increase();
}
}, String.valueOf(j)).start();
}

// 等待所有线程执行完毕
while (Thread.activeCount() > 2) {
Thread.yield();
}

System.out.println(Thread.currentThread().getName() + " final number result = " + atomicDemo.number.get());
}
}

在这个示例中,我们使用了AtomicInteger的incrementAndGet方法来实现原子性自增操作。由于AtomicInteger内部使用了高效的原子操作机制,因此可以保证在多线程环境下的正确性。运行这个程序,你会发现每次的结果都是10000,这与使用volatile修饰的变量时的结果不同。

二、volatile是如何保证可见性和有序性的?

volatile和可见性

对于volatile变量,当对volatile变量进行写操作的时候,JVM会向处理器发送一条lock前缀的指令,将这个缓存中的变量回写到系统主存中。 所以,如果一个变量被volatile所修饰的话,在每次数据变化之后,其值都会被强制刷入主存。而其他处理器的缓存由于遵守了缓存一致性协议,也会把这个变量的值从主存加载到自己的缓存中。这就保证了一个volatile在并发编程中,其值在多个缓存中是可见的。

三、synchronized和volatile 区别

synchronized的特点和缺点

synchronized是一种加锁机制,用于确保在同一时刻只有一个线程可以执行某个方法或代码块。它的主要优点在于能够确保线程安全,防止多个线程同时修改共享资源导致的数据不一致问题。然而,synchronized也存在一些缺点:

  1. 性能损耗:虽然JDK 1.6及以后的版本对synchronized进行了多种优化(如适应性自旋、锁消除、锁粗化、轻量级锁和偏向锁等),但加锁和解锁的过程仍然会带来一定的性能损耗。

  2. 产生阻塞:synchronized实现的锁本质上是一种阻塞锁。当多个线程同时访问同一段同步代码时,只有一个线程能够获取锁并进入临界区,其他线程则需要在Entry Set中等待。这可能导致线程阻塞和上下文切换,从而影响系统的并发性能。

volatile的特点和优势

volatile是Java虚拟机提供的一种轻量级同步机制,它主要用于确保变量的可见性,并禁止指令重排。与synchronized相比,volatile具有以下特点和优势:

  1. 性能更高:volatile变量的读操作与普通变量几乎无差别,写操作虽然由于需要插入内存屏障而稍慢一些,但在大多数场景下,其开销仍然比锁要低。

  2. 禁止指令重排:volatile通过内存屏障来确保变量的可见性和有序性。这意味着,当一个线程修改了volatile变量的值后,其他线程能够立即看到这个修改。同时,volatile还能够防止编译器和处理器对指令进行重排序,从而避免某些潜在的并发问题。

  3. 非阻塞:与synchronized不同,volatile不会造成线程的阻塞。它只是确保变量的可见性和有序性,而不会限制线程对共享资源的访问。

为什么需要volatile

尽管synchronized提供了强大的线程同步功能,但在某些场景下,我们仍然需要volatile。这主要是因为:

  1. 可见性问题:在某些情况下,我们可能只需要确保变量的可见性,而不需要对共享资源进行加锁。这时,使用volatile就足够了。

  2. 指令重排问题:在某些并发场景中,指令重排可能会导致潜在的问题。而volatile能够禁止指令重排,从而避免这些问题。

  3. 性能考虑:在某些对性能要求较高的场景中,使用volatile可能比使用synchronized更加合适。因为volatile的开销更低,不会造成线程的阻塞和上下文切换。

综上所述,synchronized和volatile各有其特点和适用场景。在Java并发编程中,我们需要根据具体的需求和场景来选择合适的同步机制。

ArrayList初始容量是多少

发表于 2024-10-16 | 更新于 2025-06-22 | 分类于 面试
字数统计 | 阅读时长

👌ArrayList初始容量是多少?

题目详细答案

ArrayList 是 Java 中用于动态数组的一个类。它可以在添加或删除元素时自动调整其大小。然而,ArrayList 有一个默认的初始容量,这个容量是在你创建 ArrayList 实例时如果没有明确指定容量参数时所使用的。

在 Java 的 ArrayList 实现中,默认的初始容量是 10。这意味着当你创建一个新的 ArrayList 而不指定其容量时,它会以一个内部数组长度为 10 的数组来开始。当添加的元素数量超过这个初始容量时,ArrayList 的内部数组会进行扩容,通常是增长为原来的 1.5 倍。

1
ArrayList<String> list = new ArrayList<>(); // 默认的初始容量是 10

但是,如果你知道你将要在 ArrayList 中存储多少元素,或者预计会存储多少元素,那么最好在创建时指定一个初始容量,这样可以减少由于扩容而导致的重新分配数组和复制元素的操作,从而提高性能。

1
ArrayList<String> list = new ArrayList<>(50); // 初始容量设置为 50

自从1.7之后,arraylist初始化的时候为一个空数组。但是当你去放入第一个元素的时候,会触发他的懒加载机制,使得数量变为10。

1
2
3
4
5
6
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}

所以我们的arraylist初始容量的确是10。只不过jdk8变为懒加载来节省内存。进行了一点优化。

/sp17yx6dqwcrail5>

ArrayList如何保证线程安全

发表于 2024-10-16 | 更新于 2025-06-22 | 分类于 面试
字数统计 | 阅读时长

👌ArrayList如何保证线程安全?

题目详细答案

借助锁

可以通过在访问 ArrayList 的代码块上使用 synchronized 关键字来手动同步对 ArrayList 的访问。这要求所有访问 ArrayList 的代码都知道并使用相同的锁。

1
2
3
4
5
6
7
8
9
10
List<String> list = new ArrayList<>();
// ... 填充列表 ...

synchronized(list) {
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String element = it.next();
// 处理元素...
}
}

使用 Collections.synchronizedList

Collections.synchronizedList 方法返回一个线程安全的列表,该列表是通过在每个公共方法(如 add(), get(), iterator() 等)上添加同步来实现的,其中同步是基于里面的同步代码块实现。但是,和手动同步一样,它也不能解决在迭代过程中进行结构修改导致的问题。

1721369333026-315b65c8-9222-48af-add6-fde77b52b82b.png

1
List<String> list = Collections.synchronizedList(new ArrayList<>());

使用并发集合

Java 并发包 java.util.concurrent 提供了一些线程安全的集合类,如 CopyOnWriteArrayList。这些类提供了不同的线程安全保证和性能特性。

CopyOnWriteArrayList是一个线程安全的变体,其中所有可变操作(add、set 等等)都是通过对底层数组进行新的复制来实现的。因此,迭代器不会受到并发修改的影响,并且遍历期间不需要额外的同步。但是,当有很多写操作时,这种方法可能会很昂贵,因为它需要在每次修改时复制整个底层数组。

1
List<String> list = new CopyOnWriteArrayList<>();

选择解决方案时,需要考虑并发模式、读写比例以及性能需求。如果你的应用主要是读操作并且偶尔有写操作,CopyOnWriteArrayList是一个好选择。如果你的应用有大量的写操作,那么可能需要使用其他并发集合或手动同步策略。

/hkx8lgbi0mny13uc>

ArrayList是如何扩容的

发表于 2024-10-16 | 更新于 2025-06-22 | 分类于 面试
字数统计 | 阅读时长

👌ArrayList是如何扩容的?

题目详细答案

ArrayList的扩容机制是Java集合框架中一个重要的概念,它允许ArrayList在需要时自动增加其内部数组的大小以容纳更多的元素。主要有以下的步骤

1、初始容量和扩容因子:

当创建一个新的ArrayList对象时,它通常会分配一个初始容量,这个初始容量默认为10。

ArrayList的扩容因子是一个用于计算新容量的乘数,默认为1.5。

2、扩容触发条件:

当向ArrayList中添加一个新元素,并且该元素的数量超过当前数组的容量时,就会触发扩容操作。

3、扩容策略:

扩容时,首先根据当前容量和扩容因子计算出一个新的容量。新容量的计算公式为:

newCapacity = oldCapacity + (oldCapacity >> 1),这实际上是将原容量增加50%(即乘以1.5)。

如果需要的容量大于Integer.MAX_VALUE - 8(因为数组的长度是一个int类型,其最大值是Integer.MAX_VALUE,但ArrayList需要预留一些空间用于内部操作),则会使用Integer.MAX_VALUE作为新的容量。

4、扩容过程:

创建一个新的数组,其长度为新计算的容量。

将原数组中的所有元素复制到新数组中。

将ArrayList的内部引用从原数组更新为新数组。

将新元素添加到新数组的末尾。

5、性能影响:

扩容过程涉及到内存分配和元素复制,可能会对性能产生一定的影响。因此,在使用ArrayList时,如果可能的话,最好预估需要存储的元素数量,并设置一个合适的初始容量,以减少扩容的次数。

6、扩容源码

1719246984492-7f242ac2-c31e-4cdd-a074-bc448c5d3148.png

/ghpqbh9ig3hu1coh>

ArrayList是线程安全的吗

发表于 2024-10-16 | 更新于 2025-06-22 | 分类于 面试
字数统计 | 阅读时长

👌ArrayList是线程安全的吗?

题目详细答案

ArrayList不是线程安全的。在多线程环境下,如果多个线程同时对ArrayList进行操作,可能会出现数据不一致的情况。

当多个线程同时对ArrayList进行添加、删除等操作时,可能会导致数组大小的变化,从而引发数据不一致的问题。例如,当一个线程在对ArrayList进行添加元素的操作时(这通常分为两步:先在指定位置存放元素,然后增加size的值),另一个线程可能同时进行删除或其他操作,导致数据的不一致或错误。

比如下面的这个代码,就是实际上ArrayList 放入元素的代码:

1
2
elementData[size] = e;
size = size + 1;
  1. elementData[size] = e; 这一行代码是将新的元素 e 放置在 ArrayList 的内部数组 elementData 的当前大小 size 的位置上。这里假设 elementData 数组已经足够大,可以容纳新添加的元素(实际上 ArrayList 在必要时会增长数组的大小)。
  2. size = size + 1; 这一行代码是更新 ArrayList 的大小,使其包含新添加的元素。

如果两个线程同时尝试向同一个 ArrayList 实例中添加元素,那么可能会发生以下情况:

  • 线程 A 执行 elementData[size] = eA;(假设当前 size 是 0)
  • 线程 B 执行 elementData[size] = eB;(由于线程 A 尚未更新 size,线程 B 看到的 size 仍然是 0)
  • 此时,elementData[0] 被线程 B 的 eB 覆盖,线程 A 的 eA 丢失
  • 线程 A 更新 size = 1;
  • 线程 B 更新 size = 1;(现在 size 仍然是 1,但是应该是 2,因为有两个元素被添加)

为了解决ArrayList的线程安全问题,可以采取以下几种方式:

  1. 使用Collections类的synchronizedList方法:将ArrayList转换为线程安全的List。这种方式通过在对ArrayList进行操作时加锁来保证线程安全,但可能会带来一定的性能损耗。

  2. 使用CopyOnWriteArrayList类:它是Java并发包中提供的线程安全的List实现。CopyOnWriteArrayList在对集合进行修改时,会创建一个新的数组来保存修改后的数据,这样就不会影响到其他线程对原数组的访问。因此,它适合在读操作远远多于写操作的场景下使用。

  3. 使用并发包中的锁机制:如Lock或Semaphore等,显式地使用锁来保护对ArrayList的操作,可以确保在多线程环境下数据的一致性。但这种方式需要开发人员自行管理锁的获取和释放,容易出现死锁等问题。

还可以考虑使用其他线程安全的集合类,如Vector或ConcurrentLinkedQueue等,它们本身就是线程安全的,可以直接在多线程环境下使用。

/sok8k2bhymx7np2u>

<i class="fa fa-angle-left"></i>1…789…12<i class="fa fa-angle-right"></i>

232 日志
18 分类
28 标签
GitHub
© 2025 javayun
由 Hexo 强力驱动
主题 - NexT.Gemini