当前位置: 首页 > news >正文

今日面试之项目拷打:锁与事务的深度解析

前言:

今天在做云图库项目的空间模块,简单总结一下今天学到的一些新东西

项目代码背景:

@Overridepublic long addSpace(SpaceAddRequest spaceAddRequest, User loginUser) {//1.填充默认参数值//转换实体类和DTOSpace space=new Space();BeanUtils.copyProperties(spaceAddRequest,space);//填充容量和大小Integer spaceLevel = space.getSpaceLevel();if(spaceLevel==null){space.setSpaceLevel(spaceAddRequest.getSpaceLevel());}if(space.getMaxSize()==null){space.setMaxSize(SpaceLevelEnum.getEnumByValue(spaceLevel).getMaxSize());}if(space.getMaxCount()==null){space.setMaxCount(SpaceLevelEnum.getEnumByValue(spaceLevel).getMaxCount());}//2.校验参数validSpace(space, true);//3.校验权限,非管理员只能创建普通级别的空间Long userId = loginUser.getId();space.setUserId(userId);if (SpaceLevelEnum.COMMON.getValue() != spaceAddRequest.getSpaceLevel() && !userService.isAdmin(loginUser)) {throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "无权限创建指定级别的空间");}//4.控制同一用户只能创建一个私有空间String lock = String.valueOf(userId).intern();synchronized (lock){Long newSpaceId = transactionTemplate.execute(status -> {//判断是否已有空间boolean exists = this.lambdaQuery().eq(Space::getUserId, userId).exists();//如果已有则不能创建if (exists) {throw new BusinessException(ErrorCode.OPERATION_ERROR, "每个用户仅能创建一个私有空间");}//创建boolean save = this.save(space);ThrowUtils.throwIf(!save, ErrorCode.OPERATION_ERROR, "创建私有空间失败");//返回新写入的数据idreturn space.getId();});return Optional.ofNullable(newSpaceId).orElse(-1L);}}

问题一:

为什么这里使用到transactionTemplate而不是直接在方法上增加@Transactional ?

答:①(业务逻辑角度)从时间线上来看,假设同一个用户在一个时间切片内连续发出两次请求,此时第一次请求获取到锁并完成了数据库的新增然后释放锁,因为用的是事务注解在方法上,所以是在方法完成后才会提交事务,但当方法还没有返回结果时,第二次请求迅速拿到锁开始业务逻辑处理,由于上一次事务没有提交,就导致了第二次请求再一次在数据库中新增了一条记录(这就违反了业务逻辑)。

时间 →
┌───────────────────────────────┬───────────────────────────────┐
│           线程 A(请求 1)     │          线程 B(请求 2)      │
├───────────────────────────────┼───────────────────────────────┤
│ 开启事务 T1                   │                                │
│ 获取锁                        │                               │
│ 校验:库中无记录               │                               │
│ 插入记录(未提交,仍在事务中) │                               │
│ 释放锁                        │                               │
│                               │ 获取锁成功                    │
│                               │ 校验:仍查不到记录(T1未提交)│
│                               │ 插入记录(未提交,T2中)       │
│                               │ 释放锁                        │
│ 提交事务 T1(记录1真正落库)   │                               │
│                               │ 提交事务 T2(记录2真正落库)   │
└───────────────────────────────┴───────────────────────────────┘最终结果:同一用户有两条记录 ❌

②(并发安全角度)

如果整个方法都加上了 @Transactional

  1. 事务会在方法一开始就开启,然后才进入 synchronized 块。

  2. 多个线程同时进来时,虽然 synchronized 还能保证同一个 userId 串行化,但此时数据库连接和事务资源已经被占用。

    • 如果线程很多,就可能撑爆连接池。

    • 等锁释放时,有些事务可能已经超时。

  3. 事务范围过大 → 把校验逻辑、加锁等待也算进事务里,这些其实不需要事务,却占用了事务资源。

transactionTemplate 的写法,只有真正需要保证原子性的 查询+保存 才跑在事务里,外层的锁竞争和参数校验都不会拖慢数据库事务。

问题二:

为什么String lock = String.valueOf(userId).intern();要用userId作为锁对象?

答:

  • 每个用户对应一个唯一的 userId

  • 通过 String.valueOf(userId)userId 转成字符串,再用 .intern() 保证:

    • JVM 内部 相同内容的字符串只会有一份引用(后面有详细解释)

    • 所以 synchronized(lock) 针对的是同一个用户唯一的锁对象。


String lock1 = String.valueOf(1001).intern(); 
String lock2 = String.valueOf(1001).intern(); 
System.out.println(lock1 == lock2); // true
  • 两个线程拿到的是 同一个锁对象 → 线程安全。

  • 不同用户的 userId → 不同锁对象 → 不互相阻塞,提高并发。

问题三:

为什么JVM 内部 相同内容的字符串只会有一份引用?

答:

这是一个 Java 字符串常量池(String Pool) 的机制问题。我们慢慢拆开解释。


1. 字符串常量池的概念

  • 在 Java 中,字符串是不可变对象String 是 immutable 的)。

  • 为了节省内存和提高效率,JVM 在 方法区/元空间 中维护一个 字符串常量池(String Pool)。

  • 任何 编译时的字面量字符串 或者通过 intern() 方法的字符串,都会放入常量池中。


