首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

如何针对项目中的技术难点准备面试?——黑马点评为例

  • 25-04-24 14:41
  • 3439
  • 7495
blog.csdn.net

最核心的,包装和准备

个人项目,怎么包装?一定要写出代码才可以吗?

你可以在系统A中实现就可以,了解其中实现的细节,怎么跟面试官对线等等,这些话术到位了之后,再把它融入到系统B,这样即可。

举个例子

一个大前提,你要想好怎么跟面试官对线?

知道怎么对线后,自然就知道,怎么去提前准备这块内容,举例子:

你的简历写了这句话,那么你要怎么准备?

  • 对热点数据做缓存,针对可能的缓存穿透,同时使用缓存空值与布隆过滤器解决;针对热点数据过期,根据不同的数据一致性要求,采用不同的缓存构建方案,防止缓存击穿;

你的简历写了异步秒杀业务,你又该怎么介绍?

1、业务大致逻辑的介绍

业务是用户可以抢购大额代金券,来抵扣购买课程所需金额,一个用户只能抢购一张大额优惠券

相关的表结构

平价券表

自增id、代金券标题、副标题、使用规则、支付金额、抵扣金额、类型 0普通 1秒杀、状态 1 2 3、创建时间、更新时间

秒杀券表

在平价优惠券基础上,秒杀优惠券有其他字段,独立成一张表。

关联平价券的自增id、库存、秒杀开始时间、秒杀结束时间、创建时间、更新时间

订单表

Id 订单编号(全局id)、下单用户id、购买的优惠券id、支付方式 1 2 3、订单状态 1 2 3 4 5 6 7、抢购时间、支付时间、核销时间、退款时间、更新时间

有啥难点?

一人一单、不超卖、保证并发量 等等

2、代码一步步实现的过程介绍

方案的比较

选择哪个锁?

整体逻辑的 初步设计是怎么样的?

使用基于数据库的锁 以及 JVM的锁实现功能

初步设计存在什么问题呢?

多集群部署时,JVM不能看到同一把锁

后续又基于什么、或者通过什么方式进行完善优化?

业务迁移到 redis 来做 、由最初的 JVM层面的队列,到引入redis的stream,再到引入MQ等等

那么优化了多少?

数据呈现!qps等等

怎么迭代优惠券秒杀功能?

业务场景是:用户可以抢购数量有限的大额优惠券,并且每个用户最多只能抢一张。

怎么解决超卖问题?方案对比,选择乐观锁

所以这个功能首先要完成的是:不要出现库存超卖的情况,

有两个解决方案:悲观锁syn & 乐观锁 cas

悲观锁的思想:认为我在减库存的时候,一定有其他用户也在减,为了防止这种现象,减库存时,加了一个同步锁synchronized,来解决并发问题

乐观锁的思想:乐观锁是认为我在减库存的时候,不一定会发生并发问题,就算有,我就放弃此次操作,再重新尝试减一次。实现这一机制:

就是在减库存的时候,判断 库存是否 > 0即可,只要是 > 0,就可以卖

当出现 <= 0时,就减库存失败

基于乐观锁的性能比悲观锁要好,因为

悲观锁只允许一个线程在同步代码块执行,其余线程必须等待锁释放,性能差

而基于库存是否 > 0的乐观锁,只有在库存真的 <= 0,才会并发失败,性能远远比悲观锁好。

经过以上方案的比较,项目采用乐观锁来解决超买问题。

接下来是要解决每个用户只能抢一张优惠券的问题

怎么保证每个用户只能抢一张优惠券呢?

项目是这样解决的,首先确定无法使用乐观锁来解决

因为用户抢到优惠券,在他没抢到之前,数据库并没有记录,无法根据字段进行乐观锁。

所以采用悲观锁的方案,因为目前是在解决单个用户发起的并发请求,只需要针对单个用户进行加锁,

确定锁的粒度为每个用户,锁对象为用户id,String 类型,为了防止加锁的对象不是同一个,采用的是toString().intern(),不同的请求,才会从字符串常量池中返回同一个对象,才能解决单个用户并发问题。

确定加锁范围判断用户是否已抢购 -> 乐观锁解决减库存问题 -> 把抢购记录,写入数据库

如果加锁范围只到乐观锁解决库存问题,是无法避免单个用户的并发请求问题的。

