目录
4、在单体应用中,使用本地锁与不使用本地锁各自会带来哪些问题
6、集群情况,即一个程序被多台服务器所拥有情况下,如何保证并发一致性
11、使用redis锁的时候,程序执行过程中出现异常,会导致锁无法释放,就会造成死锁,怎么解决?
17、解决缓存击穿的方法——“提前“使用互斥锁/逻辑更新是怎么实现的?
20、为什么说重写了equals就需要重写hashCode呢?
40、synchronized关键字加在静态方法和实例方法的区别?
43、在CAS操作过程中,会出现ABA问题。什么是ABA问题?如何解决?
44、CAS操作可能会受到自旋时间过长的影响,因为如果某个线程一直在自旋等待,会浪费CPU资源。该如何解决?
0、什么是死锁
在Java中,死锁是一种多线程并发执行的情况,其中两个或多个线程相互等待对方释放持有的资源,导致所有线程无法继续执行。这种情况下,系统处于僵持状态,被称为死锁。
死锁产生的条件通常是由于每个线程都在等待另一个线程释放资源,而这些资源又是其他线程正在等待的。死锁的发生通常涉及到多个锁、多个线程以及竞争共享资源。
长时间等待一个锁,一直没等到,线程一直在阻塞,没法往下执行
1、死锁产生的四个条件:
-
互斥
-
请求与保持
-
非剥夺
-
循环等待
2、避免死锁
-
按顺序获取锁
-
使用定时锁
-
使用tryLock()
尝试获取锁,如果无法获取锁,会返回,避免线程在等待资源时阻塞。
-
避免嵌套锁
-
使用统一的锁顺序
-
使用锁的层次结构
-
使用事务
-
使用专门的工具与算法
3、本地锁有哪些
-
synchronize锁
-
lock锁
4、在单体应用中,使用本地锁与不使用本地锁各自会带来哪些问题
-
使用本地锁,在高并发情况下可以解决安全问题,但是效率会变低
-
不使用本地锁,在高并发情况下,会发生线程安全问题
5、什么是线程安全问题
-
共享资源由于被多个线程同个时间持有,可能会被错误赋值(个人理解)
6、集群情况,即一个程序被多台服务器所拥有情况下,如何保证并发一致性
-
使用分布式锁,又称公共锁,如redis锁
-
不能使用本地锁,因为无法保证一个进程在当前的程序内执行(一个进程可以在多个程序内同时运行)
7、使用redis锁的进程流程
-
多个客户端同时获取锁(setnx),即尝试加锁
-
加锁成功的客户端,执行业务逻辑,执行完成,释放锁
-
其他客户端等待重试获取锁,即自旋
8、什么是自旋
-
自旋,自我循环,像while循环,for循环那样不停的自我阻塞,直到获取锁(共享资源)成功。
-
自我阻塞的时候做的事情:一直在等待锁释放,不断的尝试获取锁
9、自旋锁与非自旋锁的区别
在获取锁失败后,自旋锁的进程会占用CPU,不停的尝试获取锁,非自旋锁的进程不会占用CPU,会把自己切换为休眠状态,使得CPU可以在该进程休眠的时间内做其他的事情
当持有锁的进程释放锁后,自旋的进程和非自旋的进程都会再尝试获取锁,成功则会获取到同步资源的锁。细节是非自旋的进程需要通过切换进程状态来唤醒
自旋的优缺点,适用场合:自旋的进程用循环不停的尝试获取锁,让进程始终处于Runnable状态,缺点是如果锁一直没有被释放,那么自旋就是在做无用的尝试,会白白浪费处理器资源, 优点是如果锁可以在短时间内被释放,那么自旋节省了线程切换带来的开销,提高效率,适用于锁可以在短时间内被释放,即同步代码(共享资源)的内容不多以及并发度不高的情况
10、线程总共有多少种状态
-
new新建
-
runnable就绪
已经具备运行条件,正在等待获取CPU时间片,一旦获得CPU时间片,就会进入running运行状态
-
running运行
-
blocked阻塞
锁(共享资源)被其他线程持有,当该线程获得到锁时,进入runnable就绪状态
-
waiting等待
当线程调用
Object.wait()
、Thread.join()
或LockSupport.park()
等方法,该线程就会进入waiting状态;waiting状态的线程必须等待另外一个线程调用notify方法或者notifyAll方法才能够被唤醒,从而进入runnable就绪状态 -
time waiting超时等待
线程调用Thread.sleep(毫秒值)方法和Object.wait()时会进入Time Waiting状态。
-
terminated死亡
因为run方法正常的退出而死亡,或者因为遇到异常而死亡。
系统重新启动,线程也会结束
11、使用redis锁的时候,程序执行过程中出现异常,会导致锁无法释放,就会造成死锁,怎么解决?
-
给锁设置一个过期时间,防止死锁
-
实现方式
//需要进行加锁 Boolean lock = stringredisTemplate.opsForValue().setIfAbsent("lock", "111"); //设置过期时间 stringredisTemplate.expire("lock", Duration.ofSeconds(3L));
-
setIfAbsent含义:如果键不存在,则设置键值对,并返回
true
;如果键已经存在,则不做任何操作,并返回false
-
setIfAbsent作用:
setIfAbsent
是一个原子操作,要么完全执行,要么完全不执行,防止多个线程或进程会同时误以为自己获得了锁,可以确保只有一个线程或进程能够成功获取到锁, -
潜在问题:在设置锁和设置过期时间之间,如果程序崩溃,锁可能会一直存在,导致死锁。
-
改进:需确保设置锁和设置过期时间这两个操作是原子性的,所以考虑
-
使用 Redis 的
SET
命令的NX
和EX
选项,这两个选项可以原子性地设置键值对并设置过期时间String result = stringredisTemplate.execute((RedisCallback<String>) connection -> connection.set("lock".getBytes(), "111".getBytes(), Expiration.seconds(3), RedisStringCommands.SetOption.SET_IF_ABSENT) ); Boolean lock = "OK".equals(result);
-
使用 Spring Data Redis 提供的
setIfAbsent
方法Boolean lock = stringredisTemplate.opsForValue().setIfAbsent("lock", "111", Duration.ofSeconds(3L));
-
12、在线程1执行同步代码(获取到了锁)期间,因为一些问题,还没执行完同步代码,锁因为过期时间到了过期了,此时线程2看到锁被释放了,就尝试获取锁,最后获取到锁了,但是线程1的代码还没执行完毕,也就是还没执行到finally里的释放锁代码。此时线程1执行到了finally里的释放锁代码,会将此时线程2持有的锁给释放掉。这种情况会导致线程不安全,主要原因出在哪里,该怎么解决?
-
问题出在锁被谁持有就得被谁释放,否在就出现了锁的误删除
-
解决:每个线程在加锁的时候,生成一个UUID,用于标识当前的锁。
Boolean lock = stringredisTemplate.opsForValue().setIfAbsent("lock", uuid, 3, TimeUnit.SECONDS);
if (uuid.equals(stringredisTemplate.opsForValue().get("lock"))){
//释放锁
stringredisTemplate.delete("lock");
}
-
潜在问题:在校验uuid和释放锁中间,如果持有锁的线程1到期释放锁了,线程2获取到锁,线程1 往下执行释放锁的代码,还是会发生误删锁的问题
-
改进:需确保校验uuid和释放锁这两个操作是原子性的,所以考虑
-
使用Lua脚本
-
//定义Lua脚本
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
//定义对象 设置返回值执行
DefaultRedisScript<Long> defaultRedisScript = new DefaultRedisScript<>();
//设置脚本
defaultRedisScript.setScriptText(script);
//设置返回值类型
defaultRedisScript.setResultType(Long.class);
//执行
stringredisTemplate.execute(defaultRedisScript, Arrays.asList("lock"),uuid);
13、使用redis锁时需要注意什么?
-
加锁
每个线程在加锁的时候,生成一个UUID,用于标识当前的锁,防止误删锁
需确保设置锁和设置过期时间这两个操作是原子性的,防止程序出现问题锁不能被及时释放造成死锁
//生成锁的标识 String uuid = UUID.randomUUID().toString().replace("_",""); //设置锁 stringRedisTemplate.opsForValue().setIfAbsent("lock",uuid,3,TimeUnit.SECONDS);
-
释放锁
需确保校验uuid和释放锁这两个操作是原子性的,防止在持有锁的线程在锁过期了还在执行删除锁的代码,防止误删锁
//定义Lua脚本 String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; //定义对象 设置返回值执行 DefaultRedisScript<Long> defaultRedisScript = new DefaultRedisScript<>(); //设置脚本 defaultRedisScript.setScriptText(script); //设置返回值类型 defaultRedisScript.setResultType(Long.class); //执行 stringredisTemplate.execute(defaultRedisScript, Arrays.asList("lock"),uuid);
14、tryLock函数的作用有哪些?
-
tryLock函数有两种形式
-
第一种,无参
boolean tryLock();
尝试获取锁,如果锁已经被其他线程占用,则立即返回 false,不会阻塞。
-
第二种,带时间参数
boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException;
在指定时间内尝试获取锁。如果在超时时间内获取成功,返回 true;否则返回 false。 如果线程在等待获取锁的过程中被中断(即收到中断信号),会抛出InterruptedException。
-
作用:
-
非阻塞
如果无法获取锁,无参数版本会立即返回,而不会像 lock() 一样等待(锁释放)。
-
可控等待:
带超时版本允许在限定时间内等待获取锁,而不是无限期阻塞。
-
响应中断:
带超时版本可以被中断,适合需要响应中断的场景。
-
灵活性:
提供了比 synchronized 更灵活的锁控制。
-
15、缓存穿透、缓存击穿、缓存雪崩的原因及解决方案
-
缓存穿透
含义:客户端客请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会访问数据库。导致DB的压力瞬间变大而卡死或者宕机。
发生场景:
-
原来请求的数据是存在的,但由于某些原因(误删除、主动清理等)在缓存和数据库层面被删除了,但是前端或前置的应用程序依旧保有这些数据
-
恶意攻击行为,利用不存在的Key或者恶意尝试导致产生大量不存在的业务数据请求。
解决方法:
-
接口校验(将请求止于请求到redis):对于
id = -3872
这些无效访问就直接拦截,不允许这些请求到达Redis、DB上。 -
对空值进行缓存(将请求止于请求到数据库):比如,虽然数据库中没有
id = 1022
的用户的数据,但是在redis中对其进行缓存(key=1022, value=null
),这样当请求到达redis的时候就会直接返回一个null的值给客户端,避免了大量无法访问的数据直接打在DB上。 -
布隆过滤器:如果它告诉你不存在,则一定不存在;如果它告诉你存在,则可能不存在。(很适合判断海量元素中元素是否存在,比如设置网站的黑名单)
-
实时监控:对redis进行实时监控,当发现redis中的命中率下降的时候进行原因的排查,配合运维人员对访问对象和访问数据进行分析查询,从而进行黑名单的设置限制服务(拒绝黑客攻击)。
-
-
缓存雪崩:
含义:当redis中的大量key集体过期,可以理解为Redis中的大部分数据都清空 / 失效了,这时候如果有大量并发的请求来到,Redis就无法进行有效的响应(命中率急剧下降),也会导致DB先生的绝望。
发生场景:
-
大量热点key同时过期
-
缓存服务故障或宕机
解决方法:
-
将失效时间分散开:常用且易于实现的方法是通过使用自动生成随机数使得key的过期时间TTL是随机的,防止集体过期。
-
给业务添加多级缓存:使用nginx缓存 + redis缓存 + 其他缓存,不同层使用不同的缓存,可靠性更强。
-
构建缓存高可用集群:主要针对缓存服务故障的情景,使用Redis集群来提高服务的可用性。
-
使用锁或者队列的方式:如果查不到就加上排它锁,其他请求只能进行等待,但这种方式可能影响并发量。
-
设置缓存标记:热点数据可以不考虑失效,后台异步更新缓存,适用于不严格要求缓存一致性的情景。(因为异步任务从数据库获取的数据与原来的缓存旧数据可能不一致,异步任务的触发是由缓存标记触发的)
缓存结构: hot_data:存储实际的热点数据,设置较长的过期时间(如1小时)。 hot_data_flag:存储缓存的状态标记,设置较短的过期时间(如10秒)。当需要更新缓存时,可以将该标记设置为 true,后台异步任务检测到标记为 true 时进行缓存刷新,并将标记重置为 false。 读取逻辑: 当客户端访问热点数据时,首先检查 hot_data_flag 是否指示需要更新缓存。 如果不需要更新,直接返回 hot_data 的值。 如果需要更新,触发异步任务 updateCacheAsync() 来刷新缓存,同时返回当前可用的数据(可以是旧数据或默认值)。 异步更新: 使用 @Async 注解的方法在后台线程中执行,避免阻塞主线程。 更新主缓存 hot_data 并重置缓存标记 hot_data_flag。
-
-
缓存击穿:
含义:Redis中的某个热点key过期,但是此时有大量的用户访问该过期key。(可以看成缓存雪崩的一个特殊子集。)
解决方法:
-
使用互斥锁:只有一个请求可以获取到互斥锁,然后到DB中将数据查询并返回到Redis,之后所有请求就可以从Redis中得到响应。【缺点:所有线程的请求需要一同等待】
-
“提前“使用互斥锁/逻辑更新:在value内部设置一个比缓存(Redis)过期时间短的过期时间标识,当异步线程发现该值快过期时,马上延长内置的这个时间,并重新从数据库加载数据,设置到缓存中去。【缺点:不保证一致性,实现相较互斥锁更复杂】
-
提前对热点数据进行设置:类似于新闻、某博等软件都需要对热点数据进行预先设置在Redis中,或者适当延长Redis中的Key过期时间。
-
监控数据,适时调整:监控哪些数据是热门数据,实时的调整key的过期时长。
16、解决缓存击穿的方法——使用互斥锁是怎么实现的?
使用setnx作为Redis中的锁。
-
Redis中查询缓存
-
存在且不为空值,直接返回
-
为空值(比如“”、0等特殊值),返回失败结果
-
不存在,获取锁
-
-
获取锁失败,等待重试
-
获取成功,查找MySQL
-
不存在,Redis存入空值
-
存在,写入Redis
-
-
释放锁,返回结果
/** * 根据id查找商户,先到redis中找,再到MySQL中找 * @param id * @return */ @Override public Result queryShopById(Long id) { // 用String形式存储JSON String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id); // 如果查询结果不为null,直接返回 if (StrUtil.isNotBlank(shopJson)) { Shop shop = JSONUtil.toBean(shopJson, Shop.class); return Result.ok(shop); } // 否则Redis中查询结果为空,判断是否为“” if (shopJson != null) { return Result.fail("店铺不存在,请确认id是否正确"); } // 尝试获取锁, // 如果没有得到锁,Sleep一段时间 if (!tryLock(LOCK_SHOP_KEY + id)) { try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } // 从开始重试 return queryShopById(id); } // 获得了锁,从MySQl中查找 Shop shop = this.getById(id); // 模拟重建的延时 try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } // 不在MySQL中 if (shop == null) { // 将空值写入Redis stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES); // 释放锁 unLock(LOCK_SHOP_KEY + id); return Result.fail("店铺不存在,请确认id是否正确"); } else { // 在MySQL中,存入redis stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES); // 释放锁 unLock(LOCK_SHOP_KEY + id); return Result.ok(shop); } } public boolean tryLock(String key) { // 尝试获取锁,set成功返回true,否则返回false Boolean getLock = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS); // 避免getLock为null,使用工具类 return BooleanUtil.isTrue(getLock); } public void unLock(String key) { stringRedisTemplate.delete(key); }
-
17、解决缓存击穿的方法——“提前“使用互斥锁/逻辑更新是怎么实现的?
缓存的数据有过期时间属性,
当客户端访问该数据时,
-
访问redis
-
如果查询结果为null,直接返回失败信息
-
比较当前时间now()是否大于缓存数据的过期时间属性,
-
小于则说明缓存数据还没有过期,直接返回查询结果
-
大于则说明缓存数据过期了,需获取互斥锁shanx
-
如果获取互斥锁失败,则返回过期的查询结果信息
-
如果获取互斥锁成功,开启新线程,访问数据库并写入redis,最后释放锁
-
-
/**
* 线程池
*/
private static final ThreadFactory NAMED_THREAD_FACTORY = new ThreadFactoryBuilder().build();
private static final ExecutorService POOL = new ThreadPoolExecutor(5, 200,
0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(1024), NAMED_THREAD_FACTORY,
new ThreadPoolExecutor.AbortPolicy());
public Result queryWithExpire(Long id) {
String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
// 如果查询结果为null,直接失败
if (StrUtil.isBlank(shopJson)) {
return Result.fail("您查询的数据不存在,请检查您的输入");
}
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
Shop shop = JSONUtil.toBean((JSONObject)redisData.getData(), Shop.class);
// 判断缓存是否过期
LocalDateTime time = redisData.getExpireTime();
// 未过期,直接返回信息
if (time.isAfter(LocalDateTime.now())) {
return Result.ok(shop);
}
// 过期,获取互斥锁失败,返回过期信息
if (!tryLock(LOCK_SHOP_KEY + id)) {
unLock(LOCK_SHOP_KEY + id);
return Result.ok(shop);
}
// 过期,获取互斥锁成功,开启新线程,重建数据库
POOL.submit(() -> {
this.saveShop2Redis(id, 20L);
unLock(LOCK_SHOP_KEY + id);
});
// 返回过期信息
return Result.ok(shop);
}
/**
* 在MySQL中查找id的shop,写入Redis并更新虚拟过期时间
* @param id
* @param expireSeconds
*/
private void saveShop2Redis(Long id, Long expireSeconds) {
// 获取
Shop shop = this.getById(id);
// 封装
RedisData redisData = new RedisData();
redisData.setData(shop);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
// 写入Redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}
18、布隆过滤器是什么
-
布隆过滤器用于检索一个元素是否在一个集合中
-
由一个大型位数组(二进制数组)+多个无偏hash函数组成
多个无偏hash函数:无偏hash函数就是能把元素的hash值计算的比较均匀的hash函数,能使得计算后的多个hash值比较均匀的映射到位数组下表中。
-
当往布隆过滤器增加元素时,流程是:
-
通过k个无偏hash函数计算得到k个hash值
-
依次取模数组长度,得到hash值在数组中的索引
-
将计算得到的数组索引下标位置数据修改为1
-
-
当在布隆过滤器查询某个元素时‘,流程是:
-
通过k个无偏hash函数计算得到k个hash值
-
依次取模数组长度,得到hash值在数组中的索引
-
判断索引处的值是否全部为1,如果全部为1则可能存在(这种存在可能是误判),如果存在一个0则必定不存在
-
19、hashcode与equals的关系
-
1、如果两个对象equals,Java运行时环境会认为他们的hashcode一定相等。
-
2、如果两个对象hashcode不相等,他们一定不equals。
-
3、如果两个对象hashcode相等,他们不一定equals。
-
4、如果两个对象不equals,他们的hashcode有可能相等。
-
区别:
-
equals方法:用于比较两个对象是否相等,默认情况下,它比较的是对象的引用(即内存地址),但通常我们会根据对象的实际内容来重写这个方法。例如,在
Person
类中,我们可能会根据name
和age
属性来判断两个Person
对象是否相等。 -
hashcode方法:返回的是对象的hash值,是一个整数,用于确定对象在哈希表中的索引位置。理想情况下,不同的对象应该产生不同的哈希码,但这并不是强制的。重要的是,如果两个对象通过
equals()
方法比较是相等的,那么它们的hashCode()
方法必须返回相同的整数值。hash值相等的两个对象不一定equals
-
20、为什么说重写了equals就需要重写hashCode呢?
个人理解:因为如果两个对象equals,Java运行时环境会认为他们的hashcode一定相等。所以要确保两个对象是相等的就得重写hashcode,使得两个对象的hashcode相等。
当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。所以这两个是相互配合着使用的(尤其是在集合中)。
当把对象加入HashSet时,先比较hashcode再比较equals
21、union与union all区别
-
相同点:
-
对select语句的要求:必须保证各个select集合的结果有相同个数的列,并且每个列的类型是一样(相同或类似)的,列名不要求相同。
-
注意:union 和 union all都可以将多个结果集合并,而不仅仅是两个,所以可将多个结果集串起来。
-
-
不同点:
-
结果集:union会去重,union all不会去重,包含每个select的结果集的所有数据
-
排序:union会对结果集进行排序,union all不会,只是简单的将结果合并后就返回。
-
效率:union all比union快
-
22、sql语句优化可以从哪些方面入手
-
避免使用select *
原因:多查出来了不需要的数据,第一,通过网络IO传输的过程中,会增加数据传输的时间,第二,select *不会走覆盖索引,会出现大量回表操作,从而导致sql查询性能很低。第三,白白浪费了内存和cpu
-
用union all代替union
虽然union会对结果集去重、排序,但是很耗时跟耗cpu资源
-
小表驱动大表
in
适用于左边大表,右边小表。(因为in
会优先执行右边in里面的子查询语句
,然后作为条件查询结果与左边主查询语句的结果匹配)exists
适用于左边小表,右边大表。(因为exists
会优先执行左边主查询语句,然后作为条件查询结果与右边exists里面的子查询语句的结果匹配) -
批量操作
批量增删改查数据,只需要远程请求一次数据库
在循环中逐条增删改查数据,循环几次就需要请求几次数据库
所以,批量操作sql性能会得到提升,但建议每批数据尽量控制在500以内,如果数据太多数据库响应也会很慢
-
多用limit
select查找:只需要一些数量的数据或者特殊数据,
-
特殊场景:查找数据用于判断有无特定条件下的数据时,SQL不再使用
count
,而是改用LIMIT 1
,让数据库查询时遇到一条就返回,不要再继续查找还有多少条了
update修改与delete删除:防止误操作
-
-
in值不宜太多
in值太多会导致接口超时
解决方法:
-
可以在sql中对数据用limit做限制,或者在业务代码中加限制
-
可以分批用多线程去查询数据。每批只查500条记录,最后把查询到的数据汇总到一起返回(不适合于ids实在太多的场景。因为ids太多,即使能快速查出数据,但如果返回的数据量太大了,网络传输也是非常消耗性能的,接口性能始终好不到哪里去。)
在业务代码中加限制示例代码:
public List<Category> getCategory(List<Long> ids) { if(CollectionUtils.isEmpty(ids)) { return null; } if(ids.size() > 500) { throw new BusinessException("一次最多允许查询500条记录") } return mapper.getCategoryList(ids); }
-
-
查询的数据太多可考虑使用增量查询
场景:有时候,我们需要通过远程接口查询数据,然后同步到另外一个数据库。如果数据很多的话,查询性能会非常差。)
select * from user
where id>#{lastId} and create_time >= #{lastCreateTime}
limit 100;
按id和时间升序,每次只同步一批数据,这一批数据只有100条记录。每次同步完成之后,保存这100条数据中最大的id和时间,给同步下一批数据的时候用。
-
limit分页时的第一个参数太大时,可以使用where id>或者whhere id between
场景:使用
limit a,b
查询第几页有哪些数据时,当a数据很大时,意味着mysql会查到a+b条数据(很大),然后丢弃前面的a条(很大),只查后面的b条数据(很小),这个是非常浪费资源的。
select id,name,age
from user limit 1000000,20;
优化后:
第一种:先找到上次分页最大的id,然后利用id上的索引查询。(要求id是连续的,并且有序的。)
select id,name,age
from user where id > 1000000 limit 20;
第二种:使用between。(要求id是连续的,并且有序的。)
select id,name,age
from user where id between 1000000 and 1000020;
-
用连接查询代替子查询
子查询语句的优点是简单,结构化,如果涉及的表数量不多的话。
但缺点是mysql执行子查询时,需要创建临时表,查询完毕后,需要再删除这些临时表,有一些额外的性能消耗。
子查询:
select * from order where user_id in (select id from user where status=1)
连接查询:
select o.* from order o inner join user u on o.user_id = u.id where u.status=1
-
join的表不宜过多
根据阿里巴巴开发者手册的规定,join表的数量不应该超过
3
个。原因:第一,如果join太多,mysql在选择索引的时候会非常复杂,很容易选错索引。
第二,如果在选择索引的时候没有命中索引,nested loop join 就是分别从两个表读一行数据进行两两对比,复杂度是 n^2。
解决方法:添加冗余字段。如果实现业务场景中需要查询出另外几张表中的数据,可以在a、b、c表中
冗余专门的字段
,比如:在表a中冗余d_name字段,保存需要查询出的数据。 -
join时要注意,left join关联查询时,左边要用小表,右边可以用大表,如果能用inner join的地方,尽量少用left join。
-
left join
:求两个表的交集外加左表剩下的数据。 -
inner join
:求两个表交集的数据。
如果两张表使用inner join关联,mysql会自动选择两张表中的小表,去驱动大表,所以性能上不会有太大的问题。
如果两张表使用left join关联,mysql会默认用left join关键字左边的表,去驱动它右边的表。如果左边的表数据很多时,就会出现性能问题。
-
-
控制索引的数量
阿里巴巴的开发者手册中规定,单表的索引数量应该尽量控制在
5
个以内,并且单个索引中的字段数不超过5
个。索引能够显著的提升查询sql的性能,但索引数量并非越多越好。
原因:
-
新增数据时,需要同时为它创建索引,而索引是需要额外的存储空间的,而且还会有一定的性能消耗。
-
在insert、update和delete操作时,需要更新B+树索引。如果索引过多,会消耗很多额外的性能。
索引数量太多了,高并发系统如何优化索引数量?
解决方法:
-
能够建联合索引,就别建单个索引,可以删除无用的单个索引。
-
将部分查询功能迁移到其他类型的数据库中,比如:Elastic Seach、HBase等,在业务表中只需要建几个关键索引即可。
-
-
选择合理的字段类型
-
能用数字类型,就不用字符串,因为字符的处理往往比数字要慢。
-
尽可能使用小的类型,比如:用bit存布尔值,用tinyint存枚举值等。
-
长度固定的字符串字段,用char类型。
-
长度可变的字符串字段,用varchar类型。
-
金额字段用decimal,避免精度丢失问题。
-
-
先缩小数据范围再group by分组
group by主要的功能是去重和分组。(分组是一个相对耗时的操作)
通常它会跟
having
一起配合使用,表示分组后再根据一定的条件过滤数据。ql语句在做一些耗时的操作之前,应尽可能缩小数据范围,这样能提升sql整体的性能。
反例:先把所有的订单根据用户id分组之后,再去过滤用户id大于等于200的用户。
select user_id,user_name from order group by user_id having user_id <= 200;
正例:分组是一个相对耗时的操作,为什么我们可以先缩小数据的范围之后,再分组
select user_id,user_name from order where user_id <= 200 group by user_id
-
索引优化
可以使用
explain
命令,查看mysql的执行计划。explain select * from order where code='002';
23、唯一索引允许存在多个null值吗?
-
允许,这是因为 MySQL 将
NULL
视为一个“未知值”,而不是一个具体的值,因此多个NULL
值不会违反唯一性约束。
24、索引有哪些分类
-
主键索引:默认索引,数据列不允许重复,不允许为null
-
唯一索引:数据列不允许重复,允许为null
-
普通索引:数据列允许重复,允许为null
-
组合索引:多列值组成一个索引,用于组合搜索,效率大于索引合并
25、为什么使用索引会加快查询?
-
对于有索引的表,MySQL一般通过BTREE 算法生成一个索引文件,在查询数据库时,找到索引文件进行遍历,在比较小的索引数据里查找,然后映射到对应的数据,能大幅提升查找的效率。
-
和我们通过书的目录,去查找对应的内容,一样的道理。
26、事务四大特性ACID
-
原子性
事务包含的所有操作要么全部成功,要么全部失败回滚。
-
一致性
一致性要求事务执行前后数据库的状态保持一致。事务执行过程中可能涉及多个操作,这些操作的结果必须满足数据库的约束和规则。 例子:在购物网站上进行支付操作,支付前后库存、账户余额等信息必须保持一致,否则支付过程可能导致数据不一致。 🪄例如:那转账来说,假设用户A和用户B两者的钱加起来是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱加起来应该还得是5000,这就是事务的一致性。
-
隔离性
隔离性是指当多个用户并发访问数据库时,比如操作同一张表时,数据库为每个用户开启的事务,不能被其他事务的操作干扰,多个并发事务要互相隔离。
-
持久性
持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变是永久的,即便是在数据库系统中遇到故障的情况下也不会丢失提交事务的操作。
27、事务的隔离性
-
脏读
一个事务读取了另一个尚未提交的事务所修改的数据,如果那个未提交的事务最终被回滚(撤销),那么之前读取到的数据就是无效的,即所谓的“脏数据”。
-
不可重复读
一个事务(Transaction)执行过程中,多次读取同一数据时,由于其他事务的修改和提交,导致每次读取到的数据不一致的现象。
-
虚读(幻读)
在一个事务(Transaction)执行过程中,多次执行相同的查询时,由于其他事务的插入或删除操作,导致第二次查询返回了在第一次查询中不存在的数据行,这些新增的数据行就像是“幻影”一样,因此称为幻读。
-
隔离级别
-
Read uncommitted(读未提交):最低级别,任何情况都无法保证
-
Read committed (读已提交):可避免脏读的发生。
-
Repeatable read (可重复读):可避免脏读、不可重复读的发生。MySQL默认隔离级别
-
Serializable (串行化):可避免脏读、不可重复读、幻读的发生。
-
28、Spring的三特性
-
IOC:控制反转
将创建对象的权力交给spring来进行处理
作用:解耦,降低程序间的耦合性、
优点:解耦,降低程序间的依赖关系;
缺点:使用反射生成对象,损耗效率。
-
DI:依赖注入
作用:解耦,降低程序间的耦合性、
注入方式:使用构造函数注入;使用set方法注入;使用注解。
-
AOP:面向切面编程
将程序重复的代码抽取出来,在需要执行的时候,使用动态代理的技术,在不修改源码的基础上,对自己的已有方法进行增强。
作用:在程序运行期间,不修改源码对已有方法进行增强。
优点:减少重复代码; 提高开发效率; 维护方便。
实现方式:使用动态代理技术。
-
JDK实现动态代理(基于接口的动态代理)、
-
CGLIB实现动态代理(基于子类动态代理)。
①、JoinPoint(连接点):指那些被拦截到的点。(在Spring中这些点指的是方法,因为Spring只支持方法类型的连接点)。
②、PointCut(切入点):指的是要对哪些JoinPoint进行拦截的定义。
③、Advice(通知/增强):指拦截到JoinPoint之后所要做的事情。
④、Target(目标对象):代理的目标对象。
⑤、Weaving(织入):指把增强应用到目标对象来创建新的代理对象的过程。 spring采用动态代理织入; Aspect采用编译器织入和类装载期织入。
⑥、Aspect(切面):是切入点和通知的结合。
前置通知:在切入点方法执行之前执行。 后置通知:在切入点方法执行之后执行。它和异常通知永远只能执行一个。 异常通知:在切入点方法执行产生异常之后执行。它和后置通知永远只能执行一个。 最终通知:无论切入点方法是否正常执行,它都会在其后面执行。
@Aspect:表示当前类是一个切面类。
@Before(“ptl()”):前置通知。
@AfterReturning(“ptl()”):后置通知。
@AfterThrowing(“ptl()”):异常通知。
@After(“ptl()”):最终通知。
@Around(“ptl()”):环绕通知。
-
29、什么是内连接、外连接、交叉连接、笛卡尔积呢?
-
内连接(inner join):取得两张表中满足存在连接匹配关系的记录。
-
外连接(outer join):不只取得两张表中满足存在连接匹配关系的记录,还包括某张表(或两张表)中不满足匹配关系的记录。
-
left join会返回左表所有的行
-
right join 会返回右表所有的行
-
-
交叉连接(cross join):显示两张表所有记录–对应,没有匹配关系进行筛选,它是笛卡尔积在SQL中的实现,如果 A 表有 m 行,B 表有 n 行,那么 A 和 B 交叉连接的结果就有 m*n 行。
-
笛卡尔积:是数学中的一个概念,例如集合 A={a, b},集合 B={0, 1, 2},那么 AXB={<a, 0>, <a, 1>, <a, 2>, <b, 0>, <b, 1>, <b, 2>}。
30、数据库的三大范式
-
第一范式:数据表中的每一列(每个字段)都不可以再拆分。
例如用户表,用户地址还可以拆分成国家、省份、市,这样才是符合第一范式的。
-
第二范式:在第一范式的基础上,非主键列完全依赖于主键,而不能是依赖于主键的一部分。
假设有一个表存储学生选课信息:假设有一个表存储学生选课信息,“教室号”和“教师姓名”只依赖于“课程”(主键的一部分),而不是完全依赖于主键(学生ID, 课程)。
-
第三范式:在满足第二范式的基础上,所有非主键列都不传递依赖于主键,即非主键列之间没有依赖关系。
假设有一个学生信息表,“班主任”依赖于“班级”,而“班级”又依赖于“学生ID”(主键),存在传递依赖。
注意:
-
三大范式的作用是为了控制数据库的冗余,是对空间的节省,
-
实际上,一般互联网公司的设计都是反范式的,通过冗余一些数据,避免跨表跨库,利用空间换时间,提高性能。
31、drop,delete与truncate的区别?
-
drop:删除数据+结构
-
truncate:删除数据,不删除结构
-
delete:删除部分数据,不删除结构
32、索引不适合哪些场景?
-
数据量比较少的表
-
更新比较频繁的字段
-
离散低的字段,如性别
33、创建索引有哪些注意点?
-
索引应该建在查询应用频繁的字段。在用于where判断、order排序和join的(on)字段上创建索引。
-
索引的个数应该适量。索引需要占用空间;更新时候也需要维护。
-
区分度低的字段,例如性别,不要建索引。
-
频繁更新的值,不要作为主键或者索引
-
组合索引把散列性高(区分度高)的值放在前面为了满足最左前缀匹配原则
-
创建组合索引,而不是修改单列索引。组合索引代替多个单列索引(对于单列索引,MySQL 基本只能使用一个索引,所以经常使用多个条件查询时更适合使用组合索引)
-
过长的字段,使用前缀索引。当字段值比较长的时候,建立索引会消耗很多的空间,搜索起来也会很慢。我们可以通过截取字段的前面一部分内容建立索引,这个就叫前缀索引。
-
不建议用无序的值(例如身份证、UUID)作为索引。
34.索引哪些情况下会失效呢?
-
查询条件包含or,可能导致索引失效
-
如果字段类型是字符串,where 时一定用引号括起来,否则会因为隐式类型转换,索引失效
-
like通配符可能导致索引失效。
-
联合索引,查询时的条件列不是联合索引中的第一个列,索引失效。
-
在索引列上使用mysql的内置函数,索引失效。
-
对索引列运算(如,+、-、*、/),索引失效。
-
索引字段上使用(!= 或者<>,not in)时,可能会导致索引失效。
-
索引字段上使用is null,is not nul,可能导致索引失效。
-
左连接查询或者右连接查询查询关联的字段编码格式不一样,可能导致索引失效。
-
MySQL优化器估计使用全表扫描要比使用索引快,则不使用索引。
35、多线程有哪几种创建方式
-
实现Runnable,Runnable规定的方法是run(),无返回值,无法抛出异常
-
实现Callable,Callable规定的方法是call(),任务执行后有返回值,可以抛出异常
-
继承Thread类创建多线程:继承java.lang.Thread类,重写Thread类的run()方法,在run()方法中实现运行在线程上的代码,调用start()方法开启线程。
-
Thread 类本质上是实现了 Runnable 接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过 Thread 类的 start()实例方法。start()方法是一个 native 方法,它将启动一个新线程,并执行 run()方法
-
通过线程池创建线程. 线程和数据库连接这些资源都是非常宝贵的资源。那么每次需要的时候创建,不需要的时候销毁,是非常浪费资源的。那么我们就可以使用缓存的策略,也就是使用线程池。
36、wait 和 sleep 的区别
-
特性 sleep
wait
所属类 Thread
Object
是否释放锁 不释放锁 释放锁 唤醒方式 自动 notify
或notifyAll
使用场景 模拟延迟、定时任务 线程间通信、等待条件 -
注意:
wait
方法必须在同步代码块或同步方法中调用,否则会抛出异常。-
错误示例:
Object lock = new Object(); lock.wait(); // 会抛出 IllegalMonitorStateException
-
正确示例:
synchronized (lock) { lock.wait(); // 正常工作 }
-
-
注意:使用
wait
后,必须确保在某个条件满足时调用notify
或notifyAll
唤醒线程,否则等待的线程将一直处于阻塞状态。 -
如果只是简单地让线程暂停一段时间,使用
sleep
。 -
如果需要线程之间协作,等待某个条件时,使用
wait
搭配notify
。
37、进程和线程的区别
-
进程是指在系统中正在运行的一个应用程序;程序一旦运行就是进程;进程——资源分配的最小单位
-
系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元执行流。线程——程序执行的最小单位
38、synchronized和lock区别 ?
区别类型 | synchronized | Lock |
---|---|---|
存在层次 | Java的关键字,在jvm层面上 | 是JVM的一个接口 |
锁的获取 | 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 | 情况而定,Lock有多个锁获取的方式,大致就是可以尝试获得锁,线程可以不用一直等待(可以通过tryLock判断有没有锁) |
锁的释放 | 1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放 | 在finally中必须释放锁,不然容易造成线程死锁 |
锁类型 | 锁可重入、不可中断、非公平 | 可重入、可中断、可公平(两者皆可) |
性能 | 少量同步 | 适用于大量同步 |
支持锁的场景 | 独占锁 | 公平锁与非公平锁 |
39、多线程之间是如何通信的?
1、通过共享变量,变量需要volatile 修饰
2、使用wait()和notifyAll()方法,但是由于需要使用同一把锁,所以必须通知线程释放锁,被通知线程才能获取到锁,这样导致通知不及时。
3、使用CountDownLatch实现,通知线程到指定条件,调用countDownLatch.countDown(),被通知线程进行countDownLatch.await()。
4、使用Condition的await()和signalAll()方法。
40、synchronized关键字加在静态方法和实例方法的区别?
-
修饰静态方法,是对类进行加锁,如果该类中有methodA 和methodB都是被synchronized修饰的静态方法,此时有两个线程T1、T2分别调用methodA()和methodB(),则T2会阻塞等待直到T1执行完成之后才能执行。
-
修饰实例方法时,是对实例进行加锁,锁的是实例对象的对象头,如果调用同一个对象的两个不同的被synchronized修饰的实例方法时,看到的效果和上面的一样,如果调用不同对象的两个不同的被synchronized修饰的实例方法时,则不会阻塞。
41、什么是同步?什么是异步?什么是阻塞?什么是非阻塞?
-
同步与异步是针对IO操作来说的,访问数据时,
-
同步:需要等待,主动请求并等待IO操作完成;
-
异步:不需要等待,主动请求数据后便可以继续处理其它任务,随后等待IO操作完毕;
-
-
阻塞与非阻塞是针对线程来说的,当数据没有准备就绪时,
-
阻塞:线程被挂起,不能去干别的事,持续等待资源中数据准备完成直到返回响应结果;
-
非阻塞:线程没有被挂起,可以去干别的事情,不会持续等待资源准备数据结束后才响应结果,直接返回结果;
-
42、什么是CAS?
-
CAS是
Compare-And-Swap
(比较并交换)的缩写,是一种轻量级的同步机制,主要用于实现多线程环境下的无锁算法和数据结构,保证了并发安全性。它可以在不使用锁(如synchronized、Lock)的情况下,对共享数据进行线程安全的操作。 -
CAS操作主要有三个参数:要更新的内存位置、期望的值和新值。
-
CAS操作的执行过程如下:
-
首先,获取要更新的内存位置的值,记为var。
-
然后,将期望值expected与var进行比较,如果两者相等,则将内存位置的值var更新为新值new。
-
如果两者不相等,则说明有其他线程修改了内存位置的值var,此时CAS操作失败,需要重新尝试。
-
43、在CAS操作过程中,会出现ABA问题。什么是ABA问题?如何解决?
-
ABA问题指在CAS操作过程中,如果变量的值被改为了 A、B、再改回 A,而CAS操作是能够成功的,这时候就可能导致程序出现意外的结果。
-
解决方案:
-
AtomicStampedReference
类:版本号 -
每个共享变量都会关联一个版本号,CAS操作时需要同时检查值和版本号是否匹配。因此,如果共享变量的值被改变了,版本号也会发生变化,即使共享变量被改回原来的值,版本号也不同,因此CAS操作会失败。
-
44、CAS操作可能会受到自旋时间过长的影响,因为如果某个线程一直在自旋等待,会浪费CPU资源。该如何解决?
-
当一个线程请求获取锁时,如果当前线程已经持有锁,则将计数器加1,否则使用CAS操作来获取锁。这样可以避免了使用synchronized关键字或者ReentrantLock等锁的实现机制。
-
当线程获取锁失败时,使用自旋等待的方式,这样可以避免线程进入阻塞状态,避免了线程上下文切换的开销。当重试次数小于10时,使用自旋等待的方式,当重试次数大于10时,则使用阻塞等待的方式。这样可以在多线程环境下保证线程的公平性和效率。
-
在释放锁时,如果计数器大于0,则将计数器减1,否则将锁的拥有者设为null,唤醒其他线程。这样可以确保在有多个线程持有锁的情况下,正确释放锁资源,并唤醒其他等待线程,保证线程的正确性和公平性。
public class SpinLock { private AtomicReference<Thread> owner = new AtomicReference<>(); private int count; public void lock() { Thread currentThread = Thread.currentThread(); // 已经获取了锁 if (owner.get() == currentThread) { count++; return; } // 自旋等待获取锁 while (!owner.compareAndSet(null, currentThread)) { // 自适应自旋 if (count < 10) { count++; } else { // 阻塞等待 LockSupport.park(currentThread); } } } public void unlock() { Thread currentThread = Thread.currentThread(); // 当前线程持有锁 if (owner.get() == currentThread) { if (count > 0) { count--; } else { // 释放锁 owner.compareAndSet(currentThread, null); // 唤醒其他线程 LockSupport.unpark(currentThread); } } } }
45、CAS的应用场景有哪些?
-
线程安全计数器:由于CAS操作是原子性的,因此CAS可以用来实现一个线程安全的计数器;
-
队列:在并发编程中,队列经常用于多线程之间的数据交换。使用CAS可以实现无锁的非阻塞队列(Lock-Free Queue);
-
数据库并发控制:乐观锁就是通过CAS实现的,它可以在数据库并发控制中保证多个事务同时访问同一数据时的一致性;
-
自旋锁:自旋锁是一种非阻塞锁,当线程尝试获取锁时,如果锁已经被其他线程占用,则线程不会进入休眠,而是一直在自旋等待锁的释放。自旋锁的实现可以使用CAS操作;
-
线程池:在多线程编程中,线程池可以提高线程的使用效率。使用CAS操作可以避免对线程池的加锁,从而提高线程池的并发性能。
46、shiro框架的RABC权限模型是什么样的?
-
用户与角色,角色与权限之间的关系都是多对多的关系
-
RABC表结构:5个表,用户表,角色表,权限表,用户角色关系表,角色权限关系表
-
集成springboot,需要两个类:
-
ShiroConfig: Shiro 的一些配置
-
CustomRealm
:自定义的CustomRealm
继承AuthorizingRealm
。并且重写父类中的doGetAuthenticationInfo
(身份认证)、doGetAuthorizationInfo
(权限认证)这两个方法。-
身份认证:用户登录时,执行 subject.login(),调用自定义 Realm 类方法 doGetAuthenticationInfo() ,该方法返回认证信息,最终和 subject.login() 传入的令牌比对,比对成功后,返回一个JSESSIONID,保存在本地 Cookie。
-
权限认证:
-
请求头 Cookie 携带登录成功的 JSESSIONID,被 Shiro 拦截后进行判断该 JSESSIONID 是否已经认证。
-
调用 doGetAuthorizationInfo() 方法从数据源中获取用户的授权信息(如角色和权限),判断用户是否有权限。
-
-
-
47、redisson常见作用有哪些?
-
分布式对象:分布式对象简单来说就是存储在Redis中的Java对象。
Redisson允许你在分布式环境中创建Java对象,并将它们存储在Redis中。这样,不同的Java应用程序或服务就能够共享和访问这些对象,实现数据共享和数据同步。
-
分布式集合:简单来说就是将集合放在Redis中,并且可以多个客户端对集合进行操作。
Redisson支持常见的分布式数据结构,如List、Set、Map、Queue等,使得多个Java应用程序可以并发地访问和修改这些集合。
-
分布式锁:通过Redisson,你可以轻松地实现分布式锁,确保在分布式环境中的并发操作的正确性和一致性。
-
缓存:通过Redisson能够轻松的基于redis实现项目中的缓存
-
常见算法的分布式实现:Redisson提供了一些常用算法的分布式实现,如分布式信号量、分布式位图、分布式计数器等。
48、rabbitmq的使用场景有哪些?
-
消除峰值:用于高并发场景消除峰值,让并发请求在mq中进行排队
-
大数据处理:由于数据量太大,程序一时处理不过来,可以通过把数据放入MQ,多开几个消费者去处理消息,比如:日志收集等
-
服务异步/解耦 :服务之间通过RPC进行通信的方式是同步方式,服务消费方需要等到服务提供方相应结果后才可以继续执行,使用MQ之后的服务通信是异步的,服务之间没有直接的调用关系,而是通过队列进行服务通信, 应用程序解耦合 MQ相当于一个中介,生产方通过MQ与消费方交互,它将应用程序进行解耦合,比如:日志收集等
-
排序保证 FIFO :遵循队列先进先出的特点,可以保证数据按顺序消费
-
注意:对数据的一致性要求较高的业务场景不适合使用MQ,因为MQ具有一定的数据延迟。
49、RabbitMQ中核心概念有哪些?
-
Broker:消息队列服务进程,此进程包括两个部分:Exchange和Queue。
-
Exchange:消息队列交换机,按一定的规则将消息路由转发到某个队列,对消息进行过虑。exchange有下面四种(先了解:fanout,direct,topics,header)
-
直连模型:只需要一个生产者,一个队列,一个消费者即可
-
工作队列:让多个消费者绑定到一个队列,共同消费队列中的消息。同一个消息只向一个消费者投递。
-
发布订阅模型:允许一个消息向多个消费者投递。而对于:fanout , direct , topics都属于发布订阅模型。
-
广播模型:当生产者把消息投递给交换机,交换机会把消息投递给和它绑定的所有队列,而相应的所有的消费者都能收到消息
-
路由模型:不同的消息被不同的队列消费。交换机的类型使用direct
-
通配符:交换机的类型使用topic
-
-
Queue:消息队列,存储消息的队列,消息到达队列并转发给指定的消费方。
-
Producer:消息生产者,即生产方客户端,生产方客户端将消息发送到MQ。
-
Consumer:消息消费者,即消费方客户端,接收MQ转发的消息。
50、延迟队列实现原理
-
-
下单成功(生产者),加入下单消息到队列(order.message)
-
队列设置TTL过期时间(10000毫秒),同时指定了死信交换机“delay-exchange”和死信交换机转发消息的队列“delay-message”
-
消息进入队列,等待一段时间,如果TTL时间到,订单消息会被MQ扔给死信交换机,死信交换机会把消息扔给指定的死信队列delay-message
-
消费者正好监听了死信队列delay-message,就可以获取到消息进行消费,比如检查该消息对应的订单是否支付,做出退库存处理等。
51、什么是死信消息?
-
RabbitMQ可以给队列设置过期时间,也可以单独给每个消息设置过期时间,如果到了过期时间消息没被消费该消息就会标记为死信消息。
-
其他:
-
一是设置了TTL的消息到了TTL过期时间还没被消费,会成为死信
-
二是消息被消费者拒收,并且reject方法的参数里requeue是false,意味这这个消息不会重回队列,该消息会成为死信,
-
三是由于队列大小限制,新的消息进来队列可能满了,MQ会淘汰掉最老的消息,这些消息可能会成为死信消息
-
51、如何避免消息重复消费
-
使用消息的唯一标识来避免重复消费,大概思路如下:
-
找到消息本省的唯一标识,或者在数据中设置一个唯一标识,比如:订单就可以把订单号作为唯一标识
-
消费者每次做了消息消费后,会把这个唯一标识记录下来,比如记录到数据库,或者Redis都可以。
-
消费者每次消费前都拿到消息的这个唯一标识去判断一下消息是否被消费,如果已经被消费国了就不要再消费了