当前位置:首页 > 生活常识 > 正文

skip现在进行时

摘要: skip现在进行时最佳答案53678位专家为你答疑解惑记一次PageHelper使用不当导致的生产bug,下面一起来看看本站小编...

skip现在进行时

最佳答案 53678位专家为你答疑解惑

记一次PageHelper使用不当导致的生产bug,下面一起来看看本站小编小小怪下士的架构攻略给大家精心整理的答案,希望对您有帮助

skip现在进行时1

我的项目发生了哪些奇葩现象?

一切的问题都要从我接受的项目开始说起, 在开发这个项目的过程中,发生了各种奇葩的事情, 下面我简单说给你们听听:

账号重复注册?

你肯定在想这是什么意思? 就是字面意思,已经注册的账号,可以再次注册成功!!!

else if (UserConstants.NOT_UNIQUE.equals(userService.checkUserNameUnique(username))||"匿名用户".equals(username)){    // 注册用户已存在    msg="注册用户'" + username + "'失败";}

如上所示: checkUserNameUnique(username)用来验证数据库是否存在用户名:

<select id="checkUserNameUnique" parameterType="String" resultType="int">   select count(1) from sys_user where user_name=#{userName} limit 1</select>

正常来说,是不会有问题的,那么原因我们后面讲,接着看下一个问题。

查询全部分类的下拉列表只能查出5条数据?

如上所示,明明有十多个结果,怎么只能返回5个?我也没有添加分页参数啊?

相信用过PageHelper的同学已经知道问题出在哪里了。

修改用户密码报错?

当管理员在后台界面重置用户的密码的时候,居然报错了??

报错信息清晰的告诉了我:sql语句异常,update语句不认识 “Limit 5”

到此为止,报错信息已经告诉了我,我的sql被拼接了该死的“limit”分页参数。

小结

上面提到的几个只是冰山一角,在我使用的过程中,还有各种涉及到sql的地方,会因为这个分页参数导致的问题,我可以分为两种:

1)直接导致报错的:明确报错原因的

比如insert、update语句等,不支持limit,会直接报错。

2)导致业务逻辑错误,但是代码没有错误提示

如我上面提到的用户可以重复注册,却没有报错,实际在代码当中是有报错的,但是当前方法对异常进行了throw,最终被全局异常捕获了。

不分页的sql被拼接了limit,导致没有报错,但是数据返回量错误。

注意:异常不是每次出现,是有一定纪律的,但是触发几率较高,原因在后面会逐渐脱出。

PageHelper是怎么做到上面的问题的?

PageHelper使用

我这里只讲解项目基于的框架的使用方式。

代码如下:

@GetMapping("/cms/cmsEssayList")public TableDataInfo cmsEssayList(CmsBlog cmsBlog) {    //状态为发布    cmsBlog.setStatus("1");    startPage();    List<CmsBlog> list=cmsBlogService.selectCmsBlogList(cmsBlog);    return getDataTable(list);}

使用起来还是很简单的,通过 startPage()指定分页参数,通过getDataTable(list)对结果数据封装成分页的格式。

有些同学会问,这也没没传分页参数啊,并且实体类当中也没有,这就是比较有意思的点,下一小结就来聊聊源码。

startPage()干啥了?

protected void startPage(){    // 通过request去获取前端传递的分页参数,不需控制器要显示接收    PageDomain pageDomain=TableSupport.buildPageRequest();    Integer pageNum=pageDomain.getPageNum();    Integer pageSize=pageDomain.getPageSize();    if (StringUtils.isNotNull(pageNum) && StringUtils.isNotNull(pageSize))    {        String orderBy=SqlUtil.escapeOrderBySql(pageDomain.getOrderBy());        Boolean reasonable=pageDomain.getReasonable();        // 真正使用pageHelper进行分页的位置        PageHelper.startPage(pageNum, pageSize, orderBy).setReasonable(reasonable);    }}

PageHelper.startPage(pageNum, pageSize, orderBy).setReasonable(reasonable)的参数分别是:

pageNum:页数pageSize:每页数据量orderBy:排序reasonable:分页合理化,对于不合理的分页参数自动处理,比如传递pageNum是小于0,会默认设置为1.

继续跟踪,连续点击startpage构造方法到达如下位置:

/** * 开始分页 * * @param pageNum      页码 * @param pageSize     每页显示数量 * @param count        是否进行count查询 * @param reasonable   分页合理化,null时用默认配置 * @param pageSizeZero true且pageSize=0时返回全部结果,false时分页,null时用默认配置 */public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {    Page<E> page=new Page<E>(pageNum, pageSize, count);    page.setReasonable(reasonable);    page.setPageSizeZero(pageSizeZero);    // 1、获取本地分页    Page<E> oldPage=getLocalPage();    if (oldPage !=null && oldPage.isOrderByOnly()) {        page.setOrderBy(oldPage.getOrderBy());    }     // 2、设置本地分页    setLocalPage(page);    return page;}

到达终点位置了,分别是:getLocalPage()和setLocalPage(page),分别来看下:

getLocalPage()

进入方法:

/** * 获取 Page 参数 * * @return */public static <T> Page<T> getLocalPage() {    return LOCAL_PAGE.get();}

看看常量LOCAL_PAGE是个什么路数?

protected static final ThreadLocal<Page> LOCAL_PAGE=new ThreadLocal<Page>();

好家伙,是ThreadLocal,学过java基础的都知道吧,独属于每个线程的本地缓存对象。

当一个请求来的时候,会获取持有当前请求的线程的ThreadLocal,调用LOCAL_PAGE.get(),查看当前线程是否有未执行的分页配置。

setLocalPage(page)

此方法显而易见,设置线程的分页配置:

protected static void setLocalPage(Page page) {    LOCAL_PAGE.set(page);}

小结

经过前面的分析,我们发现,问题似乎就是这个ThreadLocal导致的。

是否在使用完之后没有进行清理?导致下一次此线程再次处理请求时,还在使用之前的配置?

我们带着疑问,看看mybatis时如何使用pageHelper的。

mybatis使用pageHelper分析

我们需要关注的就是mybatis在何时使用的这个ThreadLocal,也就是何时将分页餐数获取到的。

前面提到过,通过PageHelper的startPage()方法进行page缓存的设置,当程序执行sql接口mapper的方法时,就会被拦截器PageInterceptor拦截到。

PageHelper其实就是mybatis的分页插件,其实现原理就是通过拦截器的方式,pageHelper通PageInterceptor实现分页效果,我们只关注intercept方法:

@Overridepublic Object intercept(Invocation invocation) throws Throwable {    try {        Object[] args=invocation.getArgs();        MappedStatement ms=(MappedStatement) args[0];        Object parameter=args[1];        RowBounds rowBounds=(RowBounds) args[2];        ResultHandler resultHandler=(ResultHandler) args[3];        Executor executor=(Executor) invocation.getTarget();        CacheKey cacheKey;        BoundSql boundSql;        // 由于逻辑关系,只会进入一次        if (args.length==4) {            //4 个参数时            boundSql=ms.getBoundSql(parameter);            cacheKey=executor.createCacheKey(ms, parameter, rowBounds, boundSql);        } else {            //6 个参数时            cacheKey=(CacheKey) args[4];            boundSql=(BoundSql) args[5];        }        checkDialectExists();        //对 boundSql 的拦截处理        if (dialect instanceof BoundSqlInterceptor.Chain) {            boundSql=((BoundSqlInterceptor.Chain) dialect).doBoundSql(BoundSqlInterceptor.Type.ORIGINAL, boundSql, cacheKey);        }        List resultList;        //调用方法判断是否需要进行分页,如果不需要,直接返回结果        if (!dialect.skip(ms, parameter, rowBounds)) {            //判断是否需要进行 count 查询            if (dialect.beforeCount(ms, parameter, rowBounds)) {                //查询总数                Long count=count(executor, ms, parameter, rowBounds, null, boundSql);                //处理查询总数,返回 true 时继续分页查询,false 时直接返回                if (!dialect.afterCount(count, parameter, rowBounds)) {                    //当查询总数为 0 时,直接返回空的结果                    return dialect.afterPage(new ArrayList(), parameter, rowBounds);                }            }            resultList=ExecutorUtil.pageQuery(dialect, executor,                    ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);        } else {            //rowBounds用参数值,不使用分页插件处理时,仍然支持默认的内存分页            resultList=executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);        }        return dialect.afterPage(resultList, parameter, rowBounds);    } finally {        if(dialect !=null){            dialect.afterAll();        }    }}

如上所示是intecept的全部代码,我们下面只关注几个终点位置:

设置分页:dialect.skip(ms, parameter, rowBounds)

此处的skip方法进行设置分页参数,内部调用方法:

Page page=pageParams.getPage(parameterObject, rowBounds);

继续跟踪getPage(),发现此方法的第一行就获取了ThreadLocal的值:

Page page=PageHelper.getLocalPage();

统计数量:dialect.beforeCount(ms, parameter, rowBounds)

我们都知道,分页需要获取记录总数,所以,这个拦截器会在分页前先进行count操作。

如果count为0,则直接返回,不进行分页:

//处理查询总数,返回 true 时继续分页查询,false 时直接返回if (!dialect.afterCount(count, parameter, rowBounds)) {    //当查询总数为 0 时,直接返回空的结果    return dialect.afterPage(new ArrayList(), parameter, rowBounds);}

afterPage其实是对分页结果的封装方法,即使不分页,也会执行,只不过返回空列表。

分页:ExecutorUtil.pageQuery

在处理完count方法后,就是真正的进行分页了:

resultList=ExecutorUtil.pageQuery(dialect, executor,        ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);

此方法在执行分页之前,会判断是否执行分页,依据就是前面我们通过ThreadLocal的获取的page。

当然,不分页的查询,以及新增和更新不会走到这个方法当中。

非分页:executor.query

而是会走到下面的这个分支:

resultList=executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);

我们可以思考一下,如果ThreadLoad在使用后没有被清除,当执行非分页的方法时,那么就会将Limit拼接到sql后面。

为什么不分也得也会拼接?我们回头看下前面提到的dialect.skip(ms, parameter, rowBounds):

如上所示,只要page被获取到了,那么这个sql,就会走前面提到的ExecutorUtil.pageQuery分页逻辑,最终导致出现不可预料的情况。

其实PageHelper对于分页后的ThreaLocal是有清除处理的。

清除TheadLocal

在intercept方法的最后,会在sql方法执行完成后,清理page缓存:

finally {    if(dialect !=null){        dialect.afterAll();    }}

看看这个afterAll()方法:

@Overridepublic void afterAll() {    //这个方法即使不分页也会被执行,所以要判断 null    AbstractHelperDialect delegate=autoDialect.getDelegate();    if (delegate !=null) {        delegate.afterAll();        autoDialect.clearDelegate();    }    clearPage();}

只关注 clearPage():

/** * 移除本地变量 */public static void clearPage() {    LOCAL_PAGE.remove();}

小结

到此为止,关于PageHelper的使用方式就讲解完了。

整体看下来,似乎不会存在什么问题,但是我们可以考虑集中极端情况:

如果使用了startPage(),但是没有执行对应的sql,那么就表明,当前线程ThreadLocal被设置了分页参数,可是没有被使用,当下一个使用此线程的请求来时,就会出现问题。如果程序在执行sql前,发生异常了,就没办法执行finally当中的clearPage()方法,也会造成线程的ThreadLocal被污染。

所以,官方给我们的建议,在使用PageHelper进行分页时,执行sql的代码要紧跟startPage()方法

除此之外,我们可以手动调用clearPage()方法,在存在问题的方法之前。

需要注意:不要分页的方法前手动调用clearPage,将会导致你的分页出现问题

还有人问为什么不是每次请求都出错?

这个其实取决于我们启动服务所使用的容器,比如tomcat,在其内部处理请求是通过线程池的方式。甚至现在的很多容器是基于netty的,都是通过线程池,复用线程来增加服务的并发量。

假设线程1持有没有被清除的page参数,不断调用同一个方法,后面两个请求使用的是线程2和线程3没有问题,再一个请求轮到线程1了,此时就会出现问题了。

总结

关于PageHelper的介绍就这么多,真的是折磨我好几天,要不是项目紧急,来不及替换,我一定不会使用这个组件。

莫名其妙的就会有个方法出现问题,一通排查,发现都是这个PageHelper导致的。虽然我已经全局搜索使用的地方,保证startPage()后紧跟sql命令,但是仍然有嫌犯潜逃,只能在有问题的方法使用clearPage()来打补丁。

虽然PageHelper给我带来一些困扰,耗费了一定的时间,但是定位问题的过程中,也学习了mybatis和pagehepler的实现方式,对于热爱源码阅读的同学来说还是有一定的提升的。

来源:juejin.cn/post/7125356642366914596

skip现在进行时2

直播吧10月27日讯 湖人VS马刺的比赛正在进行中。

著名詹黑Skip更新推特写道:“威少今晚在享受生命中最美妙的时光。没有勒布朗,他(威少)的梦想成真了:这支伴随他成长的他深爱的湖人队现在是他的了。”

(胖七赖)

skip现在进行时3

Miss薛说英语:你怎么区分现在进行时和一般现在时

虽然所有常考的时态已经讲完了,但是在实际使用的时候,还是会出现没有办法准确判断该使用什么时态的情况,所以,接下来要进行辨析。

现在进行时与一般现在时的区别

(1) 现在进行时只有一个特征:目前正在做的事情,而一般现在时强调习惯,就是我们说的经常反复做的事情。如:

I’m reading a story now. 现在我正在看一个故事。(目前正在干的事情)

I read stories in my spare time. 我一有空就看小说。(习惯)

(2) 现在进行时动词一定是延续性动词,而一般现在时只表示动作的重复,而不表示动作的持续。如:

What are you doing these days? 这几天你在干什么?

They are learning English in the summer holiday. 他们暑假在学英语。

They read English every day. 他们每天读英语。

They play volleyball every Sunday. 他们每周星期天都打排球。

(3) 瞬间动作的动词(如 jump, knock, beat, pick, skip等)的进行时,表示动作的重复。如:

The girls are jumping over there. 女孩子们在那边跳。

His heart is beating fast. 他的心脏跳得很快。

(4) 某些表示希望或想法等心理活动的动词(如hope, wonder, want等)用进行时表示委婉客气。如:

I’m wondering whether you can help us now. 我想知道能否你现在帮我们一下。

I’m hoping that you will succeed. 希望你能成功

发表评论