跳转至

项目面试问题总结

垃圾回收器

redis

  • 讲讲 redis,它在你的项目中作用是什么?
  • Redis 是高性能的基于键值对的写入缓存的 内存存储系统。它支持多种数据结构如字符串、哈希表、列表、集合、有序集合等,并提供了丰富的操作命令。
  • 项目中引入 Redis 的地方是:查询品牌、客服营业状态 ,像这种品牌客服营业状态,本项目无非就两个状态:营业中/打样。而且它属于高频查询。只要用户浏览到这个店铺,前端就要自动发送请求到后端查询店铺状态。Redis 是基于键值对这种形式存储的,而且 Redis 也把将数据放到缓存中,而不是磁盘,有效缓解了这种高频查询给磁盘带来的压力。
  • 在项目中redis还有缓存商品的功能。用户端小程序展示商品数据都是通过查询数据库获得,如果用户端访问量比较大,数据库访问压力随之增大。结果:系统响应慢、用户体验差。
  • 当在后台修改菜品数据时,为了保证Redis缓存中的数据和数据库中的数据时刻保持一致,当修改后,需要清空对应的缓存数据。用户再次访问时,还是先从数据库中查询,同时再把查询的结果存储到Redis中,这样,就能保证缓存和数据库的数据保持一致。

  • 使用的是 opsForValue() 方法,它用于操作 Redis 中的 String 类型数据,而不是哈希(Hash)类型。

  • 使用了 redisTemplate.opsForValue().set(key, list) 将菜品信息存储在 Redis 中,这表示您的数据结构是将菜品信息以字符串的形式存储在 Redis 中,并使用了一个带有分类 ID 的键。这种存储方式的好处是简单直接,但也有一些限制,例如无法直接对特定字段进行更新,需要先取出整个对象,然后再修改后重新存储。

  • 如果您希望更灵活地操作菜品信息,可以考虑将菜品信息存储为哈希类型。这样,您可以在哈希中存储菜品的各个属性,例如菜品名称、价格、描述等,每个属性都可以单独访问和更新,更加灵活。

  • 商家状态:基于Redis的字符串来进行存储 约定:1表示营业 0表示打烊

  • 你的redis是怎么优化的?

  • 合理的数据结构选择:键鼠套装:一个套装的信息是用哈希存储,内部的键盘鼠标列表用键鼠套装的id作为键存储在set

  • 使用哈希类型存储对象:不使用哈希时,你可能会为每个用户属性创建一个键,需要多个命令来存储或查询单个用户的全部信息,查询效率较低,尤其是当需要获取用户的所有信息时。

  • 使用哈希类型,可以将一个用户的所有信息存储在一个键中,每个字段代表用户的一个属性,提高了查询效率,通过一个命令HGETALL user:1001就可以查询到用户的所有信息。

  • 适当的过期策略:如果不为缓存的热门商品内容设置过期时间,缓存的命中率会下降,缓存空间会越来越紧张,查询速度会变慢

  • 为缓存数据设置合理的过期时间,可以保证数据的时效性同时避免长时间占用内存。对于那些更新频率不高但读取频繁的数据,可以设置较长的过期时间。

  • 定期删除(删除还有惰性删除):每隔一段时间,我们就对一些key进行检查,删除里面过期的key(从一定数量的数据库中取出一定数量的随机key进行检查,并删除其中的过期key)。

  • 内存管理:监控和配置 Redis 的内存使用策略,如启用内存淘汰策略,在内存不足时自动删除部分数据,确保系统稳定运行。

  • redis缓存原理,为什么快?

  • Redis 的数据是保存在内存中的(内存数据库,支持持久化),因此读写速度非常快,Redis 存储的是 KV 键值对数据。
  • Redis 内部做了非常多的性能优化,比较重要的有下面 3 点:
  • Redis 基于内存,内存的访问速度是磁盘的上千倍,C语言编写;
  • Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型,主要是单线程事件循环和 IO 多路复用(Redis 线程模式后面会详细介绍到);
  • 采用单线程,避免不必要的上下文切换可竞争条件(多线程的话还要使用锁,性能会降低)
  • Redis 内置了多种优化过后的数据类型/结构实现,性能非常高。

  • redis的基本数据类型?

  • String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)。

  • String 是 Redis 中最简单同时也是最常用的一个数据类型。String 是一种二进制安全的数据类型,可以用来存储任何类型的数据比如字符串、整数、浮点数、图片(图片的 base64 编码或者解码或者图片的路径)、序列化后的对象。
  • Redis 的 List 的实现为一个 双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。
  • Redis 中的 Hash 是一个 String 类型的 field-value(键值对) 的映射表,特别适合用于存储对象,后续操作的时候,你可以直接修改这个对象中的某些字段的值。Hash 类似于 JDK1.8 前的 HashMap,内部实现也差不多(数组 + 链表)。
  • Redis 中的 Set 类型是一种无序集合,集合中的元素没有先后顺序但都唯一,有点类似于 Java 中的 HashSet 。当你需要存储一个列表数据,又不希望出现重复数据时,Set 是一个很好的选择,并且 Set 提供了判断某个元素是否在一个 Set 集合内的重要接口,这个也是 List 所不能提供的。
  • Sorted Set 类似于 Set,但和 Set 相比,Sorted Set 增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列,还可以通过 score 的范围来获取元素的列表。
  • zset底层数据结构
  • 压缩列表+跳表
  • 当数据量小的时候使用压缩列表
  • 跳表的原理就是建立多级索引,然后通过索引挑来跳去,查找时间复杂度logN (类似二分) 删除查找都是logN
  • zset有一个很核心的操作就是范围查找,范围查找的效率比红黑树、二叉树高
  • zset的实现比红黑树二叉树简单,能通过控制索引的层级,来控制内存
  • 相对平衡树,对于范围查询来说,它也可以通过中序遍历的方式达到和跳表一样的效果。但是它的每一次插入或者删除操作都需要保证整颗树左右节点的绝对平衡,只要不平衡就要通过旋转操作来保持平衡,这个过程是比较耗时的。
  • B+树它是绝对的平衡,即树的各个分支高度相差不大,确保查询和插入时间复杂度为O(log n),叶子节点间通过链表指针相连,范围查询表现出色。它的核心思想是通过可能少的 IO 定位到尽可能多的索引来获得查询数据。

  • redis 的淘汰机制是怎么样的?(假如缓存过多,内存是有限的,内存被占满了怎么办?)

  • 数据的淘汰策略:当 Redis 中的内存不够用时,此时在向 Redis 中添加新的 key,那么 Redis 就会按照某一种规则将内存中的数据删除掉,这种数据的删除规则被称之为内存的淘汰策略。

  • 这个在 redis 中提供了很多种,默认是 noeviction,不淘汰任何 key,内部不足直接报错
  • 但是内存满时不允许写入新数据。还有随机淘汰以及 LRU(最少最近使用)和 LFU(最少频率使用)等等。
  • 是可以在redis的配置文件中进行设置的,里面有两个非常重要的概念,一个是LRU,另外一个是LFU
  • LRU的意思就是最少最近使用,用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高。
  • LFU的意思是最少频率使用。会统计每个key的访问频率,值越小淘汰优先级越高
  • 我们在项目设置的allkeys-lru,挑选最近最少使用的数据淘汰,把一些经常访问的key留在redis中
  • 保证Redis中的数据都是热点数据 ?可以使用 allkeys-lru (挑选最近最少使用的数据淘汰)淘汰策略,那留下来的都是经常访问的热点数据

  • Redis的过期时间要怎么设置

  • EXPIRE key seconds

  • 要是lua脚本来保证

  • lua· if redis.call('set', KEYS[1], ARGV[1]) then return redis.call('expire', KEYS[1], ARGV[2]) else return nil end

  • redis-cli EVAL "$(cat script.lua)" 1 mykey myvalue 60

  • 在你的项目中 redis 作为缓存, MySQL 的数据如何与 redis 进行同步呢?

  • 这个项目中用户在查看客服状态和店铺状态时,需要让数据库与 redis 高度保持一致,因为如果店铺没有营业的话就不能点单了,所以它要求时效性比较高,所以采用的读写锁保证的强一致性。我们采用的是 redisson 实现的读写锁,在读的时候添加共享锁,可以保证读读不互斥,读写互斥。当我们更新数据的时候,添加排他锁,它是读写,读读都互斥,这样就能保证在写数据的同时是不会让其他线程读数据的,避免了脏数据。这里面需要注意的是读方法和写方法上需要使用同一把锁才行。
  • 商品的更新使用延迟双删,如果是写操作,我们先把缓存中的数据删除,然后更新数据库,最后再延时删除缓存中的数据
  • 其中这个延时多久不太好确定,在延时的过程中可能会出现脏数据,并不能保证强一致性,所以没有采用它。

  • 如何解决 Redis 的缓存穿透问题

  • 缓存穿透是指查询一个一定不存在的数据,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到 DB 去查询,可能导致 DB 挂掉。这种情况大概率是遭到了攻击。

  • 使用布隆过滤器来解决缓存穿透:主要是用于检索一个元素是否在一个集合中。我们当时使用的是 redisson 实现的布隆过滤器。它的底层主要是一个bitmap,里面存放的二进制 0 或 1。在一开始都是 0,当一个 key 来了之后经过 3 次 hash 计算,模于数组长度找到数据的下标然后把数组中原来的 0 改为 1,这样的话,三个数组的位置就能标明一个 key 的存在。
  • 查找的过程也是一样的。当然是有缺点的,布隆过滤器有可能会产生一定的误判,我们一般可以设置这个误判率,大概不会超过 5%,其实这个误判是必然存在的,要不就得增加数组的长度,其实已经算是很划分了,5%以内的误判率一般的项目也能接受,不至于高并发下压倒数据库。

  • 布隆过滤器是你自己写的还是第三方,自己写的误差知道是多少吗?有考虑过吗。讲讲你的布隆过滤器是怎么自定义实现的

  • 布隆的误差率是自己给定的,然后根据给定的误差率来计算出哈希函数的个数还有bitmap的大小

  • 使用布隆过滤器来解决缓存穿透:主要是用于检索一个元素是否在一个集合中。我们当时使用的是 redisson 实现的布隆过滤器。它的底层主要是一个bitmap,里面存放的二进制 0 或 1。在一开始都是 0,当一个 key 来了之后经过 3 次 hash 计算,模于数组长度找到数据的下标然后把数组中原来的 0 改为 1,这样的话,三个数组的位置就能标明一个 key 的存在。
  • 查找的过程也是一样的。当然是有缺点的,布隆过滤器有可能会产生一定的误判,我们一般可以设置这个误判率,大概不会超过 5%,其实这个误判是必然存在的,要不就得增加数组的长度,其实已经算是很划分了,5%以内的误判率一般的项目也能接受,不至于高并发下压倒数据库。
  • 自己实现https://zhuanlan.zhihu.com/p/685677923?utm_psn=1752045231543799808

  • redis 的 IO 多路复用是什么? Redis 的 IO 多路复用是一种技术,允许 Redis 同时监听多个客户端连接,并在有数据到达时及时处理,提高了 IO 效率和性能。它通过一种机制来管理和处理多个连接,使得 Redis 能够高效地处理大量客户端请求。

  • I/O多路复用是指利用单个线程来同时监听多个Socket ,并在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。目前的I/O多路复用都是采用的epoll模式实现,它会在通知用户进程Socket就绪的同时,把已就绪的Socket写入用户空间,不需要挨个遍历Socket来判断是否就绪,提升了性能。
  • 其中Redis的网络模型就是使用I/O多路复用结合事件的处理器来应对多个Socket请求,比如,提供了连接应答处理器、命令回复处理器,命令请求处理器;
  • 在Redis6.0之后,为了提升更好的性能,在命令回复处理器使用了多线程来处理回复事件,在命令请求处理器中,将命令的转换使用了多线程,增加命令转换速度,在命令执行的时候,依然是单线程,依然线程安全
  • 阶段一:用户进程调用select,指定要监听的Socket集合,内核监听对应的多个socket,任意一个或多个socket数据就绪则返回readable,此过程中用户进程阻塞
  • 阶段二:用户进程找到就绪的socket, 依次调用recvfrom读取数据,内核将数据拷贝到用户空间,用户进程处理数据
  • I/O多路复用是利用单个线程来同时监听多个Socket ,并在某个Socket可读、可写时得到通知,从而避免无效的等待充分利用CPU资源。不过监听Socket的方式、通知的方式又有多种实现,常见的有:select,poll,epoll
  • select和poII只会通知用户进程有Socket就绪,但不确定具体是哪个Socket,需要用户进程逐个遍历Socket来确认
  • epoll则会在通知用户进程Socket就绪的同时,把已就绪的Socket写入用户空间

  • redis集群的数据一致性你是怎么解决的?

  • 在Redis中提供的集群方案总共有三种:主从复制、哨兵模式、Redis分片集群

  • 主从同步:单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,可以搭建主从集群,实现读写分离。一般都是一主多从,主节点负责写数据,从节点负责读数据,主节点写入数据之后,需要把数据同步到从节点中
  • 主从同步数据的流程:主从同步分为了两个阶段,一个是全量同步,一个是增量同步。全量同步是指从节点第一次与主节点建立连接的时候使用全量同步,流程是这样的:从节点请求主节点同步数据,其中从节点会携带自己的replication id和offset偏移量。主节点判断是否是第一次请求,主要判断的依据就是,主节点与从节点是否是同一个replication id,如果不是,就说明是第一次同步,那主节点就会把自己的replication id和offset发送给从节点,让从节点与主节点的信息保持一致。在同时主节点会执行bgsave,生成rdb文件后,发送给从节点去执行,从节点先把自己的数据清空,然后执行主节点发送过来的rdb文件,这样就保持了一致。当然,如果在rdb生成执行期间,依然有请求到了主节点,而主节点会以命令的方式记录到缓冲区,缓冲区是一个日志文件,最后把这个日志文件发送给从节点,这样就能保证主节点与从节点完全一致了,后期再同步数据的时候,都是依赖于这个日志文件,这个就是全量同步
  • 增量同步指的是,当从节点服务重启之后,数据就不一致了,所以这个时候,从节点会请求主节点同步数据,主节点还是判断不是第一次请求,不是第一次就获取从节点的offset值,然后主节点从命令日志中获取offset值之后的数据,发送给从节点进行数据同步

  • redis缓存查询速度是如何进行测试的

  • 1. 使用redis-benchmark工具