这是针对单个服务可用的方法,因为synchronized锁,基于JVM实例

如果部署多台服务,有多个JVM,synchronized无法做到分布式锁,

所以在集群部署下,还会出现一人一单并发问题

思考到集群下的JVM锁问题,采取分布式锁优化:

使用分布式锁,解决集群下的一人一单问题

为了解决上面说到的问题,决定使用跨JVM的锁,即分布式锁,redis就是很好的选择。

首先自定义了一个比较简单的分布式锁

存在的问题是锁超时释放,但是业务还未执行完毕

(想要更好的解决,可以使用redis分布式工具:redisson)

支持锁重入:利用hash结构,通过记录线程id、锁的数量,来达到重入

锁超时自动续费:保证是业务执行完毕,才释放的锁,不会被其他线程趁虚而入

每隔 1/3 的时间,会重置超时时间

支持锁等待:即获取不到锁时,利用发布订阅 & 信号量的机制,等锁释放了 再去重试,对CPU友好。

到目前位置,业务流程为查询优惠券信息 ->加分布式锁来解决同一用户的并发请求-> 进行一人一单的判断,需要查询数据库->进行乐观锁库存超卖的判断,需要更新数据库->抢购成功,创建订单,写入数据库。

可以看到目前的流程存在大量的IO& 锁,整体性能通过JMeter测试,

1000个用户,200库存的优惠券,处理请求的平均耗时接近500ms

存在许多耗时的数据库操作 & 锁,还可以怎么提高性能呢?

基于redis:秒杀资格判断异步写入数据库思路

通过定时任务把MySQL中参与秒杀的代金券,同步到Redis中做库存的预扣减,基于Redis解决库存超卖与一人一单,RocketMQ实现异步解耦,QPS从400提升至1200;

对业务进行拆分,决定将耗时的数据库操作,放到redis来做,具体为:秒杀资格的判断

新增秒杀优惠券的同时,将优惠券信息预热在redis中

在redis中判断用户是否已经下过单,

使用redis数据类型:Set,存放已经下过单的用户信息,

方便以O(1)复杂度判断用户是否下单sismember、sadd

key为:seckill:order:优惠券id

如果还未下过单,使用redis判断库存是否充足,如果库存充足,则需要减1

使用redis数据类型:hash,存储优惠券信息

get、incrby减库存

key为:seckill:stock:优惠券id

上述过程,是多条命令,无法保证这些命令执行的原子性,会出现并发问题,所以使用lua脚本

保证执行上述命令的原子性

相当于把之前的分布式锁解决一人一单、乐观锁解决库存超卖的问题,通过基于内存的redis解决了

大大提高性能


RocketMQ实现异步解耦,QPS从400提升至1200;


若判断用户有资格抢购,在这之前采用的是同步操作,同步等待信息写入数据库,

即用户请求需要等待抢购信息写入数据库,才可以返回

优化的解决方案是:向消息队列RocketMQ中添加消息(分布式id、优惠券id、用户id),立刻返回用户请求,

开启异步线程,实现异步写入数据库的操作。减少响应时间,提高用户体验。

一开始使用的是JDK自带的阻塞队列,耗时200ms

阻塞队列在获取消息时,如果没有消息,就阻塞住;等到有消息加入了,就被唤醒

使用jdk自带的阻塞队列缺点:

  1. 使用的是JDK的阻塞队列,用的是JVM的内存,如果不加以限制,在高并发下,可能有无数的订单放到阻塞队列,可能会导致内存溢出,也就是内存受到限制。

  2. 消息一旦取出,就消失了,不能保证一定被消费

  3. 不支持持久化,目前是基于内存保存订单信息,如果服务宕机,内存所有订单信息都丢失;

选择Stream消息队列替代JDK自带的阻塞队列

耗时100ms

比较redis 不同方式实现消息队列之间的优缺点,即为什么选择Stream而不是List?

最重要的是记住Stream的优点(持久化、全局ID、解决消息漏读、pendin-list保证消息至少消费一次、独立于JVM的内存、支持消费者组消费,减少消息挤压、可以阻塞读取)

理解内部实现,来说明为什么有这些优点。

Stream相关的八股

具体落实到项目中,怎么实现?

创建一个Stream消息队列,不指定上限

lua脚本判断有资格后,向消息队列添加消息