2. 为什么相同内容的字符串只有一份引用

  • 当你创建一个字符串时:

    String s1 = "hello"; 
    String s2 = "hello";
    • JVM 会先去 常量池 查找是否有 "hello"

    • 如果存在,就直接返回常量池中的引用。

    • 如果不存在,才会新建一个对象放入池中。

  • 因此:

    System.out.println(s1 == s2); // true

    两个变量 引用的是同一个对象,而不是两个不同的对象。


3. intern() 的作用

  • 当你有一个 运行时生成的字符串

    String s3 = new String("hello");
    • 这是在堆上创建了一个新对象,引用和常量池不同:

      System.out.println(s1 == s3); // false
  • 使用 intern()

    String s4 = s3.intern();
    • JVM 会检查常量池中是否有 "hello"

      • 如果有 → 返回常量池的引用。

      • 如果没有 → 将 s3 的内容放入池中,并返回引用。

  • 这样就保证了 相同内容的字符串在常量池中只有一份引用

写法对象位置引用关系
String s1 = "hello";常量池s1 指向池中对象
String s2 = "hello";常量池s2 指向同一个池中对象
String s3 = new String("hello");堆(Heap)s3 指向新对象,与池中不同
s3.intern()常量池返回池中对象的引用

为了给大家彻底理清这个问题,我最后补充一个问题:

问题四:

项目中第一次执行String s3 = new String("hello");会发生什么?

答:

1. 执行 String s3 = new String("hello");

  • "hello"字面量,会先被 放入常量池(如果还没放的话)。

    • JVM 在类加载或首次使用时,会在常量池中创建 "hello" 的唯一引用。

  • new String("hello") 会在 堆上新建一个 String 对象(即 s3 指向的对象),内容是 "hello"

  • 此时堆对象和常量池对象是两个不同的对象

常量池: "hello"   ← JVM唯一
堆对象: s3 -> "hello"(新对象)

此时若是执行s3.intern()

  • intern() 会去 常量池查找是否存在相同内容的字符串

    • 如果存在 → 返回 常量池中的引用

    • 如果不存在 → 把 s3 的引用放入常量池,并返回该引用

  • 在你这个例子中,常量池里已经有 "hello"(第一次使用 "hello" 字面量时就放进去的),所以:

    String s4 = s3.intern();
    s4 == s1 // true,指向常量池对象
    s3 == s4 // false,堆对象和池对象不同
    

http://www.dtcms.com/a/415101.html

相关文章:

  • 基于IMX6ULL 芯片 UART1
  • Coze源码分析-资源库-删除数据库-后端源码-基础设施/数据存储层
  • 多后端服务器架构解析
  • 四种常用SVC(service)及其与Ingress协作方式
  • C#多线程全家桶:从Thread到async/await
  • 网站备案 论坛甘肃微信网站建设
  • 数模之路获奖总结——数据分析交流(R语言)
  • 网站 后台 数据 下载网站优化软件推荐
  • Java数据结构第二十七期:布隆过滤器,用 “模糊” 换高效的查重黑科技
  • 怎么做质量高的网站如何实现网站建设服务
  • 我的项目开发的一般流程,供交流
  • 做网站实名认证有什么用濮阳市城乡一体化示范区主任
  • InnoDB压缩技术:节省空间提升性能
  • 国任保险携手云轴科技ZStack获评鼎新杯数字化转型应用典型案例
  • 进入网站wordpress配置如何在百度搜到自己的网站
  • 建设银行网站怎么看不见余额专业类网站
  • qq登录网站授权怎么做外贸自建站平台哪个好
  • 红豆杉发展前景与培育技术(英文翻译稿)
  • 虾皮后端一面
  • 网站的新闻模块怎么做公司网络推广营销
  • 自己做刷东西的网站百度地图电脑版网页
  • 【开题答辩全过程】以 spb+疾病风险预警平台的设计与实现为例,包含答辩的问题和答案
  • 北京做百度网站有免费建网站
  • 国外的电商网站这么攻击网站
  • 新手卖家做来赞达网站如何如何用wordpress站群
  • string(1),咕咕咕
  • 网络安全基础--第六课:MySQL基础
  • 技术博客SEO优化终极指南
  • 怎么做淘宝联盟网站推广百度域名怎么注册
  • ftp 网站酒店网站策划书