Redis自带了一个性能测试工具redis-benchmark,它可以用来执行基本的性能测试。这个工具测试Redis服务器的性能,包括每秒执行的请求次数(TPS或QPS)和请求的平均延迟。

基本用法如下:

redis-benchmark -h [hostname] -p [port] -c [concurrent_connections] -n [number_of_requests]

其中:

  • -h 指定Redis服务器的主机名。
  • -p 指定Redis服务器的端口。
  • -c 设置并发连接数。
  • -n 设置请求总数。

### 2. 使用redis-cli--latency--stat命令

  • redis-cli --latency -h [hostname] -p [port]:监视Redis服务器的延迟。
  • redis-cli --stat:提供Redis服务器的统计信息,包括每秒处理的命令数。

JWT令牌

  • 项目中怎么使用的?

  • 首先是员工登录的时候每次的请求从前端都会携带jwt令牌,然后通过JWT令牌可以解析出当前登录员工id

  • 后端的拦截器会将解析出来的当前登录员工id存入threadlocal线程局部变量中

  • 在处理请求的后续步骤中,就可以通过ThreadLocal轻松访问用户信息,而无需再次解析JWT

  • 什么是threadlocal, 为什么要用这个,以及底层原理

  • ThreadLocal是多线程中对于解决线程安全的一个操作类,它会为每个线程都分配一个独立的线程副本从而解决了变量并发访问冲突的问题, ThreadLocal 同时实现了线程内的资源共享

  • 我用这个主要是为了线程内共享用户信息, 比如在每次堆数据库操作的时候都要保留操作者的id

  • 在ThreadLocal内部维护了一个 ThreadLocalMap 类型的成员变量,用来存储资源对象