项目启动时,开启异步线程,阻塞读取Stream消息队列中的消息,完成写入数据库操作

如果成功消费,那么发送ack确认给消息队列,消息才会从pending队列中移除

如果消费出现问题,就到该消费者的pending队列中,再次消费

专业消息队列RocketMQ

RocketMQ使用并发消费模式,并设置合理的线程数量(IO类型,写库存),快速处理队列中堆积的消息,使

用Redis的分布式锁+自旋锁,对商品的库存进行并发控制,把并发压力转移到Redis中,缓解DB压力;

因为并发消费,对数据库减库存操作,是不安全的

除非直接利用数据库乐观锁减

而不是先去读再减 ,直接减

但是对DB压力大

使用redis乐观锁 + sleep + 自旋来解决

3、未来展望 or 再次迭代 or 这个功能有什么可以完善的地方?

如果没下单,库存怎么还回去?

使用延时队列? 那么又引出 - 延时队列怎么实现的?

其实redis 的 stream同样的,又引出八股文,这些都是需要准备的 Stream相关的八股

.....

自定义的分布式锁,相比官方提供的,存在缺陷,如:

最严重的 业务未结束,锁先超时释放了,其他线程趁虚而入、

不支持 锁重入:用hash即可、

不支持 阻塞等待:用信号量、发布/订阅机制 即可、

在多redis实例下,即主从模式下因为是异步复制的,导致分布式锁不可靠性:官方提供的 红锁 解决

redisson 针对前面三个缺陷、RedLock 红锁

4、实现过程中遇到了什么难点?什么bug?

@Transational失效,因为不是代理对象调用。深入了理解Spring事务原理 -- Aop。

怎么解决?

  • 比较笨方法:新开一个类

  • 或者 自己注入自己,进行调用,也是代理对象的调用

  • 获取代理对象来解决。

JVM的syn悲观锁解决一人一单问题的时候:

用的是用户的id,忘记intern放到字符串常量池,

导致获取String对象的时候,每次都是新的对象,即 加 对象锁出现问题

还有syn锁范围设置的不够大,释放锁之后,事务还未写入,导致数据库记录还未变更,存在并发问题

.....

5、如果你的简历 关键字出现,分布式id、分布式锁、qps等等

心里就要思考到,哪些是会被提问的?

怎么进行压力测试的?

QPS、并发量、平均花费时间 等的关系:QPS和并发数和平均耗时的关系以及压测思路_qps和并发数的关系-CSDN博客

分布式id相关的准备

为什么不采用数据库自增id?

单一表的存储容量有上限

当分表存储时,会存在重复的id

规律性明显,容易看出订单销量等状态

分布式ID是什么?

是应用在分布式系统中,保证全局唯一的自增id。

它可以让一个业务,不管有多少个服务、多少张表,都可以拥有唯一的自增id。

全局唯一的分布式ID怎么实现?

使用redisString数据类型的incr自增命令,来帮助生成全局唯一id,有以下好处:

因为redis执行命令是单线程的,所以在执行自增命令生成自增id时,

不存在并发问题,自然不会导致id重复的问题;

并且是自增的,符合分布式id要求;

并且redis基于内存操作,性能极高;

为了保证生成的id安全性,具体如下操作:

采用long类型存储id,long类型64位

· 第一个符号位,永远为0

· 接下来的31bit,采用精确到秒的时间戳进行存储

o 时间戳如何计算得来:定义一个初始时间,用当前下单时间减去初始时间,得到31bit

· 后面的32bit,是为了解决在一秒内重复的下单,足够容纳一秒内的订单量

如何运算?

先得到当前时间 - 初始时间的时间戳,然后左移32位,给一天的订单量让出32位bit

使用自增命令,得到自增值,要保证不会超过32bit,然后直接进行或运算

return timestamp << COUNT_BITS | count;

时间戳的代码

/** * 初始时间的时间戳,本质是从1970-01-01 00:00:00 到2022-01-01 00:00:00 经过多少秒 */ private static final long BEGIN_TIMESTAMP = 1640995200L;

//测试时间戳 public static void main(String[] args) { LocalDateTime time = LocalDateTime.of(2022, 1, 1, 0, 0, 0); System.out.println( time.toEpochSecond(ZoneOffset.UTC)); }

自增命令的key怎么设置比较好?

