[{"data":1,"prerenderedAt":2964},["ShallowReactive",2],{"post:\u002Fposts\u002Fjavaspring\u002Fspring-notes-10":3,"posts:ordered":2687},{"id":4,"title":5,"body":6,"category":2671,"cover":2672,"date":2673,"description":2674,"extension":2675,"featured":2676,"meta":2677,"navigation":204,"path":2678,"seo":2679,"series":2680,"seriesOrder":335,"stem":2681,"tags":2682,"__hash__":2686},"posts\u002Fposts\u002Fjavaspring\u002Fspring-notes-10.md","Spring 学习笔记 10：Service 层与事务",{"type":7,"value":8,"toc":2653},"minimark",[9,16,21,24,42,50,54,57,109,124,128,134,163,170,263,876,889,893,900,918,921,976,979,1066,1070,1077,1189,1192,1214,1218,1225,1308,1315,1349,1362,1366,1369,1422,1429,1461,1475,1495,1499,1554,1579,1583,1586,1591,1687,1697,1746,1757,1761,1815,1834,1844,1847,1915,1919,1925,2319,2336,2342,2346,2384,2391,2395,2576,2580,2642,2649],[10,11,12],"blockquote",{},[13,14,15],"p",{},"这是一份个人学习笔记：概念部分是我按自己的理解写的，示例代码都是自己敲的，参考的是\nSpring 官方文档与 Jakarta Persistence 规范。理解有偏差的地方欢迎指出。",[17,18,20],"h2",{"id":19},"_0-借一本书涉及三次写库","0 借一本书，涉及三次写库",[13,22,23],{},"「借书」这个操作，业务上是一件事，落到数据库上却是三步：",[25,26,27,36,39],"ol",{},[28,29,30,31,35],"li",{},"插入一条借阅记录（",[32,33,34],"code",{},"loan","）；",[28,37,38],{},"把这本书的状态改成「已借出」；",[28,40,41],{},"把这位会员的借阅次数加一。",[13,43,44,45,49],{},"如果第 2 步成功、第 3 步失败，数据库里就留下了一条",[46,47,48],"strong",{},"自相矛盾的数据","：书显示已借出，但借阅\n记录没建立，或者次数没算上。这类「必须一起成功」的操作，就是事务要解决的问题。",[17,51,53],{"id":52},"_1-事务的基本语义","1 事务的基本语义",[13,55,56],{},"事务（Transaction）把若干数据库操作合成一个原子单元，具备四个性质（ACID）：",[58,59,60,73],"table",{},[61,62,63],"thead",{},[64,65,66,70],"tr",{},[67,68,69],"th",{},"性质",[67,71,72],{},"含义",[74,75,76,85,93,101],"tbody",{},[64,77,78,82],{},[79,80,81],"td",{},"原子性 Atomicity",[79,83,84],{},"要么全部生效，要么全部不生效",[64,86,87,90],{},[79,88,89],{},"一致性 Consistency",[79,91,92],{},"事务前后数据都满足业务约束",[64,94,95,98],{},[79,96,97],{},"隔离性 Isolation",[79,99,100],{},"并发事务之间互相看不到中间状态（具体程度由隔离级别决定）",[64,102,103,106],{},[79,104,105],{},"持久性 Durability",[79,107,108],{},"提交后的结果不会因为故障丢失",[13,110,111,112,115,116,119,120,123],{},"在 SQL 层面的表现就是：",[32,113,114],{},"BEGIN"," 开启，做若干读写，",[32,117,118],{},"COMMIT"," 提交或 ",[32,121,122],{},"ROLLBACK"," 回滚。",[17,125,127],{"id":126},"_2-事务该写在哪一层","2 事务该写在哪一层",[13,129,130,133],{},[46,131,132],{},"写在 Service 层","，理由有三条：",[135,136,137,143,153],"ul",{},[28,138,139,142],{},[46,140,141],{},"业务边界在 Service","。「借书」这件事由 Service 定义，事务边界自然与它对齐；",[28,144,145,148,149,152],{},[46,146,147],{},"Controller 不该管事务","。把 ",[32,150,151],{},"@Transactional"," 加到 Controller 上，事务会跨越视图渲染阶段，\n数据库连接被占用的时间被无谓拉长；",[28,154,155,158,159,162],{},[46,156,157],{},"Repository 太细","。单个 ",[32,160,161],{},"save"," 本来就是一个事务，多个 Repository 调用的组合才需要事务，\n而组合发生在 Service。",[13,164,165,166,169],{},"因此 Service 的典型形态是：",[46,167,168],{},"接口定义业务能力，实现类负责编排多个 Repository 并声明事务边界","。",[171,172,177],"pre",{"className":173,"code":174,"language":175,"meta":176,"style":176},"language-java shiki shiki-themes github-light github-dark","public interface LoanService {\n\n    Loan borrow(Long bookId, Long memberId);\n\n    List\u003CLoan> findByMember(Long memberId);\n}\n","java","",[32,178,179,199,206,231,236,257],{"__ignoreMap":176},[180,181,184,188,191,195],"span",{"class":182,"line":183},"line",1,[180,185,187],{"class":186},"szBVR","public",[180,189,190],{"class":186}," interface",[180,192,194],{"class":193},"sScJk"," LoanService",[180,196,198],{"class":197},"sVt8B"," {\n",[180,200,202],{"class":182,"line":201},2,[180,203,205],{"emptyLinePlaceholder":204},true,"\n",[180,207,209,212,215,218,222,225,228],{"class":182,"line":208},3,[180,210,211],{"class":197},"    Loan ",[180,213,214],{"class":193},"borrow",[180,216,217],{"class":197},"(Long ",[180,219,221],{"class":220},"s4XuR","bookId",[180,223,224],{"class":197},", Long ",[180,226,227],{"class":220},"memberId",[180,229,230],{"class":197},");\n",[180,232,234],{"class":182,"line":233},4,[180,235,205],{"emptyLinePlaceholder":204},[180,237,239,242,245,248,251,253,255],{"class":182,"line":238},5,[180,240,241],{"class":197},"    List\u003C",[180,243,244],{"class":186},"Loan",[180,246,247],{"class":197},"> ",[180,249,250],{"class":193},"findByMember",[180,252,217],{"class":197},[180,254,227],{"class":220},[180,256,230],{"class":197},[180,258,260],{"class":182,"line":259},6,[180,261,262],{"class":197},"}\n",[171,264,266],{"className":173,"code":265,"language":175,"meta":176,"style":176},"package com.example.library.service;\n\nimport com.example.library.domain.Book;\nimport com.example.library.domain.BookStatus;\nimport com.example.library.domain.Loan;\nimport com.example.library.domain.Member;\nimport com.example.library.repository.BookRepository;\nimport com.example.library.repository.LoanRepository;\nimport com.example.library.repository.MemberRepository;\nimport org.springframework.stereotype.Service;\nimport org.springframework.transaction.annotation.Transactional;\n\nimport java.time.LocalDate;\nimport java.util.List;\nimport java.util.NoSuchElementException;\n\n@Service\n@Transactional(readOnly = true)        \u002F\u002F 整个类的默认：查询型方法\npublic class LoanServiceImpl implements LoanService {\n\n    private final BookRepository bookRepository;\n    private final MemberRepository memberRepository;\n    private final LoanRepository loanRepository;\n\n    public LoanServiceImpl(BookRepository bookRepository,\n                           MemberRepository memberRepository,\n                           LoanRepository loanRepository) {\n        this.bookRepository = bookRepository;\n        this.memberRepository = memberRepository;\n        this.loanRepository = loanRepository;\n    }\n\n    @Override\n    @Transactional                    \u002F\u002F 写方法单独覆盖：方法级注解优先于类级\n    public Loan borrow(Long bookId, Long memberId) {\n        Book book = bookRepository.findById(bookId)\n                .orElseThrow(() -> new NoSuchElementException(\"图书不存在：\" + bookId));\n        if (book.getStatus() != BookStatus.AVAILABLE) {\n            throw new IllegalStateException(\"这本书当前不可借\");\n        }\n\n        Member member = memberRepository.findById(memberId)\n                .orElseThrow(() -> new NoSuchElementException(\"会员不存在：\" + memberId));\n\n        book.markBorrowed();\n        member.increaseActiveLoans();\n\n        return loanRepository.save(new Loan(book, member, LocalDate.now()));\n    }\n\n    @Override\n    public List\u003CLoan> findByMember(Long memberId) {\n        return loanRepository.findByMemberId(memberId);   \u002F\u002F 用类级 readOnly=true\n    }\n}\n",[32,267,268,276,280,288,295,302,309,317,325,333,341,349,354,362,370,378,383,392,420,438,443,455,465,475,480,497,508,520,535,548,561,567,572,581,591,611,628,661,682,700,706,711,727,752,757,769,780,785,813,818,823,830,850,866,871],{"__ignoreMap":176},[180,269,270,273],{"class":182,"line":183},[180,271,272],{"class":186},"package",[180,274,275],{"class":197}," com.example.library.service;\n",[180,277,278],{"class":182,"line":201},[180,279,205],{"emptyLinePlaceholder":204},[180,281,282,285],{"class":182,"line":208},[180,283,284],{"class":186},"import",[180,286,287],{"class":197}," com.example.library.domain.Book;\n",[180,289,290,292],{"class":182,"line":233},[180,291,284],{"class":186},[180,293,294],{"class":197}," com.example.library.domain.BookStatus;\n",[180,296,297,299],{"class":182,"line":238},[180,298,284],{"class":186},[180,300,301],{"class":197}," com.example.library.domain.Loan;\n",[180,303,304,306],{"class":182,"line":259},[180,305,284],{"class":186},[180,307,308],{"class":197}," com.example.library.domain.Member;\n",[180,310,312,314],{"class":182,"line":311},7,[180,313,284],{"class":186},[180,315,316],{"class":197}," com.example.library.repository.BookRepository;\n",[180,318,320,322],{"class":182,"line":319},8,[180,321,284],{"class":186},[180,323,324],{"class":197}," com.example.library.repository.LoanRepository;\n",[180,326,328,330],{"class":182,"line":327},9,[180,329,284],{"class":186},[180,331,332],{"class":197}," com.example.library.repository.MemberRepository;\n",[180,334,336,338],{"class":182,"line":335},10,[180,337,284],{"class":186},[180,339,340],{"class":197}," org.springframework.stereotype.Service;\n",[180,342,344,346],{"class":182,"line":343},11,[180,345,284],{"class":186},[180,347,348],{"class":197}," org.springframework.transaction.annotation.Transactional;\n",[180,350,352],{"class":182,"line":351},12,[180,353,205],{"emptyLinePlaceholder":204},[180,355,357,359],{"class":182,"line":356},13,[180,358,284],{"class":186},[180,360,361],{"class":197}," java.time.LocalDate;\n",[180,363,365,367],{"class":182,"line":364},14,[180,366,284],{"class":186},[180,368,369],{"class":197}," java.util.List;\n",[180,371,373,375],{"class":182,"line":372},15,[180,374,284],{"class":186},[180,376,377],{"class":197}," java.util.NoSuchElementException;\n",[180,379,381],{"class":182,"line":380},16,[180,382,205],{"emptyLinePlaceholder":204},[180,384,386,389],{"class":182,"line":385},17,[180,387,388],{"class":197},"@",[180,390,391],{"class":186},"Service\n",[180,393,395,397,400,403,407,410,413,416],{"class":182,"line":394},18,[180,396,388],{"class":197},[180,398,399],{"class":186},"Transactional",[180,401,402],{"class":197},"(",[180,404,406],{"class":405},"sj4cs","readOnly",[180,408,409],{"class":186}," =",[180,411,412],{"class":405}," true",[180,414,415],{"class":197},")        ",[180,417,419],{"class":418},"sJ8bj","\u002F\u002F 整个类的默认：查询型方法\n",[180,421,423,425,428,431,434,436],{"class":182,"line":422},19,[180,424,187],{"class":186},[180,426,427],{"class":186}," class",[180,429,430],{"class":193}," LoanServiceImpl",[180,432,433],{"class":186}," implements",[180,435,194],{"class":193},[180,437,198],{"class":197},[180,439,441],{"class":182,"line":440},20,[180,442,205],{"emptyLinePlaceholder":204},[180,444,446,449,452],{"class":182,"line":445},21,[180,447,448],{"class":186},"    private",[180,450,451],{"class":186}," final",[180,453,454],{"class":197}," BookRepository bookRepository;\n",[180,456,458,460,462],{"class":182,"line":457},22,[180,459,448],{"class":186},[180,461,451],{"class":186},[180,463,464],{"class":197}," MemberRepository memberRepository;\n",[180,466,468,470,472],{"class":182,"line":467},23,[180,469,448],{"class":186},[180,471,451],{"class":186},[180,473,474],{"class":197}," LoanRepository loanRepository;\n",[180,476,478],{"class":182,"line":477},24,[180,479,205],{"emptyLinePlaceholder":204},[180,481,483,486,488,491,494],{"class":182,"line":482},25,[180,484,485],{"class":186},"    public",[180,487,430],{"class":193},[180,489,490],{"class":197},"(BookRepository ",[180,492,493],{"class":220},"bookRepository",[180,495,496],{"class":197},",\n",[180,498,500,503,506],{"class":182,"line":499},26,[180,501,502],{"class":197},"                           MemberRepository ",[180,504,505],{"class":220},"memberRepository",[180,507,496],{"class":197},[180,509,511,514,517],{"class":182,"line":510},27,[180,512,513],{"class":197},"                           LoanRepository ",[180,515,516],{"class":220},"loanRepository",[180,518,519],{"class":197},") {\n",[180,521,523,526,529,532],{"class":182,"line":522},28,[180,524,525],{"class":405},"        this",[180,527,528],{"class":197},".bookRepository ",[180,530,531],{"class":186},"=",[180,533,534],{"class":197}," bookRepository;\n",[180,536,538,540,543,545],{"class":182,"line":537},29,[180,539,525],{"class":405},[180,541,542],{"class":197},".memberRepository ",[180,544,531],{"class":186},[180,546,547],{"class":197}," memberRepository;\n",[180,549,551,553,556,558],{"class":182,"line":550},30,[180,552,525],{"class":405},[180,554,555],{"class":197},".loanRepository ",[180,557,531],{"class":186},[180,559,560],{"class":197}," loanRepository;\n",[180,562,564],{"class":182,"line":563},31,[180,565,566],{"class":197},"    }\n",[180,568,570],{"class":182,"line":569},32,[180,571,205],{"emptyLinePlaceholder":204},[180,573,575,578],{"class":182,"line":574},33,[180,576,577],{"class":197},"    @",[180,579,580],{"class":186},"Override\n",[180,582,584,586,588],{"class":182,"line":583},34,[180,585,577],{"class":197},[180,587,399],{"class":186},[180,589,590],{"class":418},"                    \u002F\u002F 写方法单独覆盖：方法级注解优先于类级\n",[180,592,594,596,599,601,603,605,607,609],{"class":182,"line":593},35,[180,595,485],{"class":186},[180,597,598],{"class":197}," Loan ",[180,600,214],{"class":193},[180,602,217],{"class":197},[180,604,221],{"class":220},[180,606,224],{"class":197},[180,608,227],{"class":220},[180,610,519],{"class":197},[180,612,614,617,619,622,625],{"class":182,"line":613},36,[180,615,616],{"class":197},"        Book book ",[180,618,531],{"class":186},[180,620,621],{"class":197}," bookRepository.",[180,623,624],{"class":193},"findById",[180,626,627],{"class":197},"(bookId)\n",[180,629,631,634,637,640,643,646,649,651,655,658],{"class":182,"line":630},37,[180,632,633],{"class":197},"                .",[180,635,636],{"class":193},"orElseThrow",[180,638,639],{"class":197},"(() ",[180,641,642],{"class":186},"->",[180,644,645],{"class":186}," new",[180,647,648],{"class":193}," NoSuchElementException",[180,650,402],{"class":197},[180,652,654],{"class":653},"sZZnC","\"图书不存在：\"",[180,656,657],{"class":186}," +",[180,659,660],{"class":197}," bookId));\n",[180,662,664,667,670,673,676,679],{"class":182,"line":663},38,[180,665,666],{"class":186},"        if",[180,668,669],{"class":197}," (book.",[180,671,672],{"class":193},"getStatus",[180,674,675],{"class":197},"() ",[180,677,678],{"class":186},"!=",[180,680,681],{"class":197}," BookStatus.AVAILABLE) {\n",[180,683,685,688,690,693,695,698],{"class":182,"line":684},39,[180,686,687],{"class":186},"            throw",[180,689,645],{"class":186},[180,691,692],{"class":193}," IllegalStateException",[180,694,402],{"class":197},[180,696,697],{"class":653},"\"这本书当前不可借\"",[180,699,230],{"class":197},[180,701,703],{"class":182,"line":702},40,[180,704,705],{"class":197},"        }\n",[180,707,709],{"class":182,"line":708},41,[180,710,205],{"emptyLinePlaceholder":204},[180,712,714,717,719,722,724],{"class":182,"line":713},42,[180,715,716],{"class":197},"        Member member ",[180,718,531],{"class":186},[180,720,721],{"class":197}," memberRepository.",[180,723,624],{"class":193},[180,725,726],{"class":197},"(memberId)\n",[180,728,730,732,734,736,738,740,742,744,747,749],{"class":182,"line":729},43,[180,731,633],{"class":197},[180,733,636],{"class":193},[180,735,639],{"class":197},[180,737,642],{"class":186},[180,739,645],{"class":186},[180,741,648],{"class":193},[180,743,402],{"class":197},[180,745,746],{"class":653},"\"会员不存在：\"",[180,748,657],{"class":186},[180,750,751],{"class":197}," memberId));\n",[180,753,755],{"class":182,"line":754},44,[180,756,205],{"emptyLinePlaceholder":204},[180,758,760,763,766],{"class":182,"line":759},45,[180,761,762],{"class":197},"        book.",[180,764,765],{"class":193},"markBorrowed",[180,767,768],{"class":197},"();\n",[180,770,772,775,778],{"class":182,"line":771},46,[180,773,774],{"class":197},"        member.",[180,776,777],{"class":193},"increaseActiveLoans",[180,779,768],{"class":197},[180,781,783],{"class":182,"line":782},47,[180,784,205],{"emptyLinePlaceholder":204},[180,786,788,791,794,796,798,801,804,807,810],{"class":182,"line":787},48,[180,789,790],{"class":186},"        return",[180,792,793],{"class":197}," loanRepository.",[180,795,161],{"class":193},[180,797,402],{"class":197},[180,799,800],{"class":186},"new",[180,802,803],{"class":193}," Loan",[180,805,806],{"class":197},"(book, member, LocalDate.",[180,808,809],{"class":193},"now",[180,811,812],{"class":197},"()));\n",[180,814,816],{"class":182,"line":815},49,[180,817,566],{"class":197},[180,819,821],{"class":182,"line":820},50,[180,822,205],{"emptyLinePlaceholder":204},[180,824,826,828],{"class":182,"line":825},51,[180,827,577],{"class":197},[180,829,580],{"class":186},[180,831,833,835,838,840,842,844,846,848],{"class":182,"line":832},52,[180,834,485],{"class":186},[180,836,837],{"class":197}," List\u003C",[180,839,244],{"class":186},[180,841,247],{"class":197},[180,843,250],{"class":193},[180,845,217],{"class":197},[180,847,227],{"class":220},[180,849,519],{"class":197},[180,851,853,855,857,860,863],{"class":182,"line":852},53,[180,854,790],{"class":186},[180,856,793],{"class":197},[180,858,859],{"class":193},"findByMemberId",[180,861,862],{"class":197},"(memberId);   ",[180,864,865],{"class":418},"\u002F\u002F 用类级 readOnly=true\n",[180,867,869],{"class":182,"line":868},54,[180,870,566],{"class":197},[180,872,874],{"class":182,"line":873},55,[180,875,262],{"class":197},[13,877,878,888],{},[46,879,880,881,884,885,887],{},"「类上 ",[32,882,883],{},"readOnly = true"," + 写方法上 ",[32,886,151],{}," 覆盖」"," 是很常见的组合：查询方法默认按\n只读优化，写方法显式声明。方法级注解的优先级高于类级。",[17,890,892],{"id":891},"_3-transactional-为什么能生效","3 @Transactional 为什么能生效",[13,894,895],{},[896,897],"img",{"alt":898,"src":899},"Service 层的事务边界由 Spring 代理开启与提交；Spring Bean 内部自调用绕开代理，因此 @Transactional 不生效","\u002Fimages\u002Fspring-notes-10-transaction-proxy.svg",[13,901,902,904,905,908,909,911,912,914,915,169],{},[32,903,151],{}," 是",[46,906,907],{},"声明式事务","：你不写 ",[32,910,114],{}," \u002F ",[32,913,118],{},"，由 Spring 在运行时用 AOP 代理\n包一层。得到这个代理有一个前提：",[46,916,917],{},"调用必须从代理外部进来",[13,919,920],{},"由此推出四条硬性要求，违反任何一条，事务都不生效（而且是静默失效）：",[58,922,923,933],{},[61,924,925],{},[64,926,927,930],{},[67,928,929],{},"要求",[67,931,932],{},"说明",[74,934,935,945,957,968],{},[64,936,937,942],{},[79,938,939,940],{},"方法必须是 ",[32,941,187],{},[79,943,944],{},"非 public 方法代理不拦截",[64,946,947,950],{},[79,948,949],{},"必须是外部调用",[79,951,952,953,956],{},"同类内部 ",[32,954,955],{},"this.method()"," 不经过代理",[64,958,959,962],{},[79,960,961],{},"Bean 必须由容器管理",[79,963,964,965,967],{},"自己 ",[32,966,800],{}," 出来的对象没有代理",[64,969,970,973],{},[79,971,972],{},"异常必须抛出方法边界",[79,974,975],{},"在方法内部 catch 掉且不重抛，代理看不到异常，不会回滚",[13,977,978],{},"最后一条特别容易忽略：",[171,980,982],{"className":173,"code":981,"language":175,"meta":176,"style":176},"@Transactional\npublic void doSomething() {\n    try {\n        riskyOperation();\n    } catch (Exception e) {\n        log.warn(\"出错了\", e);       \u002F\u002F 吞掉异常\n    }\n    \u002F\u002F 事务照样提交，前面的写操作留下了\n}\n",[32,983,984,991,1004,1011,1018,1034,1053,1057,1062],{"__ignoreMap":176},[180,985,986,988],{"class":182,"line":183},[180,987,388],{"class":197},[180,989,990],{"class":186},"Transactional\n",[180,992,993,995,998,1001],{"class":182,"line":201},[180,994,187],{"class":186},[180,996,997],{"class":186}," void",[180,999,1000],{"class":193}," doSomething",[180,1002,1003],{"class":197},"() {\n",[180,1005,1006,1009],{"class":182,"line":208},[180,1007,1008],{"class":186},"    try",[180,1010,198],{"class":197},[180,1012,1013,1016],{"class":182,"line":233},[180,1014,1015],{"class":193},"        riskyOperation",[180,1017,768],{"class":197},[180,1019,1020,1023,1026,1029,1032],{"class":182,"line":238},[180,1021,1022],{"class":197},"    } ",[180,1024,1025],{"class":186},"catch",[180,1027,1028],{"class":197}," (Exception ",[180,1030,1031],{"class":220},"e",[180,1033,519],{"class":197},[180,1035,1036,1039,1042,1044,1047,1050],{"class":182,"line":259},[180,1037,1038],{"class":197},"        log.",[180,1040,1041],{"class":193},"warn",[180,1043,402],{"class":197},[180,1045,1046],{"class":653},"\"出错了\"",[180,1048,1049],{"class":197},", e);       ",[180,1051,1052],{"class":418},"\u002F\u002F 吞掉异常\n",[180,1054,1055],{"class":182,"line":311},[180,1056,566],{"class":197},[180,1058,1059],{"class":182,"line":319},[180,1060,1061],{"class":418},"    \u002F\u002F 事务照样提交，前面的写操作留下了\n",[180,1063,1064],{"class":182,"line":327},[180,1065,262],{"class":197},[17,1067,1069],{"id":1068},"_4-传播行为内部方法要不要用同一个事务","4 传播行为：内部方法要不要用同一个事务",[13,1071,1072,1073,1076],{},"当",[46,1074,1075],{},"事务方法调用另一个事务方法","时，第二个方法该怎么处理事务，就是传播行为（propagation）\n决定的。七种取值，实际用的主要是前两个：",[58,1078,1079,1092],{},[61,1080,1081],{},[64,1082,1083,1086,1089],{},[67,1084,1085],{},"传播行为",[67,1087,1088],{},"行为",[67,1090,1091],{},"典型用途",[74,1093,1094,1108,1124,1137,1150,1163,1176],{},[64,1095,1096,1102,1105],{},[79,1097,1098,1101],{},[32,1099,1100],{},"REQUIRED","（默认）",[79,1103,1104],{},"有事务就加入，没有就新建",[79,1106,1107],{},"绝大多数业务方法",[64,1109,1110,1115,1121],{},[79,1111,1112],{},[32,1113,1114],{},"REQUIRES_NEW",[79,1116,1117,1120],{},[46,1118,1119],{},"总是新建独立事务","，外层事务挂起，内层提交\u002F回滚互不影响",[79,1122,1123],{},"审计日志：主业务失败也要留下记录",[64,1125,1126,1131,1134],{},[79,1127,1128],{},[32,1129,1130],{},"NESTED",[79,1132,1133],{},"在当前事务里开一个保存点，内层失败可单独回滚到保存点",[79,1135,1136],{},"分批处理，允许部分回滚",[64,1138,1139,1144,1147],{},[79,1140,1141],{},[32,1142,1143],{},"SUPPORTS",[79,1145,1146],{},"有事务就加入，没有就以非事务方式执行",[79,1148,1149],{},"只读辅助方法",[64,1151,1152,1157,1160],{},[79,1153,1154],{},[32,1155,1156],{},"NOT_SUPPORTED",[79,1158,1159],{},"挂起当前事务，以非事务方式执行",[79,1161,1162],{},"明确不希望被事务包裹的操作",[64,1164,1165,1170,1173],{},[79,1166,1167],{},[32,1168,1169],{},"MANDATORY",[79,1171,1172],{},"必须在事务中调用，否则抛异常",[79,1174,1175],{},"强制约定：只能在事务内被调用",[64,1177,1178,1183,1186],{},[79,1179,1180],{},[32,1181,1182],{},"NEVER",[79,1184,1185],{},"必须在无事务环境下调用，否则抛异常",[79,1187,1188],{},"强制约定：不允许被事务包裹",[13,1190,1191],{},"两个实战要点：",[135,1193,1194,1206],{},[28,1195,1196,1201,1202,1205],{},[46,1197,1198,1200],{},[32,1199,1114],{}," 会占用一条额外连接","。外层事务占用连接不释放，内层又要一条新连接，连接池\n小的时候（比如只有 5 条）嵌套几层就可能",[46,1203,1204],{},"互相等待而死锁","。用之前先看连接池配置；",[28,1207,1208,1213],{},[46,1209,1210,1212],{},[32,1211,1114],{}," 只对「从代理外部进来的调用」有效","。同类内部调用同样不生效，需要把内层方法\n放到另一个 Bean 里。",[17,1215,1217],{"id":1216},"_5-隔离级别并发事务之间看到什么","5 隔离级别：并发事务之间看到什么",[13,1219,1220,1221,1224],{},"隔离级别管的是",[46,1222,1223],{},"并发读写时的可见性","，从弱到强：",[58,1226,1227,1242],{},[61,1228,1229],{},[64,1230,1231,1234,1237,1240],{},[67,1232,1233],{},"级别",[67,1235,1236],{},"能防住",[67,1238,1239],{},"挡不住",[67,1241,932],{},[74,1243,1244,1260,1276,1292],{},[64,1245,1246,1251,1254,1257],{},[79,1247,1248],{},[32,1249,1250],{},"READ_UNCOMMITTED",[79,1252,1253],{},"无",[79,1255,1256],{},"脏读、不可重复读、幻读",[79,1258,1259],{},"现实中基本不用",[64,1261,1262,1267,1270,1273],{},[79,1263,1264],{},[32,1265,1266],{},"READ_COMMITTED",[79,1268,1269],{},"脏读",[79,1271,1272],{},"不可重复读、幻读",[79,1274,1275],{},"Oracle、PostgreSQL 默认",[64,1277,1278,1283,1286,1289],{},[79,1279,1280],{},[32,1281,1282],{},"REPEATABLE_READ",[79,1284,1285],{},"脏读、不可重复读",[79,1287,1288],{},"幻读（MySQL 的 InnoDB 基本挡住）",[79,1290,1291],{},"MySQL 默认",[64,1293,1294,1299,1302,1305],{},[79,1295,1296],{},[32,1297,1298],{},"SERIALIZABLE",[79,1300,1301],{},"全部",[79,1303,1304],{},"—",[79,1306,1307],{},"并发性能最差，只在必要时用",[13,1309,1310,1311,1314],{},"选择原则是",[46,1312,1313],{},"用能满足业务的最弱级别","：越强越容易发生锁等待与超时。",[171,1316,1318],{"className":173,"code":1317,"language":175,"meta":176,"style":176},"@Transactional(isolation = Isolation.REPEATABLE_READ)\npublic ReportSummary buildSummary(Long memberId) { ... }\n",[32,1319,1320,1336],{"__ignoreMap":176},[180,1321,1322,1324,1326,1328,1331,1333],{"class":182,"line":183},[180,1323,388],{"class":197},[180,1325,399],{"class":186},[180,1327,402],{"class":197},[180,1329,1330],{"class":405},"isolation",[180,1332,409],{"class":186},[180,1334,1335],{"class":197}," Isolation.REPEATABLE_READ)\n",[180,1337,1338,1340,1343,1346],{"class":182,"line":201},[180,1339,187],{"class":186},[180,1341,1342],{"class":197}," ReportSummary ",[180,1344,1345],{"class":193},"buildSummary",[180,1347,1348],{"class":197},"(Long memberId) { ... }\n",[13,1350,1351,1352,1354,1355,1358,1359,1361],{},"不写 ",[32,1353,1330],{}," 时用",[46,1356,1357],{},"数据库自己的默认级别","——这也是为什么同一个 ",[32,1360,151],{}," 在 MySQL\n和 PostgreSQL 上表现不同。",[17,1363,1365],{"id":1364},"_6-回滚规则默认只回滚运行时异常","6 回滚规则：默认只回滚运行时异常",[13,1367,1368],{},"Spring 的默认规则是：",[58,1370,1371,1381],{},[61,1372,1373],{},[64,1374,1375,1378],{},[67,1376,1377],{},"抛出的异常",[67,1379,1380],{},"默认行为",[74,1382,1383,1394,1403],{},[64,1384,1385,1391],{},[79,1386,1387,1390],{},[32,1388,1389],{},"RuntimeException"," 及其子类",[79,1392,1393],{},"回滚",[64,1395,1396,1401],{},[79,1397,1398],{},[32,1399,1400],{},"Error",[79,1402,1393],{},[64,1404,1405,1417],{},[79,1406,1407],{},[46,1408,1409,1410,1413,1414,1416],{},"受检异常（",[32,1411,1412],{},"Exception"," 的子类但不继承 ",[32,1415,1389],{},"）",[79,1418,1419],{},[46,1420,1421],{},"不回滚",[13,1423,1424,1425,1428],{},"业务里常把「余额不足」「库存不足」定义成受检异常，然后发现",[46,1426,1427],{},"事务没回滚","。解决办法是显式声明：",[171,1430,1432],{"className":173,"code":1431,"language":175,"meta":176,"style":176},"@Transactional(rollbackFor = Exception.class)\npublic Loan borrow(Long bookId, Long memberId) throws BusinessException { ... }\n",[32,1433,1434,1450],{"__ignoreMap":176},[180,1435,1436,1438,1440,1442,1445,1447],{"class":182,"line":183},[180,1437,388],{"class":197},[180,1439,399],{"class":186},[180,1441,402],{"class":197},[180,1443,1444],{"class":405},"rollbackFor",[180,1446,409],{"class":186},[180,1448,1449],{"class":197}," Exception.class)\n",[180,1451,1452,1454,1456,1458],{"class":182,"line":201},[180,1453,187],{"class":186},[180,1455,598],{"class":197},[180,1457,214],{"class":193},[180,1459,1460],{"class":197},"(Long bookId, Long memberId) throws BusinessException { ... }\n",[13,1462,1463,1464,1467,1468,1471,1472,1467],{},"需要注意它的",[46,1465,1466],{},"反方向用力","：",[32,1469,1470],{},"rollbackFor = Exception.class"," 会让所有异常都回滚，如果想「某类\n异常不回滚」，用 ",[32,1473,1474],{},"noRollbackFor",[171,1476,1478],{"className":173,"code":1477,"language":175,"meta":176,"style":176},"@Transactional(noRollbackFor = NotificationFailedException.class)\n",[32,1479,1480],{"__ignoreMap":176},[180,1481,1482,1484,1486,1488,1490,1492],{"class":182,"line":183},[180,1483,388],{"class":197},[180,1485,399],{"class":186},[180,1487,402],{"class":197},[180,1489,1474],{"class":405},[180,1491,409],{"class":186},[180,1493,1494],{"class":197}," NotificationFailedException.class)\n",[17,1496,1498],{"id":1497},"_7-只读与超时","7 只读与超时",[171,1500,1502],{"className":173,"code":1501,"language":175,"meta":176,"style":176},"@Transactional(readOnly = true, timeout = 5)\npublic List\u003CBookTitleDto> search(String keyword) { ... }\n",[32,1503,1504,1532],{"__ignoreMap":176},[180,1505,1506,1508,1510,1512,1514,1516,1518,1521,1524,1526,1529],{"class":182,"line":183},[180,1507,388],{"class":197},[180,1509,399],{"class":186},[180,1511,402],{"class":197},[180,1513,406],{"class":405},[180,1515,409],{"class":186},[180,1517,412],{"class":405},[180,1519,1520],{"class":197},", ",[180,1522,1523],{"class":405},"timeout",[180,1525,409],{"class":186},[180,1527,1528],{"class":405}," 5",[180,1530,1531],{"class":197},")\n",[180,1533,1534,1536,1539,1542,1545,1548,1551],{"class":182,"line":201},[180,1535,187],{"class":186},[180,1537,1538],{"class":197}," List",[180,1540,1541],{"class":186},"\u003C",[180,1543,1544],{"class":197},"BookTitleDto",[180,1546,1547],{"class":186},">",[180,1549,1550],{"class":193}," search",[180,1552,1553],{"class":197},"(String keyword) { ... }\n",[135,1555,1556,1567],{},[28,1557,1558,1562,1563,1566],{},[46,1559,1560],{},[32,1561,883],{}," 是给提供者的提示：Hibernate 会跳过脏检查（不再为每个实体保存快照），\n查询型方法因此更快。",[46,1564,1565],{},"别用在写方法上","，轻则无效，重则异常；",[28,1568,1569,1574,1575,1578],{},[46,1570,1571,1573],{},[32,1572,1523],{}," 是秒数","：超时后事务被回滚并抛异常。它是",[46,1576,1577],{},"兜底","而不是精度控制，适合给\n外部依赖不可控的查询加一层保护。",[17,1580,1582],{"id":1581},"_8-并发冲突悲观锁与乐观锁","8 并发冲突：悲观锁与乐观锁",[13,1584,1585],{},"两个管理员同时编辑同一本书的库存，或者秒杀场景下多人抢同一本库存为 1 的书，都会撞上并发竞争。",[1587,1588,1590],"h3",{"id":1589},"_81-悲观锁先锁住再操作","8.1 悲观锁：先锁住再操作",[171,1592,1594],{"className":173,"code":1593,"language":175,"meta":176,"style":176},"public interface BookRepository extends JpaRepository\u003CBook, Long> {\n\n    @Lock(LockModeType.PESSIMISTIC_WRITE)\n    @Query(\"select b from Book b where b.id = :id\")\n    Optional\u003CBook> findByIdForUpdate(@Param(\"id\") Long id);\n}\n",[32,1595,1596,1624,1628,1638,1652,1683],{"__ignoreMap":176},[180,1597,1598,1600,1602,1605,1608,1611,1613,1616,1618,1621],{"class":182,"line":183},[180,1599,187],{"class":186},[180,1601,190],{"class":186},[180,1603,1604],{"class":193}," BookRepository",[180,1606,1607],{"class":186}," extends",[180,1609,1610],{"class":193}," JpaRepository",[180,1612,1541],{"class":197},[180,1614,1615],{"class":186},"Book",[180,1617,1520],{"class":197},[180,1619,1620],{"class":186},"Long",[180,1622,1623],{"class":197},"> {\n",[180,1625,1626],{"class":182,"line":201},[180,1627,205],{"emptyLinePlaceholder":204},[180,1629,1630,1632,1635],{"class":182,"line":208},[180,1631,577],{"class":197},[180,1633,1634],{"class":186},"Lock",[180,1636,1637],{"class":197},"(LockModeType.PESSIMISTIC_WRITE)\n",[180,1639,1640,1642,1645,1647,1650],{"class":182,"line":233},[180,1641,577],{"class":197},[180,1643,1644],{"class":186},"Query",[180,1646,402],{"class":197},[180,1648,1649],{"class":653},"\"select b from Book b where b.id = :id\"",[180,1651,1531],{"class":197},[180,1653,1654,1657,1659,1661,1664,1667,1670,1672,1675,1678,1681],{"class":182,"line":238},[180,1655,1656],{"class":197},"    Optional\u003C",[180,1658,1615],{"class":186},[180,1660,247],{"class":197},[180,1662,1663],{"class":193},"findByIdForUpdate",[180,1665,1666],{"class":197},"(@",[180,1668,1669],{"class":186},"Param",[180,1671,402],{"class":197},[180,1673,1674],{"class":653},"\"id\"",[180,1676,1677],{"class":197},") Long ",[180,1679,1680],{"class":220},"id",[180,1682,230],{"class":197},[180,1684,1685],{"class":182,"line":259},[180,1686,262],{"class":197},[13,1688,1689,1692,1693,1696],{},[32,1690,1691],{},"PESSIMISTIC_WRITE"," 会生成 ",[32,1694,1695],{},"select ... for update","，把这一行锁到事务结束：",[171,1698,1700],{"className":173,"code":1699,"language":175,"meta":176,"style":176},"@Transactional\npublic Loan borrow(Long bookId, Long memberId) {\n    Book book = bookRepository.findByIdForUpdate(bookId).orElseThrow();\n    \u002F\u002F 期间别人拿不到这行的写锁，只能等\n}\n",[32,1701,1702,1708,1719,1737,1742],{"__ignoreMap":176},[180,1703,1704,1706],{"class":182,"line":183},[180,1705,388],{"class":197},[180,1707,990],{"class":186},[180,1709,1710,1712,1714,1716],{"class":182,"line":201},[180,1711,187],{"class":186},[180,1713,598],{"class":197},[180,1715,214],{"class":193},[180,1717,1718],{"class":197},"(Long bookId, Long memberId) {\n",[180,1720,1721,1724,1726,1728,1730,1733,1735],{"class":182,"line":208},[180,1722,1723],{"class":197},"    Book book ",[180,1725,531],{"class":186},[180,1727,621],{"class":197},[180,1729,1663],{"class":193},[180,1731,1732],{"class":197},"(bookId).",[180,1734,636],{"class":193},[180,1736,768],{"class":197},[180,1738,1739],{"class":182,"line":233},[180,1740,1741],{"class":418},"    \u002F\u002F 期间别人拿不到这行的写锁，只能等\n",[180,1743,1744],{"class":182,"line":238},[180,1745,262],{"class":197},[13,1747,1748,1749,1752,1753,1756],{},"适用场景：",[46,1750,1751],{},"竞争激烈、失败代价高","（库存扣减、账号余额）。\n代价：",[46,1754,1755],{},"锁定期间别的请求排队","，锁范围大时容易连锁导致超时；锁必须在事务内才有意义。",[1587,1758,1760],{"id":1759},"_82-乐观锁先干提交时检查版本","8.2 乐观锁：先干，提交时检查版本",[171,1762,1764],{"className":173,"code":1763,"language":175,"meta":176,"style":176},"@Entity\npublic class Book {\n\n    @Version\n    private Long version;\n\n    \u002F\u002F ...\n}\n",[32,1765,1766,1773,1784,1788,1795,1802,1806,1811],{"__ignoreMap":176},[180,1767,1768,1770],{"class":182,"line":183},[180,1769,388],{"class":197},[180,1771,1772],{"class":186},"Entity\n",[180,1774,1775,1777,1779,1782],{"class":182,"line":201},[180,1776,187],{"class":186},[180,1778,427],{"class":186},[180,1780,1781],{"class":193}," Book",[180,1783,198],{"class":197},[180,1785,1786],{"class":182,"line":208},[180,1787,205],{"emptyLinePlaceholder":204},[180,1789,1790,1792],{"class":182,"line":233},[180,1791,577],{"class":197},[180,1793,1794],{"class":186},"Version\n",[180,1796,1797,1799],{"class":182,"line":238},[180,1798,448],{"class":186},[180,1800,1801],{"class":197}," Long version;\n",[180,1803,1804],{"class":182,"line":259},[180,1805,205],{"emptyLinePlaceholder":204},[180,1807,1808],{"class":182,"line":311},[180,1809,1810],{"class":418},"    \u002F\u002F ...\n",[180,1812,1813],{"class":182,"line":319},[180,1814,262],{"class":197},[13,1816,1817,1818,1821,1822,1825,1826,1829,1830,1833],{},"Hibernate 在 ",[32,1819,1820],{},"update"," 时自动带上版本条件：",[32,1823,1824],{},"where id = ? and version = ?","。若版本已变，更新影响\n0 行，抛 ",[32,1827,1828],{},"OptimisticLockingFailureException","（JPA 层是 ",[32,1831,1832],{},"OptimisticLockException","）。",[13,1835,1748,1836,1839,1840,1843],{},[46,1837,1838],{},"冲突概率低","的编辑类操作（后台管理表单）。\n代价：冲突时",[46,1841,1842],{},"必须由调用方重试或提示用户","，写代码时要处理这个异常，否则用户会看到 500。",[13,1845,1846],{},"两者怎么选：",[58,1848,1849,1862],{},[61,1850,1851],{},[64,1852,1853,1856,1859],{},[67,1854,1855],{},"对比",[67,1857,1858],{},"悲观锁",[67,1860,1861],{},"乐观锁",[74,1863,1864,1875,1886,1897],{},[64,1865,1866,1869,1872],{},[79,1867,1868],{},"冲突假设",[79,1870,1871],{},"假设会冲突，先锁",[79,1873,1874],{},"假设不冲突，提交时检查",[64,1876,1877,1880,1883],{},[79,1878,1879],{},"性能影响",[79,1881,1882],{},"锁等待，吞吐下降",[79,1884,1885],{},"无锁开销，冲突时要重试",[64,1887,1888,1891,1894],{},[79,1889,1890],{},"适合场景",[79,1892,1893],{},"强竞争、必须成功",[79,1895,1896],{},"低竞争、可重试",[64,1898,1899,1902,1908],{},[79,1900,1901],{},"实现成本",[79,1903,1904,1905],{},"加一行 ",[32,1906,1907],{},"@Lock",[79,1909,1910,1911,1914],{},"加 ",[32,1912,1913],{},"@Version"," + 处理冲突异常",[17,1916,1918],{"id":1917},"_9-编程式事务需要精确控制边界时","9 编程式事务：需要精确控制边界时",[13,1920,1921,1922,1467],{},"声明式搞不定的场景（一个方法里有几段要各自独立提交的逻辑），用 ",[32,1923,1924],{},"TransactionTemplate",[171,1926,1928],{"className":173,"code":1927,"language":175,"meta":176,"style":176},"@Service\npublic class BookImportService {\n\n    private static final Logger log = LoggerFactory.getLogger(BookImportService.class);\n\n    private final BookRepository bookRepository;\n    private final TransactionTemplate transactionTemplate;\n\n    public BookImportService(BookRepository bookRepository,\n                             PlatformTransactionManager transactionManager) {\n        this.bookRepository = bookRepository;\n        \u002F\u002F 也可以把 TransactionTemplate 声明成 @Bean 注入，这里为了看清依赖关系直接 new\n        this.transactionTemplate = new TransactionTemplate(transactionManager);\n    }\n\n    \u002F** 每本书一个独立事务：某一本 ISBN 重复时只跳过这一本，其余照常导入 *\u002F\n    public int importBooks(List\u003CBookForm> forms) {\n        int imported = 0;\n        for (BookForm form : forms) {\n            Integer result = transactionTemplate.execute(status -> {\n                try {\n                    Book book = new Book(form.getTitle(), form.getAuthor());\n                    book.setIsbn(form.getIsbn());\n                    bookRepository.saveAndFlush(book);   \u002F\u002F 立刻 flush，唯一约束冲突在这里就暴露\n                    return 1;\n                } catch (DataIntegrityViolationException e) {\n                    status.setRollbackOnly();\n                    log.warn(\"跳过 ISBN 重复的《{}》\", form.getTitle());\n                    return 0;\n                }\n            });\n            imported += result == null ? 0 : result;\n        }\n        return imported;\n    }\n}\n",[32,1929,1930,1936,1947,1951,1974,1978,1986,1995,1999,2011,2021,2031,2036,2053,2057,2061,2066,2089,2105,2119,2139,2146,2172,2187,2201,2211,2225,2235,2254,2262,2267,2272,2300,2304,2311,2315],{"__ignoreMap":176},[180,1931,1932,1934],{"class":182,"line":183},[180,1933,388],{"class":197},[180,1935,391],{"class":186},[180,1937,1938,1940,1942,1945],{"class":182,"line":201},[180,1939,187],{"class":186},[180,1941,427],{"class":186},[180,1943,1944],{"class":193}," BookImportService",[180,1946,198],{"class":197},[180,1948,1949],{"class":182,"line":208},[180,1950,205],{"emptyLinePlaceholder":204},[180,1952,1953,1955,1958,1960,1963,1965,1968,1971],{"class":182,"line":233},[180,1954,448],{"class":186},[180,1956,1957],{"class":186}," static",[180,1959,451],{"class":186},[180,1961,1962],{"class":197}," Logger log ",[180,1964,531],{"class":186},[180,1966,1967],{"class":197}," LoggerFactory.",[180,1969,1970],{"class":193},"getLogger",[180,1972,1973],{"class":197},"(BookImportService.class);\n",[180,1975,1976],{"class":182,"line":238},[180,1977,205],{"emptyLinePlaceholder":204},[180,1979,1980,1982,1984],{"class":182,"line":259},[180,1981,448],{"class":186},[180,1983,451],{"class":186},[180,1985,454],{"class":197},[180,1987,1988,1990,1992],{"class":182,"line":311},[180,1989,448],{"class":186},[180,1991,451],{"class":186},[180,1993,1994],{"class":197}," TransactionTemplate transactionTemplate;\n",[180,1996,1997],{"class":182,"line":319},[180,1998,205],{"emptyLinePlaceholder":204},[180,2000,2001,2003,2005,2007,2009],{"class":182,"line":327},[180,2002,485],{"class":186},[180,2004,1944],{"class":193},[180,2006,490],{"class":197},[180,2008,493],{"class":220},[180,2010,496],{"class":197},[180,2012,2013,2016,2019],{"class":182,"line":335},[180,2014,2015],{"class":197},"                             PlatformTransactionManager ",[180,2017,2018],{"class":220},"transactionManager",[180,2020,519],{"class":197},[180,2022,2023,2025,2027,2029],{"class":182,"line":343},[180,2024,525],{"class":405},[180,2026,528],{"class":197},[180,2028,531],{"class":186},[180,2030,534],{"class":197},[180,2032,2033],{"class":182,"line":351},[180,2034,2035],{"class":418},"        \u002F\u002F 也可以把 TransactionTemplate 声明成 @Bean 注入，这里为了看清依赖关系直接 new\n",[180,2037,2038,2040,2043,2045,2047,2050],{"class":182,"line":356},[180,2039,525],{"class":405},[180,2041,2042],{"class":197},".transactionTemplate ",[180,2044,531],{"class":186},[180,2046,645],{"class":186},[180,2048,2049],{"class":193}," TransactionTemplate",[180,2051,2052],{"class":197},"(transactionManager);\n",[180,2054,2055],{"class":182,"line":364},[180,2056,566],{"class":197},[180,2058,2059],{"class":182,"line":372},[180,2060,205],{"emptyLinePlaceholder":204},[180,2062,2063],{"class":182,"line":380},[180,2064,2065],{"class":418},"    \u002F** 每本书一个独立事务：某一本 ISBN 重复时只跳过这一本，其余照常导入 *\u002F\n",[180,2067,2068,2070,2073,2076,2079,2082,2084,2087],{"class":182,"line":385},[180,2069,485],{"class":186},[180,2071,2072],{"class":186}," int",[180,2074,2075],{"class":193}," importBooks",[180,2077,2078],{"class":197},"(List\u003C",[180,2080,2081],{"class":186},"BookForm",[180,2083,247],{"class":197},[180,2085,2086],{"class":220},"forms",[180,2088,519],{"class":197},[180,2090,2091,2094,2097,2099,2102],{"class":182,"line":394},[180,2092,2093],{"class":186},"        int",[180,2095,2096],{"class":197}," imported ",[180,2098,531],{"class":186},[180,2100,2101],{"class":405}," 0",[180,2103,2104],{"class":197},";\n",[180,2106,2107,2110,2113,2116],{"class":182,"line":422},[180,2108,2109],{"class":186},"        for",[180,2111,2112],{"class":197}," (BookForm form ",[180,2114,2115],{"class":186},":",[180,2117,2118],{"class":197}," forms) {\n",[180,2120,2121,2124,2126,2129,2132,2135,2137],{"class":182,"line":440},[180,2122,2123],{"class":197},"            Integer result ",[180,2125,531],{"class":186},[180,2127,2128],{"class":197}," transactionTemplate.",[180,2130,2131],{"class":193},"execute",[180,2133,2134],{"class":197},"(status ",[180,2136,642],{"class":186},[180,2138,198],{"class":197},[180,2140,2141,2144],{"class":182,"line":445},[180,2142,2143],{"class":186},"                try",[180,2145,198],{"class":197},[180,2147,2148,2151,2153,2155,2157,2160,2163,2166,2169],{"class":182,"line":457},[180,2149,2150],{"class":197},"                    Book book ",[180,2152,531],{"class":186},[180,2154,645],{"class":186},[180,2156,1781],{"class":193},[180,2158,2159],{"class":197},"(form.",[180,2161,2162],{"class":193},"getTitle",[180,2164,2165],{"class":197},"(), form.",[180,2167,2168],{"class":193},"getAuthor",[180,2170,2171],{"class":197},"());\n",[180,2173,2174,2177,2180,2182,2185],{"class":182,"line":467},[180,2175,2176],{"class":197},"                    book.",[180,2178,2179],{"class":193},"setIsbn",[180,2181,2159],{"class":197},[180,2183,2184],{"class":193},"getIsbn",[180,2186,2171],{"class":197},[180,2188,2189,2192,2195,2198],{"class":182,"line":477},[180,2190,2191],{"class":197},"                    bookRepository.",[180,2193,2194],{"class":193},"saveAndFlush",[180,2196,2197],{"class":197},"(book);   ",[180,2199,2200],{"class":418},"\u002F\u002F 立刻 flush，唯一约束冲突在这里就暴露\n",[180,2202,2203,2206,2209],{"class":182,"line":482},[180,2204,2205],{"class":186},"                    return",[180,2207,2208],{"class":405}," 1",[180,2210,2104],{"class":197},[180,2212,2213,2216,2218,2221,2223],{"class":182,"line":499},[180,2214,2215],{"class":197},"                } ",[180,2217,1025],{"class":186},[180,2219,2220],{"class":197}," (DataIntegrityViolationException ",[180,2222,1031],{"class":220},[180,2224,519],{"class":197},[180,2226,2227,2230,2233],{"class":182,"line":510},[180,2228,2229],{"class":197},"                    status.",[180,2231,2232],{"class":193},"setRollbackOnly",[180,2234,768],{"class":197},[180,2236,2237,2240,2242,2244,2247,2250,2252],{"class":182,"line":522},[180,2238,2239],{"class":197},"                    log.",[180,2241,1041],{"class":193},[180,2243,402],{"class":197},[180,2245,2246],{"class":653},"\"跳过 ISBN 重复的《{}》\"",[180,2248,2249],{"class":197},", form.",[180,2251,2162],{"class":193},[180,2253,2171],{"class":197},[180,2255,2256,2258,2260],{"class":182,"line":537},[180,2257,2205],{"class":186},[180,2259,2101],{"class":405},[180,2261,2104],{"class":197},[180,2263,2264],{"class":182,"line":550},[180,2265,2266],{"class":197},"                }\n",[180,2268,2269],{"class":182,"line":563},[180,2270,2271],{"class":197},"            });\n",[180,2273,2274,2277,2280,2283,2286,2289,2292,2294,2297],{"class":182,"line":569},[180,2275,2276],{"class":197},"            imported ",[180,2278,2279],{"class":186},"+=",[180,2281,2282],{"class":197}," result ",[180,2284,2285],{"class":186},"==",[180,2287,2288],{"class":405}," null",[180,2290,2291],{"class":186}," ?",[180,2293,2101],{"class":405},[180,2295,2296],{"class":186}," :",[180,2298,2299],{"class":197}," result;\n",[180,2301,2302],{"class":182,"line":574},[180,2303,705],{"class":197},[180,2305,2306,2308],{"class":182,"line":583},[180,2307,790],{"class":186},[180,2309,2310],{"class":197}," imported;\n",[180,2312,2313],{"class":182,"line":593},[180,2314,566],{"class":197},[180,2316,2317],{"class":182,"line":613},[180,2318,262],{"class":197},[13,2320,2321,2322,2324,2325,2327,2328,2331,2332,2335],{},"这里用到的 ",[32,2323,2081],{}," 是第 06 篇那个表单对象；",[32,2326,2194],{}," 的作用是",[46,2329,2330],{},"立刻把语句发到数据库","，\n让唯一约束冲突在这个 ",[32,2333,2334],{},"try"," 里就暴露出来（否则要等整个事务提交时才知道是哪一条出的问题）。",[13,2337,2338,2341],{},[46,2339,2340],{},"默认用声明式，编程式只在边界必须逐个控制时使用","——它把事务语义写进了业务代码里，可读性会下降。",[17,2343,2345],{"id":2344},"_10-事务里不该做的事","10 事务里不该做的事",[135,2347,2348,2354,2360,2372,2378],{},[28,2349,2350,2353],{},[46,2351,2352],{},"远程调用","（HTTP、消息推送）：网络抖动会让事务一直开着，数据库连接被长期占用；",[28,2355,2356,2359],{},[46,2357,2358],{},"文件与 IO 操作","：同理，而且它本来就无法回滚；",[28,2361,2362,2367,2368,2371],{},[46,2363,2364,2365],{},"循环里逐条 ",[32,2366,161],{},"：N 次数据库往返，考虑批量写（",[32,2369,2370],{},"saveAll","、JDBC 批处理）；",[28,2373,2374,2377],{},[46,2375,2376],{},"等待用户输入","：事务不能跨请求；",[28,2379,2380,2383],{},[46,2381,2382],{},"吞异常","（第 3 节已提过）。",[13,2385,2386,2387,2390],{},"原则是",[46,2388,2389],{},"事务尽量短","：进入事务前把外部数据准备好，事务里只做数据库读写。",[17,2392,2394],{"id":2393},"_11-容易踩的坑","11 容易踩的坑",[58,2396,2397,2410],{},[61,2398,2399],{},[64,2400,2401,2404,2407],{},[67,2402,2403],{},"现象",[67,2405,2406],{},"原因",[67,2408,2409],{},"处理",[74,2411,2412,2425,2438,2455,2469,2482,2499,2519,2537,2550,2561],{},[64,2413,2414,2419,2422],{},[79,2415,2416,2418],{},[32,2417,151],{}," 完全没生效",[79,2420,2421],{},"同类自调用、方法不是 public、对象不是容器管理的",[79,2423,2424],{},"把事务方法拆到另一个 Bean，或用编程式事务；检查修饰符",[64,2426,2427,2430,2433],{},[79,2428,2429],{},"抛了异常但数据没回滚",[79,2431,2432],{},"抛的是受检异常，或异常在方法内被 catch 掉",[79,2434,2435,2437],{},[32,2436,1470],{},"，或把异常抛出去",[64,2439,2440,2443,2449],{},[79,2441,2442],{},"内层方法失败，外层事务也被回滚",[79,2444,2445,2446,2448],{},"用了默认的 ",[32,2447,1100],{},"，内层加入的是同一个事务",[79,2450,2451,2452,2454],{},"需要互不影响就用 ",[32,2453,1114],{},"，并确认内层是外部调用",[64,2456,2457,2463,2466],{},[79,2458,2459,2460,2462],{},"用了 ",[32,2461,1114],{}," 之后偶发卡死",[79,2464,2465],{},"嵌套事务各占一条连接，连接池不够",[79,2467,2468],{},"检查连接池大小与事务嵌套层数，尽量避免多层嵌套",[64,2470,2471,2476,2479],{},[79,2472,2473,2475],{},[32,2474,1114],{}," 没起作用",[79,2477,2478],{},"内层方法是同类中的内部调用",[79,2480,2481],{},"内层方法移到另一个 Bean",[64,2483,2484,2487,2493],{},[79,2485,2486],{},"事务提交了但实体没写入",[79,2488,2489,2490,2492],{},"只有查询方法加了 ",[32,2491,406],{},"，或者根本没事务，靠自动提交",[79,2494,2495,2496,2498],{},"写方法显式 ",[32,2497,151],{},"（不带 readOnly）",[64,2500,2501,2507,2510],{},[79,2502,2503,2504,2506],{},"写方法上写了 ",[32,2505,883],{},"，偶尔报错",[79,2508,2509],{},"提供者在只读事务里拒绝了写操作",[79,2511,2512,2513,2515,2516,1416],{},"写方法覆盖为 ",[32,2514,151],{},"（默认 ",[32,2517,2518],{},"readOnly = false",[64,2520,2521,2524,2527],{},[79,2522,2523],{},"并发下库存扣成负数",[79,2525,2526],{},"读-判断-写之间没有保护",[79,2528,2529,2530,2533,2534,2536],{},"悲观锁（",[32,2531,2532],{},"@Lock(PESSIMISTIC_WRITE)","）或乐观锁（",[32,2535,1913],{}," + 重试）",[64,2538,2539,2544,2547],{},[79,2540,2541,2542],{},"优化后偶发 ",[32,2543,1828],{},[79,2545,2546],{},"乐观锁检测到并发修改",[79,2548,2549],{},"捕获并重试\u002F提示用户，别让它变成 500",[64,2551,2552,2555,2558],{},[79,2553,2554],{},"事务开着做远程调用，接口变慢导致连接耗尽",[79,2556,2557],{},"事务里做了 IO 或网络操作",[79,2559,2560],{},"把远程调用移到事务外，事务只包数据库操作",[64,2562,2563,2566,2569],{},[79,2564,2565],{},"事务里查出的关联在事务外访问报错",[79,2567,2568],{},"延迟加载需要活动事务或打开的会话",[79,2570,2571,2572,2575],{},"在事务内取够数据（",[32,2573,2574],{},"join fetch"," \u002F 投影），别把实体带出事务边界",[17,2577,2579],{"id":2578},"_12-小结","12 小结",[135,2581,2582,2588,2600,2609,2618,2625,2633,2636],{},[28,2583,2584,2587],{},[46,2585,2586],{},"事务边界放在 Service 层","，用接口定义业务能力，实现类声明事务；",[28,2589,2590,2591,2599],{},"常用组合：",[46,2592,2593,2594,884,2597],{},"类上 ",[32,2595,2596],{},"@Transactional(readOnly = true)",[32,2598,151],{},"；",[28,2601,2602,2604,2605,2608],{},[32,2603,151],{}," 靠 AOP 代理生效，",[46,2606,2607],{},"自调用、非 public、自建对象、吞异常","四种情况会静默失效；",[28,2610,2611,2612,2614,2615,2617],{},"传播行为先记住 ",[32,2613,1100],{},"（默认，共用事务）与 ",[32,2616,1114],{},"（独立事务），并留意它多占一条\n连接的风险；",[28,2619,2620,2621,2624],{},"隔离级别",[46,2622,2623],{},"够用就好","，不写就用数据库默认值；",[28,2626,2627,2630,2631,2599],{},[46,2628,2629],{},"默认只回滚运行时异常","，业务异常要 ",[32,2632,1444],{},[28,2634,2635],{},"并发竞争用悲观锁或乐观锁，前者适合强竞争，后者适合低冲突 + 可重试；",[28,2637,2638,2639,169],{},"事务要短：",[46,2640,2641],{},"别在事务里做远程调用、文件 IO 和用户交互",[13,2643,2644,2645,2648],{},"到这里，服务端的读写、页面、会话、事务都齐了。接下来是另一个方向：",[46,2646,2647],{},"把服务暴露成 REST API","，\n让前端框架、移动端、第三方都能用。下一篇从 HTTP 方法与状态码开始。",[2650,2651,2652],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}",{"title":176,"searchDepth":208,"depth":208,"links":2654},[2655,2656,2657,2658,2659,2660,2661,2662,2663,2667,2668,2669,2670],{"id":19,"depth":201,"text":20},{"id":52,"depth":201,"text":53},{"id":126,"depth":201,"text":127},{"id":891,"depth":201,"text":892},{"id":1068,"depth":201,"text":1069},{"id":1216,"depth":201,"text":1217},{"id":1364,"depth":201,"text":1365},{"id":1497,"depth":201,"text":1498},{"id":1581,"depth":201,"text":1582,"children":2664},[2665,2666],{"id":1589,"depth":208,"text":1590},{"id":1759,"depth":208,"text":1760},{"id":1917,"depth":201,"text":1918},{"id":2344,"depth":201,"text":2345},{"id":2393,"depth":201,"text":2394},{"id":2578,"depth":201,"text":2579},"学习",null,"2026-10-15","一组数据库操作要么全成功要么全失败，靠 @Transactional 怎么做到；传播行为、隔离级别、回滚规则怎么选，以及自调用失效、乐观锁悲观锁这些实战里的坑。","md",false,{},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-10",{"title":5,"description":2674},"Spring 学习笔记","posts\u002Fjavaspring\u002Fspring-notes-10",[2683,2684,2685],"Spring","事务","学习笔记","ZcaXyCye4Idx5d0JzkqbKQQ1z7ad2UZvj_xc2l4hEnc",[2688,2694,2699,2704,2709,2714,2719,2724,2729,2734,2739,2744,2749,2754,2759,2764,2769,2774,2779,2784,2789,2794,2799,2804,2809,2814,2819,2824,2829,2833,2837,2841,2845,2849,2853,2857,2861,2865,2866,2870,2874,2880,2884,2890,2894,2898,2902,2906,2910,2914,2918,2923,2927,2931,2936,2941,2947,2951,2956,2960],{"path":2689,"title":2690,"description":2691,"date":2692,"series":2693,"seriesOrder":522},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-28","软件分析与设计笔记 28：draw.io 作图手册：五张图的符号位置、落笔顺序与自检","不模拟软件界面，只讲清三件事：每个 UML 符号在 draw.io 里搜什么词、一张图该按什么顺序落笔、画完拿什么标准自查。覆盖用例图、活动图、顺序图、类图、界面草图五种图纸，末尾附「最容易画反的十二笔」总检表。","2026-11-19","软件分析与设计笔记",{"path":2695,"title":2696,"description":2697,"date":2698,"series":2693,"seriesOrder":510},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-27","软件分析与设计笔记 27：Paper 1 应试：从读案例到交卷的一次完整演算","拿一份完整的案例，从头到尾走一遍：怎么读案例列角色、怎么在四个小问之间分配时间、每一张图从哪一句话推出来、两张表怎么填、交卷前拿什么清单自查。附 C25 真题与样卷的题型分值对照。","2026-11-18",{"path":2700,"title":2701,"description":2702,"date":2703,"series":2693,"seriesOrder":499},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-26","软件分析与设计笔记 26：总结：把整条链路串起来","系列收尾：从需求到代码的完整链路回顾、每一环的产出与验收方式、六条贯穿全系列的原则，以及一份可以直接拿去做项目自查的清单。","2026-11-17",{"path":2705,"title":2706,"description":2707,"date":2708,"series":2693,"seriesOrder":482},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-25","软件分析与设计笔记 25：代码重构：坏味道与手法","重构不改变外部行为、只改善内部结构：四种常见坏味道与对应手法、为什么动手之前必须先有测试、什么时候该重构，以及重构和前面那些设计模型是什么关系。","2026-11-16",{"path":2710,"title":2711,"description":2712,"date":2713,"series":2693,"seriesOrder":477},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-24","软件分析与设计笔记 24：设计原则：内聚、耦合与多态","好设计怎么衡量：内聚与耦合分别在说什么、一次从「差设计」到「多态重设计」的完整对照、抽象类与接口的选择依据，以及复用为什么会从优势变成风险。","2026-11-15",{"path":2715,"title":2716,"description":2717,"date":2718,"series":2693,"seriesOrder":467},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-23","软件分析与设计笔记 23：设计建模：从对象模型到能实现的方案","分析模型能用但还不能直接开工：对象模型怎么映射到关系模型、继承映射的三种方案怎么选、数据访问层与 Data Broker 解决什么问题，以及三层架构与 MVC 的分工。","2026-11-14",{"path":2720,"title":2721,"description":2722,"date":2723,"series":2693,"seriesOrder":457},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-22","软件分析与设计笔记 22：分析建模：边界、实体与控制对象","不经过分析直接写代码会踩什么坑、三种分析对象各管一段、怎么用顺序图完成一次用例实现，以及从顺序图导出类的属性、操作与关系的方法。","2026-11-13",{"path":2725,"title":2726,"description":2727,"date":2728,"series":2693,"seriesOrder":445},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-21","软件分析与设计笔记 21：UML：类图与顺序图","为什么需要一种共同的画法：类符号三格怎么画、五种关系的符号与设计含义、每种关系落到 Java 是什么写法，顺序图的三要素怎么摆；以及这三种最容易找不到的符号在 draw.io 里各搜什么词。","2026-11-12",{"path":2730,"title":2731,"description":2732,"date":2733,"series":2693,"seriesOrder":440},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-20","软件分析与设计笔记 20：面向对象概念：对象、关系与多态","面向对象不是「用某个语言写代码」：对象与类的区别、封装真正在保护什么、五种关系（依赖、关联、聚合、组合、继承）各自何时用，以及多态为什么能减少条件判断。","2026-11-11",{"path":2735,"title":2736,"description":2737,"date":2738,"series":2693,"seriesOrder":422},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-19","软件分析与设计笔记 19：规范化与逻辑数据模型","一次改地址引发的数据不一致，讲清规范化的真正动机；UNF 到 3NF 每一步在解决什么问题，派生字段为什么不能存，优化为什么和规范化方向相反，以及逻辑数据模型在 3NF 之上补了什么。","2026-11-10",{"path":2740,"title":2741,"description":2742,"date":2743,"series":2693,"seriesOrder":394},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-18","软件分析与设计笔记 18：数据库分析：实体、关系与数据字典","为什么不能拿到需求就动手建表：实体与关系怎么判定、基数为什么必须由业务方确认、数据命名要遵守什么规则，以及数据字典四列里最容易被空着的那一列。","2026-11-09",{"path":2745,"title":2746,"description":2747,"date":2748,"series":2693,"seriesOrder":385},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-17","软件分析与设计笔记 17：用生成式 AI 做设计与开发：能用的场景与要防的坑","生成式 AI 在工作流里真正站得住的三个场景 —— 造示例数据、解释陌生代码、起草初稿；它为什么会复现社会偏见、为什么会编得像真的，以及哪些内容绝不能直接采用。","2026-11-08",{"path":2750,"title":2751,"description":2752,"date":2753,"series":2693,"seriesOrder":380},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-16","软件分析与设计笔记 16：从 persona 到 URS：一份交付物的完整骨架","把前面十几篇串成一份能交出去的东西：用户需求规格说明书的九个部分、每一章来自哪个环节、由谁确认，以及面向决策者的提案该讲清哪三件事。","2026-11-07",{"path":2755,"title":2756,"description":2757,"date":2758,"series":2693,"seriesOrder":372},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-15","软件分析与设计笔记 15：非功能需求：把「快、稳、安全」变成数字","为什么「系统要快」不是一条需求：非功能需求怎么量化，决定性能的用户量、交易量、数据量三张表怎么估，安全需求要写清什么，以及可靠性的「不可用」该怎么定义。","2026-11-06",{"path":2760,"title":2761,"description":2762,"date":2763,"series":2693,"seriesOrder":364},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-14","软件分析与设计笔记 14：软件测试：层级、方法与等价类划分","「我们测过了」这句话问题在哪：为什么静态评审和动态测试缺一不可，五个测试层级各自验证什么，白盒与黑盒的差别，以及用等价类划分和边界值把无限输入变成有限用例的方法。","2026-11-05",{"path":2765,"title":2766,"description":2767,"date":2768,"series":2693,"seriesOrder":356},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-13","软件分析与设计笔记 13：需求获取：五种调查方法与一场访谈","需求从哪里来：五种事实调查方法各自适合回答什么、代价是什么，访谈从准备到记录的全过程，问卷设计里最容易犯的错，以及访谈记录必须写下来的那几项信息。","2026-11-04",{"path":2770,"title":2771,"description":2772,"date":2773,"series":2693,"seriesOrder":351},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-12","软件分析与设计笔记 12：前端设计：把流程变成屏幕","界面设计不是「画得好看」：从活动图走到屏幕的三步，CRUD 任务流怎么当检查表，界面必须回答的三个问题，信息架构与五种导航类型，视觉一致性与留白，以及移动端与 Web 端的差别。","2026-11-03",{"path":2775,"title":2776,"description":2777,"date":2778,"series":2693,"seriesOrder":343},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-11","软件分析与设计笔记 11：Agentic AI 与业务流程自动化","AI agent 到底是什么：五项能力与四个核心组件，什么场景该用定时任务、什么该用程序、什么才值得用 agent，两种实现路径的差别，必须提前防住的四类失败模式，以及设计方法从提示词到编排的演进。","2026-11-02",{"path":2780,"title":2781,"description":2782,"date":2783,"series":2693,"seriesOrder":335},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-10","软件分析与设计笔记 10：精炼用例模型：include、extend 与泛化","三种精炼用例关系的判断标准：include 什么时候用、它和 extend 的区别到底在哪、参与者泛化与用例泛化各解决什么问题，以及精炼到什么程度就该停手。","2026-11-01",{"path":2785,"title":2786,"description":2787,"date":2788,"series":2693,"seriesOrder":327},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-09","软件分析与设计笔记 09：功能需求（二）：从流程到业务实体","活动图画完之后，怎么从流程里筛出真正的业务实体：三条判断标准、哪些名词其实不该建实体、属性从哪里来，以及领域模型和后面的数据模型分别负责什么。","2026-10-31",{"path":2790,"title":2791,"description":2792,"date":2793,"series":2693,"seriesOrder":319},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-08","软件分析与设计笔记 08：功能需求（一）：用例模型与活动图","从用户故事到用例的翻译过程：什么才算「好」的参与者，系统边界怎么划，用例描述必须写清哪四件事，活动图怎么把分支、并行与泳道画清楚，以及这两个图在 draw.io 里各用哪个形状、最容易画错哪一笔。","2026-10-30",{"path":2795,"title":2796,"description":2797,"date":2798,"series":2693,"seriesOrder":311},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-07","软件分析与设计笔记 07：用户故事：把需求写成可验收的一句话","用户故事的三段结构，怎么从用户画像和数据地图里把它提取出来，一条故事多大算合适、时序为什么也是需求的一部分，以及验收标准怎么写才算真的可验收。","2026-10-29",{"path":2800,"title":2801,"description":2802,"date":2803,"series":2693,"seriesOrder":259},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-06","软件分析与设计笔记 06：范围界定（数据侧）：用数据校验你的判断","访谈结论和数字对不上时该怎么办：分析的四个层次分别在回答什么问题、为什么不能跳级、数据准备与搜集怎么做成一张应用地图，以及数据最容易在哪几个地方骗你。","2026-10-28",{"path":2805,"title":2806,"description":2807,"date":2808,"series":2693,"seriesOrder":238},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-05","软件分析与设计笔记 05：范围界定（用户侧）：用户画像与客户旅程图","为什么「所有用户」是一个陷阱，项目边界该怎么划，用户画像要写哪五个要素、最容易写成什么废纸，客户旅程图怎么把阶段、想法和情绪画在同一张纸上，以及怎么从这两样东西一步步推到可验证的需求。","2026-10-27",{"path":2810,"title":2811,"description":2812,"date":2813,"series":2693,"seriesOrder":233},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-04","软件分析与设计笔记 04：接手陌生案例的第一步：三问与一张表","拿到一个完全陌生的业务案例，怎么从零开始不跑偏：只问 Who、What、Why 三个问题，把「问题—事实—假设—理由」记成一张四列表，说清为什么假设必须写明理由，以及带着问题进访谈为什么比空手去有效得多。","2026-10-26",{"path":2815,"title":2816,"description":2817,"date":2818,"series":2693,"seriesOrder":208},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-03","软件分析与设计笔记 03：Agile 与 Scrum：迭代、角色与产出物","从「三周做完交付，客户说这不是我要的」讲起，说清瀑布式与迭代式的根本差别，Scrum 的三个角色和三种产出物，一个 sprint 里四次固定活动各自在解决什么问题，以及敏捷里最常见的几个误解。","2026-10-25",{"path":2820,"title":2821,"description":2822,"date":2823,"series":2693,"seriesOrder":201},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-02","软件分析与设计笔记 02：设计思维、用户驱动与数据驱动","从「客服要一个能一次看完所有排期的界面」这个翻车案例讲起，说清设计思维到底是什么、用户驱动与数据驱动各自能看见什么又看不见什么、为什么真正的落点是把两者合起来的 data-informed design，以及访谈中怎么问出有用的信息。","2026-10-24",{"path":2825,"title":2826,"description":2827,"date":2828,"series":2693,"seriesOrder":183},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-01","软件分析与设计笔记 01：课程地图与从需求到设计的主干","这一篇不写具体方法，先把整条链路铺开：软件分析与设计到底要解决什么问题，从需求到设计分成哪两段，中间有哪十几个环节，一份完整的交付物里应该有什么，以及后面 27 篇笔记怎么排、新人该按什么顺序读。","2026-10-23",{"path":2830,"title":2831,"description":2832,"date":2673,"series":2680,"seriesOrder":183},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-01","Spring 学习笔记 01：IoC、依赖注入与第一个 Spring Boot 应用","从「手写 new 出来的三层结构」的痛点讲起，说清 IoC 容器、Bean 与依赖注入是怎么回事，再用 Maven 与 Spring Initializr 跑起第一个能返回 JSON 的应用。",{"path":2834,"title":2835,"description":2836,"date":2673,"series":2680,"seriesOrder":201},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-02","Spring 学习笔记 02：Spring Data JPA 基础——实体映射与 Repository","用 ORM 把类映射到表：@Entity、@Id、@GeneratedValue 怎么用，CrudRepository 三层接口怎么选，H2 与 MySQL 怎么配，以及 ddl-auto 到底该写哪个值。",{"path":2838,"title":2839,"description":2840,"date":2673,"series":2680,"seriesOrder":208},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-03","Spring 学习笔记 03：JPA 查询——派生方法、JPQL 与分页排序","Repository 自带的方法只够按主键查，这篇讲三种自己造查询的方式：方法名派生、@Query 写 JPQL、原生 SQL，以及 Pageable 分页排序和投影返回。",{"path":2842,"title":2843,"description":2844,"date":2673,"series":2680,"seriesOrder":233},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-04","Spring 学习笔记 04：JPA 关联映射与仓储测试","一对多、多对多怎么映射，谁负责维护外键，FetchType 与 CascadeType 该怎么选，顺带用 @DataJpaTest 与 AssertJ 把仓储层测起来。",{"path":2846,"title":2847,"description":2848,"date":2673,"series":2680,"seriesOrder":238},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-05","Spring 学习笔记 05：Spring MVC 与 Thymeleaf 入门","一次页面请求在 Spring MVC 里走了哪几步，@Controller 怎么把数据交给模板，@PathVariable 与 @RequestParam 怎么取参数，以及控制器为什么必须是无状态的。",{"path":2850,"title":2851,"description":2852,"date":2673,"series":2680,"seriesOrder":259},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-06","Spring 学习笔记 06：表单绑定与服务端校验","把表单字段绑到对象上，用 Bean Validation 注解做服务端校验，把错误信息显示回用户刚填的那一页，再补上自定义校验注解与全局异常处理。",{"path":2854,"title":2855,"description":2856,"date":2673,"series":2680,"seriesOrder":311},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-07","Spring 学习笔记 07：Thymeleaf 片段复用与国际化","用 th:fragment 与 th:insert\u002Fth:replace 把导航栏、页脚抽成片段，页面套用统一布局，再用 messages.properties 与 LocaleResolver 做中英文切换。",{"path":2858,"title":2859,"description":2860,"date":2673,"series":2680,"seriesOrder":319},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-08","Spring 学习笔记 08：HTTP Session 与会话状态管理","HTTP 无状态带来的问题，Cookie 与 Session 怎么分工，Spring 里读写会话的几种方式，以及会话固定、超时、多实例共享这些必须想清楚的事。",{"path":2862,"title":2863,"description":2864,"date":2673,"series":2680,"seriesOrder":327},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-09","Spring 学习笔记 09：拦截器 HandlerInterceptor","登录检查、权限判断、请求耗时统计这类横切逻辑该放哪：HandlerInterceptor 的三个回调各在什么时机执行，怎么注册与排除路径，以及它和过滤器的分工。",{"path":2678,"title":5,"description":2674,"date":2673,"series":2680,"seriesOrder":335},{"path":2867,"title":2868,"description":2869,"date":2673,"series":2680,"seriesOrder":343},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-11","Spring 学习笔记 11：REST API 基础——把 Service 暴露成接口","同一套 Service 怎么同时服务页面和客户端：@RestController 的路由与方法语义、状态码怎么给、DTO 怎么定，以及 MockMvc 端到端和切片测试的两种写法与各自的坑。",{"path":2871,"title":2872,"description":2873,"date":2673,"series":2680,"seriesOrder":351},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-12","Spring 学习笔记 12：REST 进阶——版本、更新、跨域与文档化","接口能跑通之后要处理的几件事：URL 与响应结构怎么定、版本怎么演进、更新怎么做到可安全重试、CORS 怎么配、HATEOAS 与 Spring Data REST 该不该用。",{"path":2875,"title":2876,"description":2877,"date":2878,"series":2879,"seriesOrder":233},"\u002Fposts\u002Fjava\u002Fjava-functionalprogramming-notes","Java 函数式编程笔记：Lambda、函数式接口与 Streams","学习 Java 函数式编程时的个人笔记：Lambda 表达式的五步简写与方法引用、常见函数式接口、Stream 流水线与惰性求值，以及创建流、中间操作、终止操作与收集器的用法。示例代码均为自写，运行结果来自本地实测。","2026-10-13","Java 学习笔记",{"path":2881,"title":2882,"description":2883,"date":2878,"series":2879,"seriesOrder":208},"\u002Fposts\u002Fjava\u002Fjava-generics-notes","Java 泛型笔记：泛型方法、有界类型与泛型类","学习 Java 泛型时的个人笔记：从重复的重载方法与多态尝试讲起，再到泛型方法与类型推断、有界泛型类型、Comparable 与 Number 边界，以及泛型类在集合中的用法。示例代码均为自写，运行结果来自本地实测。",{"path":2885,"title":2886,"description":2887,"date":2888,"series":2889,"seriesOrder":233},"\u002Fposts\u002Fmysql\u002Fmysql-learning-04","MySQL 学习笔记 04：数据操纵（增删改）","MySQL 学习笔记第四篇：INSERT 插入（含批量插入与 INSERT ... SELECT）、UPDATE 更新、DELETE 删除，以及各自的坑。","2026-10-10","SQL 学习笔记",{"path":2891,"title":2892,"description":2893,"date":2888,"series":2889,"seriesOrder":238},"\u002Fposts\u002Fmysql\u002Fmysql-learning-05","MySQL 学习笔记 05：DDL 概览","MySQL 学习笔记第五篇：数据定义语言概览——Schema、Table、View、Index、Constraints 这几类对象分别对应哪些 CREATE \u002F ALTER \u002F DROP 命令。",{"path":2895,"title":2896,"description":2897,"date":2888,"series":2889,"seriesOrder":259},"\u002Fposts\u002Fmysql\u002Fmysql-learning-06","MySQL 学习笔记 06：CREATE TABLE 与数据类型","MySQL 学习笔记第六篇：CREATE TABLE 的完整语法、常用数据类型的选型建议，以及外键的两种定义方式。",{"path":2899,"title":2900,"description":2901,"date":2888,"series":2889,"seriesOrder":311},"\u002Fposts\u002Fmysql\u002Fmysql-learning-07","MySQL 学习笔记 07：索引","MySQL 学习笔记第七篇：索引的作用与 B+ 树结构、单列与复合索引的创建、索引的代价与适用场景。",{"path":2903,"title":2904,"description":2905,"date":2888,"series":2889,"seriesOrder":319},"\u002Fposts\u002Fmysql\u002Fmysql-learning-08","MySQL 学习笔记 08：DROP 与 ALTER","MySQL 学习笔记第八篇：DROP 删除数据库对象、ALTER 修改表结构，以及常见的坑。",{"path":2907,"title":2908,"description":2909,"date":2888,"series":2889,"seriesOrder":327},"\u002Fposts\u002Fmysql\u002Fmysql-learning-09","MySQL 学习笔记 09：约束与数据完整性","MySQL 学习笔记第九篇：SQL 约束——NOT NULL、CHECK、PRIMARY KEY、FOREIGN KEY 等如何保证数据完整性，以及它们的对比。",{"path":2911,"title":2912,"description":2913,"date":2888,"series":2889,"seriesOrder":335},"\u002Fposts\u002Fmysql\u002Fmysql-learning-10","MySQL 学习笔记 10：视图","MySQL 学习笔记第十篇：视图（Views）的定义与特性、和表的对比、创建与使用，以及可更新视图的限制。",{"path":2915,"title":2916,"description":2917,"date":2888,"series":2889,"seriesOrder":343},"\u002Fposts\u002Fmysql\u002Fmysql-learning-11","MySQL 学习笔记 11：存储过程","MySQL 学习笔记第十一篇：存储过程（Stored Procedures）的概念、创建与调用、带参数的写法，以及优缺点。",{"path":2919,"title":2920,"description":2921,"date":2922,"series":2889,"seriesOrder":183},"\u002Fposts\u002Fmysql\u002Fmysql-learning-01","MySQL 学习笔记 01：MySQL 基础知识","MySQL 学习笔记第一篇：MySQL Server 与 Workbench 的安装，数据库与 DBMS 的基本概念，关系模型、ACID 事务、常见数据类型，以及后续的学习路线。","2026-10-09",{"path":2924,"title":2925,"description":2926,"date":2922,"series":2889,"seriesOrder":201},"\u002Fposts\u002Fmysql\u002Fmysql-learning-02","MySQL 学习笔记 02：SELECT 查询与过滤","MySQL 学习笔记第二篇：SELECT 的基本语法与逻辑执行顺序，WHERE 过滤（比较、LIKE、IN、BETWEEN、NULL）、各种 JOIN、UNION 与子查询。",{"path":2928,"title":2929,"description":2930,"date":2922,"series":2889,"seriesOrder":208},"\u002Fposts\u002Fmysql\u002Fmysql-learning-03","MySQL 学习笔记 03：聚合、分组与输出控制","MySQL 学习笔记第三篇：COUNT\u002FSUM\u002FAVG 等聚合函数、GROUP BY 分组、HAVING 过滤、ORDER BY 与 LIMIT，以及一条 SELECT 的完整逻辑执行顺序。",{"path":2932,"title":2933,"description":2934,"date":2935,"series":2879,"seriesOrder":201},"\u002Fposts\u002Fjava\u002Fjava-oop-notes","Java 面向对象笔记：类、继承、多态与异常","学习 Java 面向对象编程时的个人笔记：类与对象、封装与访问修饰符、继承与多态、集合、抽象类与接口、异常处理。示例代码均为自写，运行结果来自本地实测。","2026-10-06",{"path":2937,"title":2938,"description":2939,"date":2940,"series":2879,"seriesOrder":183},"\u002Fposts\u002Fjava\u002Fjava-basics-notes","Java 基础语法笔记：从环境安装到方法与数组","学习 Java 基础语法时的个人笔记：JDK 安装、输入输出、变量与运算符、条件与循环、字符串、数组与方法。示例代码均为自写，运行结果来自本地实测。","2026-10-05",{"path":2942,"title":2943,"description":2944,"date":2945,"series":2946,"seriesOrder":183},"\u002Fposts\u002Fgit\u002Fgit-install-and-basics","Git 安装与基本使用","从装好 Git 到跑通日常提交流程，把最常用的几条命令和它们背后的三个区域讲清楚。","2026-10-04","Git 入门",{"path":2948,"title":2949,"description":2950,"date":2945,"series":2946,"seriesOrder":201},"\u002Fposts\u002Fgit\u002Flocal-remote-repository","本地远程仓库的部署","用自己的机器当 Git 服务器：建一个裸仓库、走 SSH 连上去，把本地提交推上去。",{"path":2952,"title":2953,"description":2954,"date":2945,"series":2955,"seriesOrder":183},"\u002Fposts\u002Fguide\u002Fguide-file-layout","文件都放在哪","文章、图片、封面分别放在仓库的什么位置，哪些目录可以放手改、哪些碰了会出问题。","博客使用指南",{"path":2957,"title":2958,"description":2959,"date":2945,"series":2955,"seriesOrder":201},"\u002Fposts\u002Fguide\u002Fguide-frontmatter","frontmatter 每个字段是什么意思","文章开头那段被横线包住的字段，逐个说明作用、默认行为和写错的后果。",{"path":2961,"title":2962,"description":2963,"date":2945,"series":2955,"seriesOrder":208},"\u002Fposts\u002Fguide\u002Fguide-markdown","正文怎么写 —— Markdown 与排版效果","标题、链接、图片、代码、表格分别怎么写，以及本站对它们做了哪些自定义处理。",1793268186002]