当我们调用 set 方法,就是以 ThreadLocal 自己作为 key,资源对象作为 value,放入当前线程的 ThreadLocalMap 集合中

当调用 get 方法,就是以 ThreadLocal 自己作为 key,到当前线程中查找关联的资源值

当调用 remove 方法,就是以 ThreadLocal 自己作为 key,移除当前线程关联的资源值

  • ThreadLocal 作用,如何保证数据安全?

    • 作用:ThreadLocal 为每一个线程提供一个单独的存储空间,具有线程隔离的作用,只有在同一个线程内才可以获得他的值,以保证线程安全。
    • 在本次开发中,在对 JWT 令牌进行解析,获得当前请求的用户 ID 以后将该 ID 保存至 ThreadLocal 中,以便在之后的操作中查看当前用户。
  • 我们公司之前用这个出现过脏读问题,你变量是怎么清除。

  • ThreadLocal通常用于在单个线程的上下文中保存数据,使得这些数据只对当前线程可见。这种机制通常不会引起传统意义上的脏读问题,因为每个线程都有其自己的独立副本,不会相互干扰。

  • 脏读(Dirty Read)通常发生在数据库事务中,指一个事务读取了另一个事务未提交的数据。如果这些未提交的数据最终被回滚(撤销),那么第一个事务读取的就是无效的或者错误的数据。在ThreadLocal的上下文中,"脏读"可能被广义地解释为一个线程读取到了由于ThreadLocal不当使用而残留的、上一个使用相同线程的操作遗留下的数据。
  • 在使用ThreadLocal时,最重要的一点是确保不再需要它时及时清理存储在其中的数据,特别是在使用线程池的情况下。线程池中的线程是会被重用的,如果在使用完ThreadLocal后不清理数据,为了清理ThreadLocal中的数据,你应该在适当的时候调用ThreadLocal.remove()方法: 1. 请求处理完毕的时候; 2. 在对象被销毁前

  • JWT 令牌的定义及其作用, jwt的参数是什么样的,有什么意义

  • 定义:JWT(JSON Web Token)是一种用于身份验证和授权的开放标准。它由三部分组成,分别是头部(Header)、载荷(Payload)和签名(Signature)。其中,签名是用于验证令牌的完整性和可信任性。

  • JWT 自身包含了身份验证所需要的所有信息,因此,我们的服务器不需要存储 Session 信息。这显然增加了系统的可用性和伸缩性,大大减轻了服务端的压力。

  • 作用:JWT 令牌主要用于实现一种无状态的认证机制,定义了一种紧凑且自包含的方式,在各方之间安全的传输信息,主要用于用户首次登录成功以后,服务器会创建一个 JWT,将其发回给用户,随后用户的每次请求都会包含这个 JWT。JWT 使得服务器无需去存储用户的登录状态,从而实现无状态认证。

  • Header : 描述 JWT 的元数据,定义了生成签名的算法以及 Token 的类型。

  • Payload : 用来存放实际需要传递的数据(JWT 签发方\接收方\过期时间)

  • Signature(签名):服务器通过 Payload、Header 和一个密钥(Secret)使用 Header 里面指定的签名算法(默认是 HMAC SHA256)生成。

  • payload中的信息会被解密吗?密文密码能被存入负载中吗?为什么?

  • Payload 部分默认是不加密的,一定不要将隐私信息存放在 Payload 当中!!!
  • Signature 部分是对前两部分的签名,作用是防止 JWT(主要是 payload) 被篡改。
  • 密钥一定保管好,一定不要泄露出去。JWT 安全的核心在于签名,签名安全的核心在密钥

  • jwt验证流程

  • 在基于 JWT 进行身份验证的的应用程序中,服务器通过 Payload、Header 和 Secret(密钥)创建 JWT 并将 JWT 发送给客户端。客户端接收到 JWT 之后,会将其保存在浏览器的localStorage 里面,以后客户端发出的所有请求都会携带这个令牌。
  • 用户向服务器发送用户名、密码以及验证码用于登陆系统。
  • 如果用户用户名、密码以及验证码校验正确的话,服务端会返回已经签名的 Token,也就是 JWT
  • 客户端接收到 JWT 之后,会将其保存在浏览器的localStorage 里面,以后客户端发出的所有请求都会携带这个令牌
  • 服务端检查 JWT 并从中获取用户相关信息。

  • JWT优缺点以及localStorage

  • localStorage是Web存储的一种形式,它允许网站在用户的浏览器中存储数据。与sessionStorage相比,存储在localStorage中的数据没有时间限制,即数据在用户关闭浏览器窗口或标签页后依然可以保持。这使得localStorage成为存储跨会话数据的理想选择,例如用户的偏好设置或身份验证令牌(如JWT)。