在自增中,采用的是32bit来存储自增值,也就是说自增值超过32bit存储容量,就会不符合我们的要求。

所以在设置key时,采用一天一个key,一天订单量很难超过32bit,也就是自增值不会超过

o 如:("icr:" + keyPrefix + ":"+"2022:03:20"),keyPrefix 为业务名称

o 还带来统计方便的好处

§ 比如某天的订单数,直接看对应key的自增数字就可以。这样做统计简单很多。

自增id生成器代码

  1. @Component
  2. public class RedisIdWorker {
  3. /**
  4. \* 初始时间的时间戳,本质是从1970-01-01 00:00:00 到2022-01-01 00:00:00 经过多少秒
  5. */
  6. private static final long BEGIN_TIMESTAMP = 1640995200L;
  7. //测试时间戳
  8. public static void main(String[] args) {
  9. LocalDateTime time = LocalDateTime.of(2022, 1, 1, 0, 0, 0);
  10. System.out.println( time.toEpochSecond(ZoneOffset.UTC));
  11. }
  12. /**
  13. \* 序列号的位数
  14. */
  15. private static final int COUNT_BITS = 32;
  16. @Resource
  17. private StringRedisTemplate stringRedisTemplate;
  18. /**
  19. \* @param keyPrefix key前缀,不同业务有不同的key
  20. \* @return long型,作为id,占用更少空间,有利于索引建立
  21. */
  22. public long nextId(String keyPrefix) {
  23. // 符号位不用管,只要保证正数就可以,怎么保证? 时间戳中,当前时间 - 初始时间,当前时间要 > 初始时间
  24. ​ // 1.生成当前时间的 时间戳
  25. ​ LocalDateTime now = LocalDateTime.now();
  26. ​ long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
  27. // 当前时间 - 初始时间
  28. ​ long timestamp = nowSecond - BEGIN_TIMESTAMP;
  29. ​ // 2.生成序列号
  30. ​ // 2.1.获取当前日期,精确到天
  31. ​ String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
  32. ​
  33. ​ long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
  34. ​ // 3.拼接并返回,如果直接拼接得到的是字符串,返回要long。所以这里采用位运算
  35. // 先把时间戳挪到高位,在这里 左移32位。 再跟序列号进行 或运算
  36. ​ return timestamp << COUNT_BITS | count;
  37. }
  38. }
你还了解哪些分布式ID生成算法?

除了基于redis生成的分布式id,还了解雪花算法、uuid、数据库自增id

雪花算法 同样采用64bit存储

o 第一位表示符号位,为0

o 接下来的41bit,用于表示精确到毫秒的时间戳

o 接下来的10bit,(这一部分可以灵活调整)

§ 前5位表示机器id,后5位表示机房id

o 剩下的12bit,用来表示一毫秒内,能够生成的id数量

优点:

生成速度快,有序递增、易于再此基础上改造

缺点:

依赖于时间,当机器的时间对应不上时,可能导致重复id

uuid 基于时间、机器id的生成方案

缺点是:

占用内存大,128bit

时间问题,导致id重复

可以保证唯一,但是不是自增的

若redis服务宕机,分布式id如何生成?

采用redis主从复制 + 哨兵机制,来达到服务的高可用

当主节点宕机时,自动故障转移

主从复制保证数据同步。

6、分布式锁相关的准备

分布式锁是什么?

满足分布式或集群模式下,多线程可见 且 互斥的锁。

怎么基于redis实现?

使用redis的 setnx命令,来实现分布式锁,非阻塞,获取失败,直接返回

加锁操作:setnx

因为redis执行命令是单线程,不会并发安全问题

并且为了防止死锁,加了key的过期时间

并且将value设置唯一标识,是为了防止锁误删的现象

解锁操作:基于lua脚本,因为不止一条命令

首先判断该锁是不是自己加的,即检查唯一标识get

如果是,才可以进行解锁del

锁误删现象是什么?

比如目前线程A,持有锁,当时因为阻塞,导致业务没执行完,锁超时释放了

此时线程B重新持有锁,进行业务处理,

在线程B还没处理完业务时,线程A处理好了,并且二话不说,直接把锁删除了

这就导致线程B的锁,被线程A删掉的情况。导致锁误删

这时,其他线程又可以趁虚而入了。

唯一标识怎么设置?

因为目前讨论的是项目在集群部署的环境下,线程id可能重复

