ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java租房管理系统实战:从数据库设计到并发预约与事务控制

Java租房管理系统实战:从数据库设计到并发预约与事务控制 1. 租房管理系统确实是 Java 系的最佳练习题但前提是别只做 CRUD租房管理系统在毕业设计和日常项目里都算得上特别眼熟的那类业务系统需求一句话就能讲明白无非是房源、租客、合同、账单、权限。但真正动手做的时候才会发现越是这样需求明确的系统越考验工程能力。前前后后折腾了大半个学期我把一个基于 Java 的租房管理系统从零搭到了能演示、能答辩、能写成论文的程度也在这个过程中想明白了一个道理——框架用得多不如用得对功能堆得全不如有几处能经得起追问的特色。为什么选择 Java 技术栈来做这个系统理由很现实。第一Java 生态里开源轮子齐全Spring Boot 做后端几乎成了业务系统的默认选项社区资料多遇到问题检索效率高。第二Java 的类型系统和异常机制虽然在写小工具时显得繁琐但在租房这种牵扯合同、账单、状态流转的场景里反而是一种保护——编译器能挡掉一部分低级错误事务和异常处理能把业务一致性这条线守得更稳。第三论文答辩和面试时的认可度高技术选型讲出来大家都能听懂不会因为用了冷门框架被额外追问。有一点我觉得值得专门说一下这类系统不要一上来就上微服务。我见过不少同学搭个租房管理系统就准备拆成用户服务、房源服务、订单服务再引入 Nacos、OpenFeign 那一套。以这种业务规模来说微服务除了给演示环境增加几个必须同时启动的进程之外几乎没有正向收益。单体应用 模块化分包已经是够用的复杂度分布式事务和跨服务调用反而是给自己埋坑。论文里写单体应用架构完全没问题把单体内部的职责边界讲清楚比堆一堆中间件更能体现设计能力。我最终选型的组合是Spring Boot MyBatis MySQL前端用普通的 HTML JavaScript 页面配合管理后台的表格和表单。没有用前后端分离框架一方面是重心放在后端业务逻辑上另一方面是论文需要的系统截图和流程演示并不依赖复杂前端。缓存我引入了 Redis但只用在验证码和热点房源列表上不把核心业务强依赖缓存。很多人把这个系统做成纯 CRUD页面一打开全是增删改查答辩的时候老师问一句你的系统特色在哪就只剩下沉默。所以我在设计之初就给自己定了几个目标房源要有状态流转合同要有生命周期看房预约要能处理并发冲突租期到期要能自动提醒。这些目标全部围绕租房业务里真实存在的复杂场景展开而不是为了炫技术硬塞功能。2. 先把需求梳理清楚租客、房东、管理员这三类人的诉求完全不同做系统最忌讳的需求分析方式是打开数据库设计工具直接开始画表。我第一版系统就是这么干的结果做到一半发现业务逻辑根本串不起来房源下架之后还能被预约合同签完之后房子状态没有联动变更管理员想查的数据散在五六张表里。后来老老实实回到需求分析把三类用户的使用场景逐个列出来再反推数据结构和接口设计。租客侧的核心诉求是找房—看房—签约—履约这条主链路。找房阶段需要有条件筛选和关键字搜索最好能按价格、面积、区域排序看房阶段要预约具体时间并且希望预约能收到明确的成功或失败反馈签约阶段要能看到押金、租金、租期起止时间这些关键条款履约阶段要管理每月的账单记录和缴费状态。租客的操作路径天然是线性的环节之间有强依赖。房东侧的核心诉求不太一样。房东更关心房源发布是否方便、房屋信息的审核进度、看房预约是否冲突、合同到期前有没有提醒。一个房东名下可能有几十套房靠 Excel 记录每套房子的租期和账单是极其痛苦的事情这也是系统存在的价值。我把房东端的核心流程设计为发布—审核—上架—接收预约—签约—收租—退租每个环节都要有清晰的状态反馈。管理员的诉求就更偏数据治理层面了。管理员要审核房源真实性要处理租客和房东之间的纠纷工单需要查看整个平台的成交数据、房源空置率、租金分布等统计信息。这块我建议作为系统的一个亮点模块来做因为很多同题材系统根本没有统计功能论文需求分析里写了数据统计实现里却没有对应页面这属于明显的逻辑缺口。三类用户的用例整理出来之后优先级就非常清楚了。我用了下面的顺序来排开发节奏基础账号体系注册、登录、权限校验这是所有功能的前提。房源管理发布、审核、上下架这是业务的核心对象。合同与账单签约、退租、生成账单、缴费记录这是业务闭环的关键。看房预约与提醒这是做出特色的抓手能讲出并发和定时任务的场景。数据统计与后台报表这是提升系统完成度的模块也是论文里数据展示的素材。业务规则方面也要提前定清楚否则实现过程中会反复改。我定下的三个核心规则是房源状态必须和合同状态联动同一时间同一套房只能有一个预约被确认账单金额和合同金额保持一致。这三条规则在后面写代码时成了事务设计和并发处理的主线。3. 数据表设计决定系统成败我的表结构和几个容易踩烂的字段坑需求梳理完下一步就是数据库设计。租房管理系统的表不算多但每张表之间的关联关系直接决定了代码好不好写。我最终维护了这么几张核心表用户表、房源表、房源审核记录表、看房预约表、租赁合同表、账单表、合同提醒记录表。先看我最想分享的房源表结构。这里我踩过的第一个坑就是金额字段。第一版我把 rent_price 定义成了 float 类型后来在计算押一付三、生成账单时频繁出现 0.30000000000000004 这种精度问题整得人头皮发麻。做租务系统凡是涉及钱的地方一律用 decimalJava 侧对应 BigDecimal不要图省事用 double。CREATE TABLE house ( id bigint NOT NULL AUTO_INCREMENT, title varchar(100) NOT NULL, address varchar(255) NOT NULL, area decimal(10,2) DEFAULT NULL, rent_price decimal(10,2) NOT NULL, status tinyint NOT NULL DEFAULT 1 COMMENT 1-空置 2-待看房 3-已签约 4-已下架, landlord_id bigint NOT NULL, version int NOT NULL DEFAULT 0, deleted tinyint NOT NULL DEFAULT 0, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_landlord (landlord_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有几个字段是反复思考后加的。landlord_id关联用户表用于隔离不同房东的数据权限version是乐观锁版本号后续处理并发更新时用deleted是逻辑删除标记不推荐直接物理删记录因为合同和账单都外联着房源信息物理删除会导致历史数据链断裂。租务系统里另一个容易出现的问题是状态字段用字符串而不是数字。有些同学喜欢把状态直接存成已租空置待看房看着直观但后续写查询条件、做状态机判断时非常痛苦还得担心字符集和拼写不一致。用 tinyint 存状态码用代码里的枚举常量去映射查询和统计都干净得多。租赁合同表是另一个重点。我设计合同表时特意加上了lease_start_date、lease_end_date、monthly_rent、deposit、status这几个关键字段。合同状态我定义成 0-待生效、1-履行中、2-已到期、3-已退租、4-已违约。这样定义之后租客违约在代码里就是一次简单的状态更新加上违约原因字段就可以记录审计信息论文的数据字典部分也能直接复用。账单表和合同表的关系是典型的主表—子表。一份合同对应多个月度账单账单里要冗余保存contract_id和amount这样查询某个合同是否还欠费的时候就无需回查合同表。这里也提醒一句账单和合同金额的一致性靠应用层保证不要指望数据库触发器代码里的事务逻辑才是主力。设计表结构时考虑查询索引也很重要。租房列表最常做的查询是关键字搜标题/地址 价格区间 状态过滤。一开始我没有给 status 建索引测试数据只有几百条时感受不到问题后来导入了一万多条模拟数据列表页明显变慢。后来加了idx_status和idx_landlord查询基本是毫秒级返回。论文的性能测试部分也顺便有了素材。4. 核心模块实现Spring Boot MyBatis 的分层代码应该怎么写技术选型定了表结构设计完了就到了动手写代码的阶段。我这套系统的代码目录是按照经典的三层结构来组织的controller 层接请求、封装参数校验service 层写业务规则和事务控制mapper 层通过 MyBatis 操作数据库。实体类对应数据表DTO 负责接收前端传入的数据VO 负责向前端输出数据。先看 pom.xml 里的关键依赖。这里特别要注意 spring-boot-starter-web 和 mybatis-spring-boot-starter 的版本兼容问题我遇到过 Spring Boot 3 和旧版 mybatis-starter 不兼容的情况后来统一用 Spring Boot 2.7.x mybatis-spring-boot-starter 2.3.2稳定很多。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependencyMyBatis 的 mapper XML 中最常用的就是多条件组合查询。租房列表页的筛选项非常多关键字、最低价、最高价、户型、朝向、状态。如果每个条件都写一个独立 SQL代码冗余到没法看。用where和if标签动态拼条件是最优雅的解法。select idselectHouseList resultTypecom.example.rent.entity.House SELECT id, title, address, area, rent_price, status, landlord_id, create_time FROM house where if testkeyword ! null and keyword ! AND (title LIKE CONCAT(%, #{keyword}, %) OR address LIKE CONCAT(%, #{keyword}, %)) /if if testminPrice ! null AND rent_price gt; #{minPrice} /if if testmaxPrice ! null AND rent_price lt; #{maxPrice} /if if teststatus ! null AND status #{status} /if AND deleted 0 /where ORDER BY create_time DESC /select写好这个 XML 后对应的 Service 层就要处理一个很容易被忽略的问题keyword 为空字符串怎么办。如果你在 XML 里判断if testkeyword ! null and keyword ! 空字符串不会进入条件这是对的。但前端如果传进来的是带空格的目标值比如 朝阳 就会出现搜不到任何结果的情况。我的处理方式是在 Service 层统一做 trim把首尾空格去掉再传入 mapper。业务代码的核心是签约流程。签约不是一个简单的 insert它牵动房源状态、合同记录、账单记录三张表必须保证要么全部成功要么全部回滚。这是整个系统中最核心的事务场景。Transactional(rollbackFor Exception.class) public Long signContract(SignContractDTO dto) { boolean changed changeHouseStatus(dto.getHouseId(), HouseStatus.ACTIVE, HouseStatus.RENTED); if (!changed) { throw new BusinessException(房源状态已变化请刷新后重试); } LeaseContract contract buildContract(dto); contractMapper.insert(contract); PaymentRecord firstPayment buildFirstPayment(contract); paymentMapper.insert(firstPayment); return contract.getId(); }Transactional(rollbackFor Exception.class)这一段值得展开讲讲。Spring 默认只在遇到 RuntimeException 和 Error 时才回滚事务如果signContract里抛出自定义的BusinessException通常继承 RuntimeException那是可以触发回滚的。但如果你在 service 里用 try-catch 把异常吞掉或者转换成返回结果后再 throw 一个普通 Exception事务就不动了。我在联调阶段就遇到过一次合同插入失败但房源状态已经改成已租整个系统的数据瞬间变得没法看。后来统一约定所有业务异常都直接向调用层抛出绝不捕获后静默处理。分层代码的一个容易混淆的地方是实体类、DTO、VO 的职责。有人直接把 Entity 返回给前端这样做一旦表结构改动前端接口也跟着变耦合太重。我的做法是前端提交的数据封装成 DTO数据库查询出来的表记录映射到 Entity输出给前端的组装成 VO。虽然多写一点转换代码但后期接前端联调时省心很多。5. 让系统称得上特色的几个功能看房预约、合同状态机、租期提醒如果一个租房管理系统的所有页面只是把数据表格搬来搬去那它的最终命运就是在答辩现场被老师一句话问住。想要让论文和项目都有亮点必须有几个能讲清楚我是怎么解决真实问题的功能。我重点做了三个看房预约的防冲突处理、合同状态机、到期自动提醒。先说看房预约。租客提交预约时最怕出现的情况是同一个时段被两三个人同时约上。最直接的解法是在数据库层面加唯一约束但看房预约的时间维度不是某个具体时间点而是时间段比如上午 10 点到 11 点就不能只靠唯一索引硬撑。我的方案是预约时先把房源状态从空置改成待看房这个变更必须是一个条件更新只有当前状态还是空置时才允许改成待看房。Transactional(rollbackFor Exception.class) public void makeAppointment(AppointmentDTO dto) { int rows houseMapper.updateStatusByFromStatus(dto.getHouseId(), HouseStatus.IDLE, HouseStatus.APPOINTED); if (rows ! 1) { throw new BusinessException(该房源刚刚被别人预约了请重新选择时段); } appointmentMapper.insert(dto); }条件更新的关键在 SQL 的 where 句子里UPDATE house SET status #{toStatus}, update_time NOW() WHERE id #{houseId} AND status #{fromStatus} AND deleted 0两个用户同时提交预约InnoDB 会通过行锁让这两个更新串行执行第一个更新成功受影响行数是 1第二个更新执行时 status 已经不是原来的值受影响行数是 0。这个方案不需要悲观锁不需要 Redis直接在数据库层面解决了并发冲突实现简单答辩也好解释。这种做法比先查再改安全得多因为先查再改在并发下一定会有中间态窗口。再讲合同状态机。一个合同从创建到结束会经历不同状态这个状态迁移不能是随意跳转的。我把合同状态定义为待生效、履行中、已到期、已退租、已违约。可允许的迁移路径是待生效→履行中、履行中→已到期、履行中→已违约、履行中→已退租。Service 层提供一个统一的changeStatus方法每次状态迁移都校验当前状态是否允许迁移不允许就抛异常。这样做的好处是无论哪个接口触发状态变更都走同一套规则不会出现某个接口绕过校验的情况。到期提醒用 Spring 的Scheduled定时任务实现。每天早上 9 点扫描未来 7 天到期的合同给房东推送提醒消息。为了防止同一份合同被提醒两次表里专门加了remind_status字段只在等于 0 时发送提醒并更新为 1。Scheduled(cron 0 0 9 * * ?) Transactional(rollbackFor Exception.class) public void remindExpiringContracts() { ListLeaseContract list leaseContractMapper.listExpiringInDays(7); for (LeaseContract contract : list) { if (contract.getRemindStatus() 0) { remindService.pushToLandlord(contract); leaseContractMapper.markReminded(contract.getId()); } } }定时任务引出了一个必须提前想到的问题如果应用部署了多个实例到点会不会重复发送提醒我在本地测试时没感觉因为只有一个实例。但论文里如果要谈高可用就不能回避这个问题。最简单的办法是引入分布式锁比如基于 Redis 的 setnx 锁让每个任务在同一时刻只有一个实例执行。虽然我这套系统最终部署还是单机但我在论文里把这个扩展方案作为未来优化方向写进去了答辩时老师问了也不慌。房源排序是我额外做的一个小功能。列表页默认按发布时间倒序但也支持按租金价格排序。这里如果直接在 SQL 的 ORDER BY 后面拼接前端传入的字段名会有 SQL 注入风险。我用白名单方式处理前端传price_asc、price_desc、default后端用 switch 映射成固定的 ORDER BY 子句剩下的参数一律不处理。6. 我在并发预约和事务回滚上翻的车以及最终的解法前面把方案讲得比较顺实际上开发过程并没有这么轻松这个章节里最值得记录的是我真实踩过的两个坑一个并发逻辑漏洞一个事务失效事故。第一个坑出现在预约功能第一版。当时我偷懒先执行 select 查询房源状态判断是空置再执行 update。逻辑写出来自己看着没问题用单线程测试也全部通过。但后来我写了个 JMeter 脚本模拟 50 个租客同时预约同一套房子发现成功预约的人数超过预期的几个——原因就是两步操作之间不是一个原子操作。比如两个请求都查询到了空置状态然后 A 先更新成功B 紧接着也更新成功因为 B 的 update 并没有校验我查到的还是不是空置。这个问题的根源就是经典的查改分离。解法就是前面的条件更新把状态校验和状态变更合并成一条 SQL让数据库行锁帮我们保证原子性。第二个坑是事务失效。当时我在 service 内部调用了同一个类的另一个方法想着外部方法上已经加了Transactional内部方法应该也在同一个事务里。结果发现数据部分成功、部分失败去查资料才知道这是 Spring 事务的一个经典陷阱同类内部调用不走代理Transactional注解没有生效。解决方式有三种把事务方法拆到另一个 service 类里再调用、把目标方法抽到接口实现里让别人调用、或者用TransactionTemplate手动管理事务。我后来选择了把核心事务操作抽到独立的ContractTransactionService里让业务逻辑代码和事务边界分离得更清晰。还有一个和事务无关但很影响体验的问题MyBatis 的#{}和${}的使用。动态 SQL 里如果写错把${}用在 where 条件上轻则性能问题重则 SQL 注入。我的经验是所有的参数值一律用#{}只有表名、字段名这种无法用占位符的场景才允许用${}并且必须用白名单校验。我在房源排序功能里就是这么处理的字段名走 switch 映射绝不直接使用用户输入。做完了这些处理之后我把系统导入了一万条模拟数据做了一次简单性能测试。不带条件的分页查询耗时在 20 毫秒以内带关键字和价格区间的组合查询在 50 毫秒左右并发预约测试中系统没有出现状态不一致的现象。这个结果放在答辩 PPT 的性能测试小节里说服力不弱因为测试数据是真实的 JMeter 截图不是凭感觉写的性能良好。7. 从系统到论文图表安排与答辩现场的常见追问项目做得差不多之后写论文又是一个需要认真对待的活儿。很多人的论文结构和系统实现脱节老师一看就知道是系统做完了再硬凑文字。我的经验是让论文的逻辑跟随项目开发的推进节奏每一个论文章节都有实际的产出物支撑。论文的章节我推荐这种组织方式绪论里写背景和意义以及国内外研究现状这一部分主要靠文献调研系统分析部分放需求分析和用例图系统设计部分放总体架构、功能模块划分和数据表设计系统实现部分按模块逐个讲实现思路、关键代码和页面截图测试部分写功能测试和性能测试的过程与结果。这套结构与高校软件工程类论文的习惯吻合老师看着熟悉不至于在格式上挑太多毛病。图表是论文里非常加分的部分。用例图描述三类用户的操作行为ER 图展示核心表之间的关系系统架构图表达Spring Boot MyBatis MySQL 前端页面的整体结构时序图可以挑看房预约和签约两个核心流程来画。画图工具有很多我用的 ProcessOn导出成高清图片放到论文里效果很好。特别是 ER 图直接决定老师对你数据库设计能力的印象值得认真打磨把每张表的字段和表间关系都画清楚。答辩现场老师问的问题通常很聚焦。我总结了几个高频问题这里一并分享为什么选择 Spring Boot MyBatis不选 SSH 或者 MyBatis-Plus回答重点是 Spring Boot 简化配置、内嵌 Tomcat、生态成熟MyBatis 对复杂查询可控性强方便写动态 SQL 和优化性能。如果要提 MyBatis-Plus就说单表 CRUD 用起来快但自定义复杂查询我还是用原生 XML 更可控。看房预约怎么处理并发冲突直接讲条件更新的思路重点说把状态校验和更新合并为一条 SQL如果能画一个简单的状态变化过程给老师看基本就稳了。合同和账单的数据一致性如何保证讲Transactional事务边界讲异常自动回滚讲业务异常必须抛出而不能吞掉。到期提醒的定时任务有没有重复执行风险先承认单机部署没有这个问题再讲多实例部署时需要分布式锁展示你考虑过延展场景。你的系统特色到底在哪这个问题如果前面的功能都做了很自然地就能展开状态机让合同流转可控、条件更新解决预约冲突、定时任务实现到期提醒、统计报表给管理员决策支持。关于系统演示有一点个人经验不要只准备一条顺利路径。答辩演示时最怕现场数据状态不对临时点不出效果。我的做法是准备了两套演示数据一套是刚初始化完成的空状态方便展示注册、登录、发布房源另一套是带有完整合同链路的模拟数据方便直接演示签约、账单、状态流转。两套数据之间用不同账号切换演示时完全不会卡壳。写到这里这套系统的核心内容基本都记录完了。做这类项目最大的收获不在于把功能跑通而在于把用户需求→数据模型→业务规则→代码实现→论文表达这条链路走通。中间踩的那些坑比如金额精度、事务失效、并发冲突、动态 SQL 注入单独拿出来每个都是 Java 面试八股里常问的点现在它们在我心里都变成了实实在在的经验。在线更新系统的后续计划我还有几个想法比如把看房预约改为用户可自行取消、增加房源收藏评价功能、把统计报表做成基于 ECharts 的可视化图表。如果时间允许下一次迭代我会优先把合同的电子签章和在线支付接入进来让系统的业务闭环再完整一些。
返回列表