浮点数精度

  • 浮点数精度丢失问题考虑过吗?如何解决精度丢失问题
  • 考虑过了,主要是在设计钱和订单定价的方面,要考虑浮点数的精度问题
  • 从java变量类型,到redis存储的数据机构,还有mysql存储的数据结构都要考虑
  • java里面采用BigDecimal来存储前相关的单价什么的,然后再redis里面用string来存储单价,在mysql里面用的是decimal类型
  • mysql的decimal类型可以指定数值的位数和其中小数的长度,DECIMAL(8, 2)
  • 注意BigDecimal用的是compareTo能忽略精度,equals是不能忽略两位小数和三位小数的差别的
  • 浮点数为什么会精度丢失,讲讲底层
  • 这个和计算机保存浮点数的机制有很大关系。我们知道计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。这也就是解释了为什么浮点数没有办法用二进制精确表示。就比如说十进制下的 0.2 就没办法精确转换成二进制小数.

nginx

  •   Nginx 负载均衡和正向、反向代理

  • 负载均衡: Nginx 的负载均衡是将访问请求分发到多个服务器上,以达到平衡服务器负载和提高系统可用性的目的。

  • 正向代理:(VPN) 正向代理是客户端发送请求后通过代理服务器访问目标服务器,代理服务器代表客户端发送请求并将响应返回给客户端。正向代理隐藏了客户端的真实身份和位置信息,为客户端提供代理访问互联网的功能。
  • 反向代理:(服务器端) 反向代理是指代理服务器接收客户端的请求,然后将请求转发给后端服务器,并将后端服务器的响应返回给客户端。反向代理隐藏了服务器的真实身份和位置信息,客户端只知道与反向代理进行通信,而不知道真正的服务器。

 - 为什么要反向代理

  • 前端请求地址:http://localhost/api/employee/login

  • 后端接口地址:http://localhost:8080/admin/employee/login

  • 提高访问速度

因为nginx本身可以进行缓存,如果访问的同一接口,并且做了数据缓存,nginx就直接可把数据返回,不需要真正地访问服务端,从而提高访问速度。

  • 进行负载均衡

所谓负载均衡,就是把大量的请求按照我们指定的方式均衡的分配给集群中的每台服务器。

  • 保证后端服务安全

因为一般后台服务地址不会暴露,所以使用浏览器不能直接访问,可以把nginx作为请求访问的入口,请求到达nginx后转发到具体的服务中,从而保证后端服务的安全。

 - 有自己部署过反向代理和负载均衡吗

  • 负载均衡从本质上来说也是基于反向代理来实现的,最终都是转发请求。
  • 可以输入监听的端口,然后设置proxy_pass, 设置代理服务器的地址,可以是主机名称,IP地址加端口号等形式。
  • 负载均衡可以用轮询还有,weight,fair(时间相应短的先)

消息队列

websocket

  • 后端如何与商家端建立链接,实现实时通信? 使用 Websocket 来实现用户端和商家端通信:WebSocket 是一种在 Web 应用程序中实现双向通信的协议。它允许客户端和服务器之间建立持久的、双向的通信通道,使得服务器可以主动向客户端推送消息,而无需客户端发送请求。客户端和服务器之间可以实时地发送消息和接收消息,不需要频繁地发起请求。这样可以减少网络流量和延迟,并提供更好的用户体验。

  • Websocket 与 HTTP 有什么区别? 既然 WebSocket 支持双向通信,功能看似比 HTTP 强大,那么是不是可以基于 WebSocket 开发所有的业务功能? HTTP 协议和 WebSocket 协议对比:

  • HTTP 是短连接

  • WebSocket 是长连接
  • HTTP 通信是单向的,基于请求响应模式
  • WebSocket 支持双向通信s
  • HTTP 和 WebSocket 底层都是 TCP 连接 不能使用 WebSocket 并不能完全取代 HTTP,它只适合在特定的场景下使用,原因如下:

  • 资源开销:WebSocket 需要保持持久连接,对服务器资源有更高要求,不适合所有场景。

  • 功能与约定:HTTP 提供丰富的功能和约定(如状态码、缓存控制),适合更广泛的业务需求。
  • 安全性和兼容性:虽然 WebSocket 支持加密,但管理安全性可能更复杂;且某些环境下 WebSocket 不被支持或有限制。
  • 设计和实践:RESTful API 和相关的 HTTP 设计原则不易直接应用于 WebSocket。

  • websocket常被用在视频实时弹幕、网页聊天、股票事实跟新

  • 我们项目里面用在订单的实时更新、来单提醒