所以基于每个线程的id + UUID来进行唯一标识的设置。

为什么解锁要使用lua脚本

因为解锁是两个操作get、del,必须保证解锁的原子性,否则可能出现以下现象:锁误删

判断该锁是我之前加的

进行解锁时,阻塞了

知道锁超时释放,接着其他线程进行加锁

自己从阻塞状态恢复,执行业务,dek把别人的锁又给删除了

自定义的分布式锁,存在什么问题?

锁误删问题解决了,但是还存在一个比较严重的问题,就是锁超时时间的设置

如果设置的太短,可能业务还没执行完 或者 业务阻塞,导致锁超时释放

其他线程趁虚而入,又导致了一人不止下一单问题的出现。

不支持锁重入、锁超时自动续费、锁等待、

主从模式下因为是异步复制的,导致分布式锁不可靠性

怎么解决自定义分布式锁问题?

使用redis分布式工具:redisson

· 支持锁重入:利用hash结构,通过记录线程id、锁的数量,来达到重入

· 锁超时自动续费:保证是业务执行完毕,才释放的锁,不会被其他线程趁虚而入

o 每隔 1/3 的时间,会重置超时时间

· 支持锁等待:即获取不到锁时,利用发布订阅 & 信号量的机制,等锁释放了 再去重试,对CPU友好。

Redis 如何解决集群情况下分布式锁的可靠性?

redis官方是实现了红锁RedLock,专门来解决集群模式下分布式锁不可靠的问题,

redis推荐使用5个独立的redis主服务器

它加锁的过程如下:

记录开始访问的时间t1,线程依次访问5个主服务器,进行set nx px的操作,

会带上唯一标识

加上超时时间,是为了锁一定会被释放

并且还设定了获取锁的时间,一般设置为几十毫秒,

如果在时间内获取不到,那么就返回,不会再某个redis服务耗费太多的获取锁时间

最后统计线程成功获取了几把锁,要获取到一半以上,并且将获取锁的总时间 与 设置的锁过期时间对比

如果 获取锁的总时间>设置的锁过期时间,那么加锁失败

如果没有获取到一半以上的锁,在这里是3把锁,也是加锁失败

故加锁成功要同时满足两个条件:

· 获取到超过半数以上的锁

· 加锁的总耗时,不大于 锁的过期时间

并且在执行业务时,真正能够利用的锁时间为:设置的锁超时时间 - 获取锁的总耗时

如果觉得锁的时间已经来不及完成业务执行,那么可以直接释放全部锁,让下一个线程来操作

避免业务还没执行完,就出现释放锁的现象

解锁操作:

加锁失败后,会向所有redis主节点发起解锁操作,执行lua脚本保证解锁的原子性

完整代码,要稍微注意一下lua脚本怎么写

  1. // 在项目一启动类加载时就加载static代码块,只加载一次,性能最好。
  2. // DefaultRedisScript是实现类,泛型为脚本的返回值类型
  3. private static final DefaultRedisScript UNLOCK_SCRIPT;
  4. static {
  5. // 因为要写不止一行,所以放到代码块
  6. UNLOCK_SCRIPT = new DefaultRedisScript<>();
  7. // 去类路径下找
  8. UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
  9. // 设置返回值类型
  10. UNLOCK_SCRIPT.setResultType(Long.class);
  11. }
  12. @Override
  13. public void unlock() {
  14. // 释放锁
  15. // stringRedisTemplate.delete(KEY_PREFIX + name);
  16. /*// 获取线程标示
  17. String threadId = ID_PREFIX + Thread.currentThread().getId();
  18. // 获取锁中的标示
  19. String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
  20. // 判断标示是否一致
  21. if(threadId.equals(id)) {
  22. // 释放锁
  23. stringRedisTemplate.delete(KEY_PREFIX + name);
  24. }*/
  25. // 调用lua脚本
  26. stringRedisTemplate.execute(
  27. UNLOCK_SCRIPT,
  28. // 生成单元素的集合:singletonList方法
  29. Collections.singletonList(KEY_PREFIX + name),
  30. ID_PREFIX + Thread.currentThread().getId());
  31. }

注:本文转载自blog.csdn.net的宏夏c的文章"https://blog.csdn.net/qq_32792547/article/details/142886458"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2492) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

131
学习和成长
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2024 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top