前言
在我使用mybatis替换JDBC重构用户操作的CRUD时,发现了一个非常奇怪的问题:
当我进行新增和修改等操作时,查看后台和数据库,发现数据已经新增或修改了,但是页面还是显示第一次登录时的数据,除非我们重启服务器,再次查看(此时数据库又变成了最新的状态)。
我开始以为是JSP和页面显示的错误,因此在前端页面进行排查错误,始终未发现错误,我又回到后端,理解mybatis工作原理,终于发现了问题所在,因此特别在这里总结一下,为了帮助更多的人能够发现问题,解决问题。
有问题的项目是(可以在我的资源中下载源码) :
问题出现分析
当前的数据库表结构及数据如下:
忽略user_id的不同(不规律),因为我采用了主键自增的语法,并且我进行了一些列的操作(删除),所以看上去id不是规律的(这里只关注数据行的变化)
运行项目:
输入数据库中存在的数据,进行登录。
查看数据:点击上下页,发现数据正确,且展示出来了。
接下来,新增用户:
发现页面没有更新(如果更新了,数据应该显示在最上方)。
查看后台和数据库:
查看控制台,发现已经执行了insert操作。
刷新数据库,数据已经增加。
此时发生了数据库数据增加,但是前台不更新数据。
问题原因
因为每次查看分页数据都需要请求/userPage这里url,因此要找对应的servlet进行分析
对应的servlet如下:
UserPageServlet
package servlet;
import dao.UserDao;
import entity.User;
import entity.dto.UserDto;
import org.apache.ibatis.session.SqlSession;
import util.PageUtil;
import util.SqlSessionFactoryUtil;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
@WebServlet("/userPage")
public class UserPageServlet extends HttpServlet {
SqlSession session= SqlSessionFactoryUtil.getSessionSql();
UserDao userDao= session.getMapper(UserDao.class);
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String keyword= request.getParameter("keyword");
if (keyword==null){//当关键字为null时,设置为空,防止没有进行模糊查询时,sql失效
keyword="";
}
String currentPage =request.getParameter("pageIndex");
if (currentPage==null){//防止第一次访问servlet时,当前页为空,设置为第一页
currentPage="1";
}
int pageIndex = Integer.parseInt(currentPage);
// UserDao userDao =new UserDaoImpl();
int count = userDao.selectUsersCount();
//总页数
int totalPages= PageUtil.getTotalPages(count,PageUtil.PAGE_SIZE);
//防止查询越界
if (pageIndex<1){
pageIndex=1;
}
if (pageIndex>totalPages){
pageIndex=totalPages;
}
UserDto userDto =new UserDto();
userDto.setKeyword(keyword);
// System.out.println(userDto.getKeyword());
userDto.setPageIndex(PageUtil.PAGE_SIZE*(pageIndex-1));
// System.out.println(userDto.getPageIndex());
userDto.setPageSize(PageUtil.PAGE_SIZE);
List<User> list= userDao.selectUserListByPage(userDto);
//在request作用域设置值,仅在同一个请求中有效
request.setAttribute("list",list);
for (User user : list) {
System.out.println(user);
}
request.setAttribute("keyword",keyword);
request.setAttribute("pageIndex",pageIndex);
request.setAttribute("totalPages",totalPages);
//转发当前请求到userList.jsp
request.getRequestDispatcher("/front/userList.jsp").forward(request,response);
// request.getRequestDispatcher("zhezhaoceng.jsp").forward(request,response);
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req,resp);
}
}
原因解析:
我们将SqlSession放在全局变量的作用范围,变成了servlet的一个属性,而servlet在整个应用的生命周期是单例的(并且没有调用close方法关闭SqlSession实例),导致前端的每次http请求,对应后端的一个线程处理时,每个线程使用了同一个SqlSession,导致每次查询的都是第一次存储在一级缓存中的数据 ,所以页面数据不会更新。
什么是一级缓存?
一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该 Session 中的所有 Cache 就将清空,默认打开一级缓存。
一级缓存也是session级缓存,在session未关闭的情况的下,多次执行相同的查询 ,会直接取缓存数据,不再查询数据库
sesson是线程不安全的,使用完要关闭。
SqlSession为什么不应该共享?
SqlSession 每个线程都应该有它自己的SqlSession实例。SqlSession的实例不能被共享,也是线程不安全的。因此最佳的范围是请求或方法范围。使用Web框架时,要考虑SqlSession放在一个和HTTP请求对象相似的范围内。基于收到的HTTP请求,打开了一个SqlSession,返回响应后关闭。关闭Session很重要,应该确保使用finally块来关闭它。基本模式:
SqlSession session = sqlSessionFactory.openSession(); try { // do work } finally { session.close(); }
重点:在使用SqlSession时,使用完成后需要关闭。
解决方案
将SqlSession从属性变为局部变量:
将SqlSession从全局变量变为局部变量,每次调用doPost方法时,相当于获取一个新的SqlSession对象,此时需要查询数据时,要从数据库中查找,而不是从一级缓存中查找,从而将页面的数据更新。
总结
Mybatis默认是有一个一级缓存的机制的,当我们查询数据库中的数据时,同一个的SqlSession中的多次sql操作,都是操作缓存区中的数据,而不是从数据库中直接获取,从而导致了页面数据和数据库数据的不一致现象(或者说缓存和数据库中数据不一致的现象)
如果缓存中存在数据,则直接从缓存中返回数据到dao