商品超卖

  • 商品超卖问题如何解决?

  • 乐观锁非常乐观就是认为别的线程不会同时修改数据,所以不上锁,但是在更新的时候会判断一下期间有没有别的线程更新过这个数据

  • 乐观锁就是使用CAS,同一时间只处理一个,拒绝其他所有的请求,所以效率比较低

  • 每轮只能买一个

  • 乐观锁: 使用乐观锁(Optimistic Locking)进行并发控制。在购买商品时,先查询当前库存数量,然后在更新库存时进行版本号比对,如果版本号一致才执行更新操作,否则返回库存不足错误。这种方式适用于读多写少的场景。

    1. 悲观锁: 在商品购买过程中,使用悲观锁(Pessimistic Locking)对库存进行加锁,确保同一时间只有一个用户可以执行减库存操作,避免并发冲突导致的超卖问题。例如,在数据库层面使用数据库行级锁或者事务进行控制。
  • 分布式锁: 在分布式系统中,使用分布式锁对库存进行加锁,确保同一时间只有一个节点可以执行库存减少操作,避免多个节点之间的并发冲突。

  • 乐观锁是什么?乐观锁是 SQL 实现还是逻辑实现?

  • 逻辑实现就是CAS实现,用java中自带的atomic包中的原子变量就能实现乐观锁

  • 一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数。当数据被修改时,version 值会 +1。当线程 A 要更新数据时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值与当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功
  • update user set name ="zs",version = oldVersion +1 whereversion = oldVersion

  • 乐观锁是一种并发控制机制,通过假设并发冲突很少发生(即乐观的态度),不使用阻塞操作,而是在更新操作之前先进行检查。如果检查发现其他事务已经对数据进行了修改,那么当前事务就会放弃或者重新尝试。

  • 乐观锁通常通过版本号(Version)或时间戳(Timestamp)来实现。在每个数据记录中添加一个版本号或时间戳字段,当数据被读取时,将版本号或时间戳一并读取并记录在内存中。当数据更新时,检查内存中的版本号或时间戳与数据库中的是否一致,如果一致则执行更新操作,否则表示其他事务已经对数据进行了修改,需要根据具体情况进行回滚或重新尝试。
  • 乐观锁是逻辑实现,即由应用程序通过编程实现在业务逻辑层面进行控制,而不是由数据库系统提供的特定的锁机制。乐观锁适用于读多写少的场景,并且能够减少锁的竞争,提高系统的并发性能。

spring

  • 什么是 AOP?
  • 概念: AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,旨在通过将横切关注点(cross-cutting concerns)从核心业务逻辑中分离出来,以提高代码的模块化性、可维护性和复用性。

    • 横切关注点: 比如日志、事务、安全性等,这些关注点会横跨多个模块,导致代码重复、耦合性增加、难以维护等问题。AOP 通过将这些横切关注点抽象成一个个“切面”(Aspect),并将其独立于业务逻辑之外,以达到解耦的目的。
  • AOP 的核心概念包括以下几个要素:

    1. 切面(Aspect): 切面是横切关注点的抽象,它包含了一组横切关注点以及在何时何处应用这些关注点的逻辑。通常,切面由一组通知(Advice)和一个切点(Pointcut)组成。
    2. 通知(Advice): 通知是切面中具体的逻辑实现,它定义了在何时何地执行横切关注点的具体行为,包括“前置通知”(Before Advice)、“后置通知”(After Advice)、“环绕通知”(Around Advice)等。
    3. 切点(Pointcut): 切点是在程序中指定的某个位置,通知将在这些位置执行。切点可以使用表达式或其他方式进行定义,以便匹配到程序中的特定方法或代码块。
    4. 连接点(Join Point): 连接点是在程序执行过程中可以应用通知的具体位置,通常是方法调用、方法执行或异常抛出等。
    5. 织入(Weaving): 织入是将切面逻辑应用到目标对象中的过程,可以在编译时、加载时或运行时进行。织入可以通过源代码修改、字节码操作、动态代理等方式实现。
  •  你项目中aop的例子 

  • 事务管理:@Transactional 注解可以让 Spring 为我们进行事务管理比如回滚异常操作,免去了重复的事务管理逻辑。@Transactional注解就是基于 AOP 实现的。
  • 线商店的应用程序,其中有一个功能是“下订单”。在这个过程中,主要的业务逻辑是接收用户的订单信息,然后在数据库中创建一个订单。但除了这个主要逻辑之外,还有很多其他的操作需要在这个过程中执行,例如:安全检查、日志记录、性能监控、事务管理
  • 如果我们将所有这些功能都写在下订单的业务逻辑中,那么代码将会非常复杂和难以维护。这些额外的操作(安全检查、日志记录、性能监控、事务管理)虽然与主业务逻辑(下订单)紧密相关,但它们实际上是横切关注点
  • 切面(Aspect):日志记录本身就是一个切面。这是因为日志记录是一个跨越应用程序多个部分的关注点。在下订单的流程中,我们可能在方法开始时记录一条日志,表明方法被调用,以及在方法结束时记录一条日志,表明方法执行完成,这个过程中如果有异常发生也会记录相关日志。
  • 通知(Advice):在日志切面中,实际执行日志记录操作的代码块就是通知。这可以是“前置通知”(在方法执行前运行)、“后置通知”(在方法执行后运行,无论是正常返回还是异常退出)、“环绕通知”(在方法执行前后都运行)等。
  • 切点(Pointcuts):切点定义了通知应该在哪些连接点上执行。在下订单的场景中,切点可以指定为“任何订单服务类(OrderService)中的方法调用”。
  • 连接点(Join points):在下订单的流程中,每一个方法的调用都是一个连接点,因为它们是可以被AOP框架插入额外行为的点。例如,createOrder方法的调用就是一个连接点。
  • 织入(Weaving):这是AOP框架将切面逻辑(如日志记录)应用到目标对象(如订单服务)的过程。这通常在运行时发生,但也可以在编译时或加载时完成

  • 面向AOP编程的意义

  • 以日志记录为例进行介绍,假如我们需要对某些方法进行统一格式的日志记录,没有使用 AOP 技术之前,我们需要挨个写日志记录的逻辑代码,全是重复的的逻辑。
  • 使用 AOP 技术之后,我们可以将日志记录的逻辑封装成一个切面,然后通过切入点和通知来指定在哪些方法需要执行日志记录的操作。

  • 什么是 IOC?

  • IoC(Inverse of Control:控制反转)是一种设计思想或者说是某种模式。这个设计思想就是 将原本在程序中手动创建对象的控制权交给第三方比如 IoC 容器。 不需要我们手动去new一个类了, 对于我们常用的 Spring 框架来说, IoC 容器实际上就是个 Map(key,value),Map 中存放的是各种对象。
  • IoC 最常见以及最合理的实现方式叫做依赖注入(Dependency Injection,简称 DI)。

  • 什么是反射? 反射是一种在程序运行时检查和操作类的机制,通过获取类的信息并动态调用方法、创建对象等。这种机制让程序能够在运行时根据需要动态地获取和操作类的结构和成员。

  • 获取 Class 对象: 程序通过类的全限定名、对象的 getClass ()方法或. Class 语法来获取对应的 Class 对象。

  • 查询类信息: 通过 Class 对象可以获取类的信息,包括类名、包名、父类、实现的接口、构造函数、方法、字段等。
  • 动态创建对象: 通过 Class 对象的 newInstance ()方法调用类的默认构造函数来创建对象,或者通过 Constructor 对象调用类的其他构造函数来创建对象。
  • 动态调用方法: 通过 Method 对象调用类的方法,传递参数并获取返回值。
  • 动态访问字段: 通过 Field 对象获取和设置类的字段值。 整个流程就是通过获取 Class 对象,然后根据需要动态地调用类的方法、创建对象、访问字段等操作,实现了对类的动态操作和调用。

