ARTICLE DETAIL

资讯详情

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

JSP+Servlet搭建活动管理系统:分层、事务与并发控制实战

JSP+Servlet搭建活动管理系统:分层、事务与并发控制实战 简介一套面向 Java Web 学习与课程设计场景的活动管理系统项目前后台分离覆盖管理员与普通用户两种角色。管理员登录后可完成活动管理、活动类型管理、报名管理与游客管理普通用户登录后可分类查看活动、查看详情、提交报名、确认报名并查看个人报名记录。项目采用 JSPServletMySQLBootstrap 技术组合核心逻辑清晰适合用来理解 Servlet 请求处理、JDBC 数据访问、Session 用户状态以及前端页面交互。资源包共 214 个文件约 10.56MB其中包括 55 个 Java 源文件与对应 class、27 个 JSP 页面、数据库 SQL 脚本、Bootstrap/jQuery/CSS/JS 等前端资源以及项目运行所需的 jar 依赖与配置文件导入 IDE 配置好 Tomcat 和 MySQL 即可启动。已有 564 人学习下载。通过该项目可快速掌握前后台权限控制、活动报名流程设计、分类检索实现是一份能直接用于实训、答辩或二次开发的完整参考。1. 活动管理系统这个Java老四样组合为什么还在被反复用很多刚入行的同学一听到 JSPServlet第一反应是“这都老掉牙了”。但接手活动管理系统这类以增删改查和报名流程为主的业务我反而愿意先用这套组合。它没有 Spring 全家桶的 Bean 装配黑匣子请求从 JSP 到 Servlet 再到数据库的路径是透明的配一个 Tomcat 和一个 MySQL 就能跑维护成本低到小团队可以一个人扛。这个标题里的活动管理系统典型需求就是活动发布、列表展示、用户报名、后台审核外加一个简单的权限区分。适合两类人一是需要简历项目且面试时能把每一行代码讲明白的 Java 初学者二是正在维护老后台、想快速定位改动的在职工程师。下面按我实际搭建的顺序把项目结构、核心代码、事务边界、常见踩坑一次讲透。2. 技术选型与项目骨架JSPServletBootstrapMySQL各管哪一块2.1 分层思路别把Servlet当成业务层一个传统的 Java Web 项目最容易翻车的地方不是代码写法而是分层乱了有人把 SQL 直接写在 Servlet 里服务一多就变成一坨互不调用的“上帝类”。我做活动管理系统时坚持一个原则Servlet 只做路由和参数组装业务规则放到 ServiceJDBC 只出现在 DAO。这样 JSP 负责渲染MySQL 负责存储Bootstrap 负责让页面不至于太丑。常见做法是先建一个 Maven 工程目录如下activity-system/ ├── pom.xml ├── src/main/java/com/example/activity/ │ ├── controller/ActivityServlet.java │ ├── service/ActivityService.java │ ├── dao/ActivityDao.java │ ├── model/Activity.java │ └── util/DBUtil.java ├── src/main/resources/db.sql └── src/main/webapp/ ├── WEB-INF/web.xml ├── css/bootstrap.min.css ├── js/bootstrap.bundle.min.js ├── login.jsp └── activity/ ├── list.jsp └── edit.jsp这里每个包名不是摆设controller里的 Servlet 接收请求把参数转换成对象service处理“报名人数有没有满”“能不能删除”这类业务规则dao只负责和 MySQL 对话返回结果集或受影响行数。为什么不在 Servlet 里直接conn.createStatement()因为一个页面请求往往要串多个动作比如报名时要“查活动 查已报名 插入报名记录”如果这些动作分散在 Servlet事务就没法统一管理。分层清晰以后每个方法都能单独测面试也能讲出逻辑。Maven 依赖里最关键的是 Servlet API 和 MySQL 驱动dependencies dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency /dependenciesscopeprovided表示 Tomcat 已经自带 Servlet 相关类打包成 war 时不需要把 Tomcat 的库塞进去如果你用的 Tomcat 10 及以上javax.servlet要换成jakarta.servlet包名和坐标都变了这个坑后面单独说。MySQL 驱动的版本最好和数据库大版本对齐MySQL 8 用 8.x 驱动基本不会出兼容性问题。2.2 建表和JDBC连接先让数据活起来活动管理系统至少要三张表活动表、用户表、报名表。活动表负责维护基础信息用户表存账号和角色报名表记录谁报了哪个活动。建表 SQL 我一般写成db.sql放到项目里当存档新环境一执行就能跑CREATE TABLE activity ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(100) NOT NULL, content TEXT, location VARCHAR(100), start_time DATETIME NOT NULL, quota INT DEFAULT 0, signed_up INT DEFAULT 0, status TINYINT DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_start_time (start_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, role TINYINT DEFAULT 0 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE signup ( id INT PRIMARY KEY AUTO_INCREMENT, activity_id INT NOT NULL, user_id INT NOT NULL, signup_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_activity_user (activity_id, user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;三张表都用了 InnoDB因为后面的报名流程需要事务user里的UNIQUE约束防止用户名重复signup里的uk_activity_user是唯一索引保证同一个用户不能对同一个活动报两次。idx_start_time是给活动列表页的 MySQL 排序准备的按start_time DESC查即将开始的活动有索引比全表扫描快得多。如果你还在看 MySQL 安装配置教程记得装完把默认字符集改成 utf8mb4否则后面中文乱码还要返工。JDBC 工具类不需要写得复杂但连接参数要一次配对否则经常报错public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/activity_db ?useUnicodetruecharacterEncodingutf8 useSSLfalseserverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD 你的密码; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }useSSLfalse是为了避免本机开发时出现 SSL 握手失败serverTimezoneAsia/Shanghai解决 MySQL 8 的时区报错characterEncodingutf8保证中文能正常读写。这个类只适合项目初期线上并发上来以后我一般会换 HikariCP 连接池但思路是一样的提供一个统一的getConnection()所有 DAO 都从这儿拿连接。2.3 Bootstrap在JSP里落地页面骨架与组件用法Bootstrap 在这个项目里不是重点但没有它活动列表页会回到原始网页风格。我的习惯是把bootstrap.min.css和bootstrap.bundle.min.js下载到webapp/css和webapp/js目录而不是直接引用 CDN。活动管理系统经常要部署在内网外网一断样式就没了本地文件才是真后悔药。每个 JSP 页面头部固定用这套模板% page contentTypetext/html;charsetUTF-8 languagejava % !DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1 link relstylesheet href${pageContext.request.contextPath}/css/bootstrap.min.css title活动管理系统/title /head body classbg-light !-- 内容 -- /body /html关键在${pageContext.request.contextPath}。如果你直接在href里写css/bootstrap.min.css当 URL 是/activity-system/activity/list时浏览器会去/activity-system/activity/css/找文件结果 404。加上contextPath就变成了/activity-system/css/bootstrap.min.css不管你 Servlet 转发到哪一层路径都不会错。页面布局用栅格分列活动卡片用card列表用table表单用form-control这套组合已经足够覆盖活动管理 80% 的界面需求。3. 核心功能落地活动发布的增删改查与报名流程3.1 活动管理模块Servlet接收请求Service处理事务活动管理最基础的是发布、编辑、删除、列表。我会用一个ActivityServlet承担所有活动相关请求URL 设计成/activity/list、/activity/detail、/activity/edit、/activity/delete避免用通配符把自己绕晕。WebServlet(/activity/*) public class ActivityServlet extends HttpServlet { private ActivityService activityService new ActivityService(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String path req.getPathInfo(); if (/list.equals(path)) { req.setAttribute(activityList, activityService.listActivities()); req.getRequestDispatcher(/activity/list.jsp).forward(req, resp); } else if (/detail.equals(path)) { int id Integer.parseInt(req.getParameter(id)); req.setAttribute(activity, activityService.getActivityById(id)); req.getRequestDispatcher(/activity/detail.jsp).forward(req, resp); } else if (/delete.equals(path)) { int id Integer.parseInt(req.getParameter(id)); activityService.deleteActivity(id); resp.sendRedirect(req.getContextPath() /activity/list); } } Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String path req.getPathInfo(); if (/save.equals(path)) { String id req.getParameter(id); Activity activity new Activity(); if (id ! null !id.isEmpty()) { activity.setId(Integer.parseInt(id)); } activity.setTitle(req.getParameter(title)); activity.setContent(req.getParameter(content)); activity.setLocation(req.getParameter(location)); activity.setStartTime(req.getParameter(startTime)); activityService.saveActivity(activity); resp.sendRedirect(req.getContextPath() /activity/list); } } }这段代码看起来简单但有一个细节删除用的是redirect而不是forward。因为forward后如果用户刷新页面浏览器会再次执行上一次的 POST 或 GET 请求可能导致活动被重复删除或表单重复提交。redirect让浏览器重新发起一次干净的 GET 请求这就是典型的 PRG 模式。getPathInfo()是WebServlet(/activity/*)匹配到的*部分比如请求/activity/list取到的就是/list。事务放在 Service 层这是很多新手忽略的地方public void saveActivity(Activity activity) { try (Connection conn DBUtil.getConnection()) { if (activity.getId() null) { activityDao.insert(conn, activity); } else { activityDao.update(conn, activity); } } catch (SQLException e) { throw new RuntimeException(活动保存失败, e); } }ActivityDao里的insert和update需要接收Connection参数不能让 DAO 自己再开一个连接。如果 DAO 自己开连接saveActivity里就控制不了事务边界——你想让两条 SQL 要么同时成功要么同时失败必须用同一个连接。这就是分层里面隐含的事务规则连接的创建和提交必须在 ServiceDAO 只负责执行。3.2 报名与名额扣减事务边界划在哪里报名是活动管理系统里最值得讲的模块因为这里藏着一个经典的并发问题两个人同时点报名系统可能多放一个名额。我最初写的时候也翻过车后来把事务边界划得很明确。public void signUp(int activityId, int userId) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); Activity activity activityDao.getActivityForUpdate(conn, activityId); if (activity.getSignedUp() activity.getQuota()) { throw new BusinessException(活动名额已满); } if (activityDao.existsSignUp(conn, activityId, userId)) { throw new BusinessException(你已经报过这个活动); } activityDao.increaseSignedUp(conn, activityId); activityDao.insertSignUp(conn, activityId, userId); conn.commit(); } catch (Exception e) { if (conn ! null) { conn.rollback(); } throw e; } finally { if (conn ! null) { conn.setAutoCommit(true); conn.close(); } } }这里有两个关键点。第一getActivityForUpdate必须使用SELECT ... FOR UPDATE把这一行锁住这样第二个请求进来时会等第一个事务提交读出的是最新人数。如果只是普通SELECTMySQL 在默认的事务隔离级别下读出来的是快照两个请求会同时看到signed_up19然后都通过校验最后名额变 21。第二existsSignUp要放在FOR UPDATE之后避免锁等待期间出现重复报名再加上signup表的唯一索引数据库层又一次拦住重复数据。这种写法用上了 MySQL 事务处理但隐患也存在如果活动表数据量大FOR UPDATE会锁行长事务会让其他更新排队。所以事务里不要做耗时操作不要远程调用该提交就提交。小团队活动管理系统每天几千人报名这个方案完全够用如果要做秒杀级别的抢票就得换 Redis 原子扣减或者数据库条件更新UPDATE activity SET signed_up signed_up 1 WHERE id ? AND signed_up quota;这条 SQL 自身就是原子的并发下不会超卖。执行后影响行数为 0 就说明抢完了再回滚业务即可。我一般会把这种排查经验记在项目 README 里避免下次重新踩。3.3 前端校验与后端校验Bootstrap表单和Servlet双重防线Bootstrap 表单能立刻让页面好看但它的校验样式只是视觉反馈真正拦不住恶意请求。前端校验的目的是减少无效提交后端校验才是数据安全的底线。前端我用 Bootstrap 的needs-validation配合简单 JSform idactivityForm classneeds-validation novalidate methodpost action${pageContext.request.contextPath}/activity/save div classmb-3 label classform-label活动标题/label input typetext nametitle classform-control required maxlength50 /div div classmb-3 label classform-label活动时间/label input typedatetime-local namestartTime classform-control required /div button classbtn btn-primary typesubmit保存活动/button /form script document.getElementById(activityForm).addEventListener(submit, function (event) { var title this.querySelector(input[nametitle]).value.trim(); var startTime this.querySelector(input[namestartTime]).value; if (title || startTime ) { alert(标题和时间不能为空); event.preventDefault(); } }); /scriptnovalidate会关闭浏览器自带的提示气泡让我们的 JS 统一接管前端校验只负责提示用户把 JS 禁用后依然能提交。所以 Servlet 里必须再校验一遍String title req.getParameter(title); if (title null || title.trim().isEmpty() || title.trim().length() 50) { req.setAttribute(error, 活动标题不能为空且不能超过50字); req.getRequestDispatcher(/activity/edit.jsp).forward(req, resp); return; }校验不通过时用forward回到编辑页并且把错误信息放到 request 里页面上用${error}显示。这样用户能看到刚才填写的内容不需要重新输入整张表单。后端校验的另一个重点是 SQL 注入ActivityDao里所有 SQL 必须用PreparedStatement的占位符不能用字符串拼接否则用户在标题里写一段 OR 11--就能把整个活动表拉出来。4. 用户登录与权限Session、Filter、角色控制4.1 登录态识别Session的失效和续期活动管理系统如果没有登录用户就能直接访问后台地址删除活动。登录逻辑我用一个简单的LoginServlet完成用户提交用户名和密码校验通过后把User对象放进 Session后续每个请求都从 Session 里取当前用户。WebServlet(/login) public class LoginServlet extends HttpServlet { private UserService userService new UserService(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username req.getParameter(username); String password req.getParameter(password); User user userService.login(username, password); if (user ! null) { HttpSession session req.getSession(); session.setAttribute(loginUser, user); session.setMaxInactiveInterval(30 * 60); resp.sendRedirect(req.getContextPath() /activity/list); } else { req.setAttribute(error, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); } } }session.setMaxInactiveInterval(30 * 60)设置的是 Session 闲置超时时间单位是秒意思是 30 分钟不操作就自动失效。这里有个常见分歧要不要设超时如果不设Session 会一直活着管理后台容易被别人捡到登录态如果设太短用户填活动内容时停一会儿就要重新登录。我的经验是管理后台 30 分钟比较合适实操中可以把“记住我”做成 Cookie但活动管理系统没必要。密码不能用明文存储UserService.login里要对用户输入做同样的哈希再比对至少用SHA-256加盐生产环境建议直接用 BCrypt。4.2 用Filter统一做登录检查和字符编码如果每个 Servlet 都写一遍req.setCharacterEncoding(UTF-8)代码会重复到让人崩溃。Filter 可以解决这个问题我通常写两个一个负责编码一个负责登录拦截。WebFilter(/*) public class EncodingFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding(UTF-8); resp.setCharacterEncoding(UTF-8); resp.setContentType(text/html;charsetUTF-8); chain.doFilter(req, resp); } }WebFilter(/*) public class LoginFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String uri request.getRequestURI(); String contextPath request.getContextPath(); String path uri.substring(contextPath.length()); HttpSession session request.getSession(false); boolean loggedIn session ! null session.getAttribute(loginUser) ! null; boolean isLoginPage path.equals(/login.jsp) || path.equals(/login); boolean isStaticResource path.startsWith(/css/) || path.startsWith(/js/); if (loggedIn || isLoginPage || isStaticResource) { chain.doFilter(req, resp); } else { response.sendRedirect(request.getContextPath() /login.jsp); } } }两个 Filter 都用了WebFilter(/*)但执行顺序取决于部署描述符里filter-mapping的先后或者 Servlet 3.0 规范里类名的字母排序。如果你想明确顺序建议在web.xml里手动配置先配编码 Filter再配登录 Filter。登录 Filter 里必须放行登录页和静态资源否则加载登录页的 CSS 也会被拦截形成重定向死循环。另一个细节是request.getSession(false)如果你用getSession(true)Filter 里会给未登录用户也创建一个 Session这就失去了拦截的意义。4.3 管理员与普通用户的菜单分流活动管理系统的用户角色不能只是一个摆设。管理员能发布和删除活动普通用户只能浏览和报名。按钮在页面上要按角色显示但更重要的是后端接口也要拦截。JSP 里我通常用 JSTL 的c:if控制菜单c:if test${sessionScope.loginUser.role 1} a classbtn btn-primary btn-sm href${pageContext.request.contextPath}/activity/edit 发布活动 /a /c:ifsessionScope.loginUser.role会去 Session 里取出loginUser再读它的role属性。这里约定role1是管理员role0是普通用户。按钮隐藏能挡住小白用户但挡不住知道 URL 的人他完全可以手动访问/activity/delete?id1。所以删除操作在 Servlet 里必须再校验角色User loginUser (User) req.getSession().getAttribute(loginUser); if (loginUser null || loginUser.getRole() ! 1) { resp.sendError(HttpServletResponse.SC_FORBIDDEN); return; }前端控制是体验后端控制是安全。普通用户和管理员可以共用一个表只是不同角色看到不同的功能入口如果后续权限矩阵复杂再引入permission表也不迟。我见过很多项目把角色判断写死在 JSP 的% if %里虽然能跑但维护起来很累用 JSTL 至少保证页面标签统一。5. 常见问题避坑从部署到并发我踩过的5个坑5.1 现象1Tomcat启动后JSP页面样式全丢现象项目启动后页面能打开但所有 Bootstrap 样式都没有浏览器控制台一堆 404路径都指向/activity/css/bootstrap.min.css。原因JSP 里写了相对路径。当浏览器地址是/activity-system/activity/list时相对路径css/bootstrap.min.css会解析成/activity-system/activity/css/bootstrap.min.css而真实目录是/activity-system/css。解决把静态资源路径全部改成${pageContext.request.contextPath}/css/bootstrap.min.css。或者在head里加base href${pageContext.request.contextPath}/这样页面里所有相对路径都基于项目根。我一般用前者因为base会把链接、表单 action 的路径规则一起改掉容易引发新问题。5.2 现象2数据库连接失败或SSL连接错误现象运行时报Communications link failure或者SSL connection error: Failed to set server hostname。原因MySQL 8 默认开启 SSL 握手驱动版本落后会不认识新特性另外 MySQL 8 对时区有要求连接串没写serverTimezone也会报错。解决连接串改成jdbc:mysql://localhost:3306/activity_db?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue。本地开发关掉 SSL 没问题线上如果走公网一定要用 SSL 或在内网限定访问。还要检查 MySQL 服务有没有真的启动很多时候Communications link failure不是驱动问题而是服务没起来用mysqladmin ping先确认数据库层面是通的。5.3 现象3报名超卖名额变负数现象活动报名人数超过quota甚至出现负数后台数字彻底不可信。原因报名逻辑没有考虑并发。两个请求同时读到signed_up19、quota20都通过了“人数未满”判断然后各自执行1最终变成 21。普通SELECT在事务里读的是快照没有锁就拦不住。解决用SELECT ... FOR UPDATE锁住活动行或者用条件更新UPDATE activity SET signed_up signed_up 1 WHERE id ? AND signed_up quota。二级防线是给signup表加唯一索引即使业务代码漏了数据库也会拒绝重复报名。这条我是拿血泪换来的上线第一天就翻车从此以后凡是有“扣减”语义的操作我一定先问自己一句两个请求同时进来会怎样5.4 现象4JSP页面中文乱码现象页面上的活动标题变成???或乱码数据库里也是乱码。原因乱码是编码链路断了。文件的保存编码、JSP 页面声明的编码、HTTP 请求编码、响应编码、数据库表字符集、JDBC 连接编码这六层里任意一层不是 UTF-8结果就乱。最常见的是文件用系统默认 GBK 保存以及建表时用了latin1。解决统一全部 UTF-8。JSP 顶部写% page contentTypetext/html;charsetUTF-8 %Filter 里设置req.setCharacterEncoding(UTF-8)建表用utf8mb4连接串加characterEncodingutf8IDE 里把文件编码改成 UTF-8。如果数据库里已经存了乱码先把旧数据备份再改表和字段的字符集但旧数据不一定能恢复所以最好一开始就防住。5.5 现象5Servlet路径匹配混乱404/405现象点击表单提交后 404或者页面提示 405 Method Not Allowed。原因405 是请求方法不匹配比如表单methodpost但 Servlet 只重写了doGet。404 往往是路径映射写错例如WebServlet(/activity/*)和getRequestDispatcher(/activity/list.jsp)混淆或者用了相对的forward路径。解决把表单的action写成全路径${pageContext.request.contextPath}/activity/saveServlet 里同时实现doGet和doPost。如果WebServlet(/activity/*)里的getPathInfo()总为 null检查一下是不是在web.xml里重复映射了同一 URL。还有一种情况forward到 JSP 时路径以/开头是相对于当前应用根不以/开头是相对于当前 Servlet 的路径写错就从 controller 变成 404。这个是一个黑匣子建议所有转发路径都写成/WEB-INF/pages/xxx.jsp或者调试时打印一下req.getRequestURI()。6. 验证与进阶用最小的命令把系统跑起来再给项目加一层缓存6.1 最小验证清单从数据库脚本到浏览器验收项目写完以后我习惯先用最原始的命令验证一遍而不是直接上 IDE 的 Debug。最小命令是这样mvn clean package cp target/activity-system.war $CATALINA_HOME/webapps/ $CATALINA_HOME/bin/startup.sh然后访问http://localhost:8080/activity-system/activity/list。如果页面 404先看 Tomcat 日志确认 war 有没有解压成功。功能性验证我一般走一条完整链路管理员登录 → 发布活动 → 普通用户注册 → 报名 → 再次报名看是否被拦截 → 管理员删除活动。这里最需要盯的是报名前后的signed_up数值变化以及第二次报名是否被提示“已经报过”。6.2 进阶改动把活动列表热点数据放进Redis活动系统跑稳以后列表页会成为访问热点。活动列表不会像库存那样一秒钟变一次完全可以用 Redis 缓存但要注意缓存不能覆盖报名人数。做法是只缓存活动的基本信息和描述剩余名额每次实时查库发布活动和编辑活动时删除对应缓存 key。String key activity:list:all; String json redis.get(key); if (json null) { ListActivity list activityDao.listAll(); redis.setex(key, 60, JSON.toJSONString(list)); return list; } return JSON.parseArray(json, Activity.class);setex设置了 60 秒过期时间避免数据长期不更新编辑活动时再主动del一次 key让下次请求重新查库。这里不要把signed_up字段一起缓存因为报名人数是高频更新的缓存里的旧值会让用户看到错误的名额这比性能差更严重。我的习惯是缓存只放“可以容忍旧一点”的数据和钱、库存、名额相关的数据必须走实时库。希望这个边界能帮到你少踩一次缓存坑。本文还有配套的精品资源点击获取
返回列表