mysql

  • 怎么保证在同时操作多张数据库表出现程序错误时保证数据的一致性? 我在涉及多表操作时使用了事务(Transaction): 将涉及到的数据库操作封装在一个事务中。在事务中,要么所有的数据库操作都成功提交,要么全部失败回滚,保证了数据的一致性。如果发生异常,可以通过捕获异常并执行回滚操作来保证数据的一致性。 具体操作:
  • 在启动类上方添加@EnableTransactionManagement
  • 开启事务注解之后,我们只需要在需要捆绑成为一个事务的方法上添加@Transactional
  • 这样就把对两张表的操作捆绑成为了一个事务。

  • 索引结构?什么时候创建索引?什么字段不适合使用索引?

  • 索引(index)是帮助MySQL高效获取数据的数据结构(有序)。提高数据检索的效率,降低数据库的IO成本(不需要全表扫描)。通过索引列对数据进行排序,降低数据排序的成本,降低了CPU的消耗

  • MySQL的默认的存储引擎InnoDB采用的B+树的数据结构来存储索引,选择B+树的主要的原因是:第一每个节点有多个分叉,阶数更多,路径更短,第二个磁盘读写代价B+树更低,非叶子节点只存储指针,叶子阶段存储数据,第三是B+树便于扫库和区间查询,叶子节点是一个双向链表

  • 什么时候创建索引:1. 高频查询字段:经常用于查询条件的字段,如用户ID、订单号;2. 排序和分组字段:在ORDER BYGROUP BY操作中频繁使用的字段也应考虑建立索引;3. 外键字段:数据库外键通常也是建立索引的好选择,因为它们经常用于连接查询;4. 唯一性验证:对于需要保证唯一性的字段(例如,用户邮箱或手机号码),创建唯一索引不仅可以提高查询效率,还可以防止数据重复。5. 范围查询:对于经常进行范围查询的字段(如日期区间查询),索引可以显著提高查询性能。

  • 什么字段不适合创建索引:1. 变动频繁的字段:如果一个字段的值经常变化,那么每次变化都需要更新索引,这可能导致性能下降;2. 低基数字段:所谓低基数,是指字段中不同值的比例非常低的情况(如性别字段)。这类字段即使建立索引,也不会显著提高查询性能,反而会增加存储空间和维护成本。3. 大型文本字段:大型文本字段(如用户评论)建立索引不实际,一方面因为它们的数据量很大,另一方面因为全文搜索通常需要专门的搜索引擎(如Elasticsearch)来优化。

  • 聚簇索引主要是指数据与索引放到一块,B+树的叶子节点保存了整行数据,有且只有一个,一般情况下主键在作为聚簇索引的

非聚簇索引值的是数据与索引分开存储,B+树的叶子节点保存对应的主键,可以有多个,一般我们自己定义的索引都是非聚簇索引

  • 事务 ACID了解吗?

  • 事务是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。

  • 原子性(Atomicity):事务是不可分割的最小操作单元,要么全部成功,要么全部失败。

  • 一致性(Consistency):事务完成时,必须使所有的数据都保持一致状态。

  • 隔离性(Isolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行。

  • 持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。要持久化,就是保存到磁盘之中

  • 并发事务带来哪些问题?

  • 脏读、不可重复读、幻读
  • 第一是脏读, 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
  • 第二是不可重复读:比如在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
  • 第三是幻读(Phantom read):幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。

  • 数据库的隔离级别有哪些?解决什么问题?

  • 第一个是,未提交读(read uncommitted)它解决不了刚才提出的所有问题,一般项目中也不用这个。
  • 第二个是读已提交(read committed)它能解决脏读的问题的,但是解决不了不可重复读和幻读。
  • 第三个是可重复读(repeatable read)它能解决脏读和不可重复读,但是解决不了幻读,这个也是mysql默认的隔离级别。
  • 第四个是串行化(serializable)它可以解决刚才提出来的所有问题,但是由于让是事务串行执行的,性能比较低。
  • 一般使用的都是mysql默认的隔离级别:可重复读

  • 并发环境下,如何避免不同事务插入重复数据?事务中的隔离性是如何保证的呢?MVCC了解吗

  • 好的,其中redo log日志记录的是数据页的物理变化,服务宕机可用来同步数据,而undo log 不同,它主要记录的是逻辑日志,当事务回滚时,通过逆操作恢复原来的数据,比如我们删除一条数据的时候,就会在undo log日志文件中新增一条delete语句,如果发生回滚就执行逆操作;
  • redo log保证了事务的持久性,undo log保证了事务的原子性和一致性
  • 事务的隔离性是由锁和mvcc实现的。
  • 其中mvcc的意思是多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突,它的底层实现主要是分为了三个部分,第一个是隐藏字段,第二个是undo log日志,第三个是readView读视图
  • 隐藏字段是指:在mysql中给每个表都设置了隐藏字段,有一个是trx_id(事务id),记录每一次操作的事务id,是自增的;另一个字段是roll_pointer(回滚指针),指向上一个版本的事务版本记录地址
  • undo log主要的作用是记录回滚日志,存储老版本数据,在内部会形成一个版本链,在多个事务并行操作某一行记录,记录不同事务修改数据的版本,通过roll_pointer指针形成一个链表
  • readView解决的是一个事务查询选择版本的问题,在内部定义了一些匹配规则和当前的一些事务id判断该访问那个版本的数据,不同的隔离级别快照读是不一样的,最终的访问的结果不一样。如果是rc隔离级别,每一次执行快照读时生成ReadView,如果是rr隔离级别仅在事务中第一次执行快照读时生成ReadView,后续复用

  • 分库分表

  • 订单表述可以用到垂直分表,订单太多可以水平分表,查找的时候根据id节点取模
  • 员工表、产品分类表、具体的产品表、产品特点表、套餐表格、用户表、地址表、订单表、订单详情表
  • 价格使用decimal 名称使用varchar id用的是bigint
  • 垂直分库:根据业务不同将不同的表拆分到不同的库中
  • 垂直分表:将不常用的字段或者大字段拆分进附表
  • 水平分库:将一个库的数据拆分到多个库中
  • 水平分表:将一个表的数据拆分到多个表中

线程池

  • 参数介绍?
  • corePoolSize 核心线程数目
  • maximumPoolSize 最大线程数目 = (核心线程+救急线程的最大数目)
  • keepAliveTime 生存时间 - 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放
  • workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务
  • threadFactory 线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等
  • handler 拒绝策略 - 当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略

  • 拒绝策略?

  • AbortPolicy(默认策略)直接抛出RejectedExecutionException异常。

  • CallerRunsPolicy不会抛出异常,而是将任务回退给调用者线程自己来执行。

  • DiscardPolicy默默地丢弃无法处理的任务,不给予任何处理也不抛出异常。

  • DiscardOldestPolicy丢弃队列中最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。

    • - Swagger 作用
    • 定义:Swagger 是一个用于设计、构建和文档化 RESTful Web 服务的工具集。
    • 作用:用来在后端生成接口文档,辅助前端测试。使用 Swagger 只需要按照它的规范定义接口以及接口的相关信息,就可以做到生成接口文档,以及在线接口调试页面。
    • Knife 4 j 是 Swagger 的一个增强工具,是基于 Swagger 构建的一款功能强大的文档工具。它提供了一系列注解,用于增强对 API 文档的描述和可视化展示。如在项目中常用的 Knife 4 j 注解介绍:
      • @Api :用于对 Controller 类进行说明和描述,可以指定 Controller 的名称、描述、标签等信息。
    • @ApiOperation:用于对 Controller 中的方法进行说明和描述,可以指定方法的名称、描述、请求方法(GET、POST 等)等信息。
  • Pagehelper 如何实现分页查询? 通过一个 SetLocalPage 方法调用 ThreadLocal 将 Page 对象封装到存储到当前的线程存储空间中,在后面开始分页查询时,Mapper 中的 SQL 语句自动接上 limit 开始页,显示的数据数量。

  • 设置分页参数:在执行查询之前,首先通过 PageHelper.startPage(int pageNum, int pageSize) 方法设置分页的参数,调用该方法时,通过 ThreadLocal 存储分页信息。
  • 拦截查询语句PageHelper 利用 MyBatis 提供的插件 API(Interceptor 接口)来拦截原始的查询语句。MyBatis 执行任何 SQL 语句前,都会先通过其插件体系中的拦截器链,PageHelper 正是在这个环节介入的。
  • 修改原始 SQL 语句:在拦截原始查询语句后,PageHelper 会根据分页参数动态地重写或添加 SQL 语句,使其成为一个分页查询。
  • 执行分页查询:修改后的 SQL 语句被执行,返回当前页的数据。
  • 查询总记录数(可选):如果需要获取总记录数,PageHelper 会自动执行一个派生的查询,以计算原始查询(不包含分页参数)的总记录数。这通常通过移除原始 SQL 的排序(ORDER BY)和分页(LIMITOFFSET 等)条件,加上 COUNT(*) 的包装来实现。
  • 返回分页信息:查询结果被封装在 PageInfo 对象中(或其他形式的分页结果对象),这个对象除了包含当前页的数据列表外,还提供了总记录数、总页数、当前页码等分页相关的信息,方便在应用程序中使用。

  • Stream 流

  • 流是 java 8 引入的一个关键抽象概念,允许以声明的方式处理数据集合。
  • 流操作分为中间操作和终端操作:

    • 中间操作返回流本身,可以链接多个操作。
      1. filter:过滤流中的元素,根据给定的谓词保留那些符合条件的元素。
      2. map:将流中的每个元素映射成另一种形式。
      3. flatMap:类似于 map,但是每个输入元素可以被转换成零个、一个或多个输出元素(比如,把流中的每个列表转换成流中的元素)。
      4. distinct:返回一个去除重复元素的流。
      5. sorted:产生一个新流,其中按自然顺序或提供的 Comparator 排序。
      6. peek:生成另一个流,其中的每个元素在被消费时都会执行给定的操作(常用于调试)。
      7. limit:截取流使其最大长度不超过给定的数量。
      8. skip:返回一个丢弃了前 n 个元素的流。如果流中元素不足 n 个,则返回一个空流。
      9. 该中间操作为 map 接受一个函数作为参数,这个函数应用于流中的每一个元素并将其映射为一个新的元素。
    • 终端操作返回一个结果或副作用
      1. forEach:对流中的每个元素执行给定的操作,通常用于产生副作用(如输出)。
      2. collect:已讨论,通过给定的收集器,将流元素累积成一个汇总结果(如列表、集、字符串)。
      3. reduce:将流中的元素组合起来,使用一个初始值和一个二元累积函数,返回一个 Optional 结果。
      4. count:返回流中的元素数量。
      5. anyMatch:如果流中至少有一个元素匹配给定的谓词,则返回 true
      6. allMatch:如果流中每个元素都匹配给定的谓词,则返回 true
      7. noneMatch:如果流中没有元素匹配给定的谓词,则返回 true
      8. findFirst:返回流的第一个元素,如果流为空则返回一个空的 Optional
      9. findAny:返回流中的任意元素,如果流为空则返回一个空的 Optional。 ![[Pasted image 20240314163615.png]]
  • 讲讲什么是 HttpclientHttpclient是一个服务器端进行 HTTP 通信的库,他使得后端可以发送各种 HTTP 请求和接收 HTTP 响应,使用 HTTPClient,可以轻松的发送 GET, POST, PUT, DELETE 等各种类型的的请求。  在我们的项目中,在进行微信登录开发时,后端在使用登录凭证校验接口的时候就需要发送指定请求到给定的 URL 中。因此我们使用 Httpclient 去完成该任务。

  • spring task 处理定时任务 Spring Task(Spring 任务调度)是 Spring 框架提供的一种任务调度框架,用于执行定时任务、异步任务、任务监听、任务调度等。 在项目中使用 Spring task 用来执行定时任务查看有没有已经派送结束的订单但状态没有更改为已完成,在第二天的固定时间统一调用该定时任务去更改需要调整的订单状态。

备用

#### 用唯一索引能不能保证一人一单,和你的分布式锁比起来怎么用?

在处理“一人一单”这类限制购买数量的业务场景时,我们常常需要考虑数据的一致性和系统的并发处理能力。唯一索引和分布式锁是两种常见的技术方案,各有其优缺点和适用场景。

### 唯一索引

唯一索引是数据库层面的约束,用来保证某列(或列的组合)的数据的唯一性。在“一人一单”的场景中,可以通过为用户ID和商品ID组合设置唯一索引来实现。

**优点**:

- **简单高效**: 直接由数据库管理,不需要额外的代码逻辑。
- **强制约束**: 保证了数据的一致性,任何违反唯一约束的插入操作都会被数据库拒绝。

**缺点**:

- **灵活性差**: 一旦规则发生变化(如活动规则从“一人一单”改为“一人两单”),可能需要调整数据库结构。
- **异常处理**: 当违反唯一约束时,数据库会抛出异常,需要在应用层面进行捕获和处理,可能导致用户体验不佳。

### 分布式锁

分布式锁是一种跨多个进程或服务器的同步机制,用于保证在分布式系统中某个时刻只有一个操作能访问特定资源。

**优点**:

- **高度灵活**: 可以在代码中根据业务需求灵活控制锁的粒度和持有时间,适应各种复杂场景。
- **减少数据库负担**: 通过应用层面的控制减少对数据库的压力,尤其是在高并发场景下。

**缺点**:

- **实现复杂**: 需要选择合适的分布式锁实现(如Redis、Zookeeper等),并正确处理锁的获取、续期和释放。
- **性能考量**: 锁的持有和释放需要网络通信,可能会增加系统的响应时间。

### 唯一索引与分布式锁的对比使用

- **唯一索引**适合数据一致性要求高、规则相对固定的场景。由于其操作简单、性能较好,是实现“一人一单”非常直接的方法。
- **分布式锁**适合业务规则复杂、需要灵活控制并发操作的场景。尤其在需要控制粒度细、业务流程长的操作中,分布式锁提供了更多的控制能力。

在实际应用中,选择哪种方案取决于具体的业务需求、系统架构和性能要求。有时,为了达到最优的效果,甚至会结合使用唯一索引和分布式锁,比如使用唯一索引保证数据的一致性,同时使用分布式锁来控制复杂的业务流程,以此来平衡性能和一致性的要求。
#### 为什么要用分布式锁? 分布式锁是在事务开启前加还是事务开始后

分布式锁主要用于在分布式系统中保持数据一致性和防止资源冲突,特别是在多个进程或服务需要访问共享资源时。在分布式环境下,由于系统分散在不同的服务器或进程中,无法直接使用传统的单体系统中的锁机制(如数据库锁或内存锁)来同步操作。因此,引入分布式锁成为了保持高并发环境下数据一致性和系统稳定性的关键技术。

### 为什么要用分布式锁?

1. **数据一致性**: 在并发环境下,多个进程或服务可能同时操作同一数据,如果没有适当的同步机制,就可能导致数据不一致。
2. **防止资源冲突**: 分布式锁可以确保同一时刻只有一个进程或服务操作某个资源,避免了操作冲突。
3. **顺序控制**: 在需要按照特定顺序执行操作的场景中,分布式锁可以确保操作的有序性。
4. **业务逻辑保护**: 在复杂的业务逻辑中,通过分布式锁可以保证某些操作不会因为并发执行而被打断,保障业务逻辑的完整性。

### 分布式锁是在事务开启前加还是事务开始后加?

分布式锁的使用时机取决于具体的业务场景和保护资源的需求。然而,有几个考虑因素通常会影响这一决策:

- **锁定资源前加锁**:如果目标是保护多个操作作为一个整体执行(即事务),那么应该在开启事务之前就获取分布式锁。这样可以确保在事务开始执行前,没有其他进程或服务能够操作这些资源。这种方式适用于需要在事务开始之前就确保资源独占的场景。
- **锁定非事务性资源**:在某些场景下,需要在事务外部对一些非事务性资源(如缓存更新、文件系统操作等)进行控制,这时候通常会在操作这些资源之前获取分布式锁。
- **性能考虑**:在事务开始之前加锁可能会增加锁持有的时间,从而影响系统的吞吐量和响应时间。因此,需要在锁的粒度和持有时间与业务需求之间做出平衡。

综上,没有一个统一的规则适用于所有场景,关键是根据实际业务需求、资源类型和系统架构来决定最佳的加锁时机。在设计系统时,应仔细考虑分布式锁的使用,以避免死锁、降低性能和增加系统复杂度等问题。
  • 你用什么技术实现数据导出的功能的?

  • 在本项目中使用 Apache POI 技术完成数据的导出能,提供一张创建好的模板表,然后使用 ApachePOI 来实现填充数据,最后导出数据。

  • Apache POI(Poor Obfuscation Implementation)是一个用于处理 Microsoft Office 格式文档的开源 Java 库。POI 提供了一组可以读取、写入和操作各种 Office 文件的 API,包括 Word 文档(. Doc 和. Docx)、Excel 电子表格(. Xls 和. Xlsx)以及 PowerPoint 演示文稿(. Ppt 和. Pptx)。

通过 POI,开发者可以在 Java 应用程序中读取和编辑 Office 文档,实现对文档内容、样式、格式和元数据的操作。它提供了向现有文档添加新内容、修改现有内容、删除内容以及进行格式设置和样式调整等功能。

  • SpringCache

  • SpringCache 是 Spring 框架提供的一个抽象层,旨在提供一种透明的方式来缓存应用中的数据。SpringCache 不是一个具体的缓存实现,而是一个集成不同缓存解决方案的接口,如 EHCache、Caffeine、Guava、Redis 等。它允许开发者通过简单的注解来控制方法的缓存行为,例如,使用 @Cacheable 来标记一个方法的返回值应该被缓存,以及使用 @CacheEvict 来标记何时移除缓存。SpringCache 为应用提供了一致的缓存视图,而开发者不需要关心具体使用哪种缓存技术。

  • 简单的说:它也是一种缓存技术,使得所用工具不局限于 Redis。相比较于使用 Redis 的时候需要把相关代码内嵌到方法体种,Spring Cache 是一种基于注解方式来达到内嵌代码相同的效果。