定时任务中将redis数据存入数据库相关问题

我的GitHub: Powerveil · GitHub
我的Gitee: Powercs12 (powercs12) - Gitee.com
皮卡丘每天学Java

觉得昨天晚上只是分析和解决问题,很多人可能不知道出bug的场景,今天决定重现bug,让代码改回错误代码,重现看到一大串异常的情形[手动狗头]

问题出处:

B站:81.代码实现-同步redis数据到数据库_哔哩哔哩_bilibili

9:37时运行时报错,NullPointerException

异常信息


2023-01-20 11:27:40.009 ERROR 15584 --- [   scheduling-1] o.s.s.s.TaskUtils$LoggingErrorHandler    : Unexpected error occurred in scheduled task

org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException: 
### Error updating database.  Cause: java.lang.NullPointerException
### The error may exist in com/power/mapper/ArticleMapper.java (best guess)
### The error may involve com.power.mapper.ArticleMapper.updateById
### The error occurred while executing an update
### Cause: java.lang.NullPointerException
    at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:96) ~[mybatis-spring-2.0.6.jar:2.0.6]
    at com.baomidou.mybatisplus.extension.toolkit.SqlHelper.executeBatch(SqlHelper.java:179) ~[mybatis-plus-extension-3.4.3.jar:3.4.3]
    at com.baomidou.mybatisplus.extension.toolkit.SqlHelper.executeBatch(SqlHelper.java:204) ~[mybatis-plus-extension-3.4.3.jar:3.4.3]
    at com.baomidou.mybatisplus.extension.service.impl.ServiceImpl.executeBatch(ServiceImpl.java:248) ~[mybatis-plus-extension-3.4.3.jar:3.4.3]
    at com.baomidou.mybatisplus.extension.service.impl.ServiceImpl.updateBatchById(ServiceImpl.java:200) ~[mybatis-plus-extension-3.4.3.jar:3.4.3]
    at com.baomidou.mybatisplus.extension.service.IService.updateBatchById(IService.java:177) ~[mybatis-plus-extension-3.4.3.jar:3.4.3]
    at com.baomidou.mybatisplus.extension.service.IService$$FastClassBySpringCGLIB$$f8525d18.invoke(<generated>) ~[mybatis-plus-extension-3.4.3.jar:3.4.3]
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.3.7.jar:5.3.7]
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:779) ~[spring-aop-5.3.7.jar:5.3.7]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.3.7.jar:5.3.7]
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750) ~[spring-aop-5.3.7.jar:5.3.7]
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123) ~[spring-tx-5.3.7.jar:5.3.7]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:388) ~[spring-tx-5.3.7.jar:5.3.7]
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) ~[spring-tx-5.3.7.jar:5.3.7]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.7.jar:5.3.7]
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750) ~[spring-aop-5.3.7.jar:5.3.7]
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:692) ~[spring-aop-5.3.7.jar:5.3.7]
    at com.power.service.impl.ArticleServiceImpl$$EnhancerBySpringCGLIB$$4ebc6fef.updateBatchById(<generated>) ~[classes/:na]
    at com.power.job.UpdateViewCountJob.updateViewCount(UpdateViewCountJob.java:74) ~[classes/:na]
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_321]
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_321]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_321]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_321]
    at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:84) ~[spring-context-5.3.7.jar:5.3.7]
    at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54) ~[spring-context-5.3.7.jar:5.3.7]
    at org.springframework.scheduling.concurrent.ReschedulingRunnable.run(ReschedulingRunnable.java:95) [spring-context-5.3.7.jar:5.3.7]
    at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) [na:1.8.0_321]
    at java.util.concurrent.FutureTask.run(FutureTask.java:266) [na:1.8.0_321]
    at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180) [na:1.8.0_321]
    at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293) [na:1.8.0_321]
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_321]
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_321]
    at java.lang.Thread.run(Thread.java:750) [na:1.8.0_321]
Caused by: org.apache.ibatis.exceptions.PersistenceException: 
### Error updating database.  Cause: java.lang.NullPointerException
### The error may exist in com/power/mapper/ArticleMapper.java (best guess)
### The error may involve com.power.mapper.ArticleMapper.updateById
### The error occurred while executing an update
### Cause: java.lang.NullPointerException
    at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30) ~[mybatis-3.5.7.jar:3.5.7]
    at org.apache.ibatis.session.defaults.DefaultSqlSession.update(DefaultSqlSession.java:196) ~[mybatis-3.5.7.jar:3.5.7]
    at com.baomidou.mybatisplus.extension.service.impl.ServiceImpl.lambda$updateBatchById$3(ServiceImpl.java:203) ~[mybatis-plus-extension-3.4.3.jar:3.4.3]
    at com.baomidou.mybatisplus.extension.toolkit.SqlHelper.lambda$executeBatch$1(SqlHelper.java:208) ~[mybatis-plus-extension-3.4.3.jar:3.4.3]
    at com.baomidou.mybatisplus.extension.toolkit.SqlHelper.executeBatch(SqlHelper.java:169) ~[mybatis-plus-extension-3.4.3.jar:3.4.3]
    ... 31 common frames omitted
Caused by: java.lang.NullPointerException: null
    at com.power.utils.SecurityUtils.getLoginUser(SecurityUtils.java:24) ~[classes/:na]
    at com.power.utils.SecurityUtils.getUserId(SecurityUtils.java:49) ~[classes/:na]
    at com.power.handler.mybatisplus.MyMetaObjectHandler.updateFill(MyMetaObjectHandler.java:41) ~[classes/:na]
    at com.baomidou.mybatisplus.core.MybatisParameterHandler.lambda$updateFill$1(MybatisParameterHandler.java:154) ~[mybatis-plus-core-3.4.3.jar:3.4.3]
    at java.util.Optional.ifPresent(Optional.java:159) ~[na:1.8.0_321]
    at com.baomidou.mybatisplus.core.MybatisParameterHandler.updateFill(MybatisParameterHandler.java:152) ~[mybatis-plus-core-3.4.3.jar:3.4.3]
    at com.baomidou.mybatisplus.core.MybatisParameterHandler.process(MybatisParameterHandler.java:115) ~[mybatis-plus-core-3.4.3.jar:3.4.3]
    at com.baomidou.mybatisplus.core.MybatisParameterHandler.processParameter(MybatisParameterHandler.java:81) ~[mybatis-plus-core-3.4.3.jar:3.4.3]
    at com.baomidou.mybatisplus.core.MybatisParameterHandler.<init>(MybatisParameterHandler.java:64) ~[mybatis-plus-core-3.4.3.jar:3.4.3]
    at com.baomidou.mybatisplus.core.MybatisXMLLanguageDriver.createParameterHandler(MybatisXMLLanguageDriver.java:45) ~[mybatis-plus-core-3.4.3.jar:3.4.3]
    at org.apache.ibatis.session.Configuration.newParameterHandler(Configuration.java:645) ~[mybatis-3.5.7.jar:3.5.7]
    at org.apache.ibatis.executor.statement.BaseStatementHandler.<init>(BaseStatementHandler.java:69) ~[mybatis-3.5.7.jar:3.5.7]
    at org.apache.ibatis.executor.statement.PreparedStatementHandler.<init>(PreparedStatementHandler.java:41) ~[mybatis-3.5.7.jar:3.5.7]
    at org.apache.ibatis.executor.statement.RoutingStatementHandler.<init>(RoutingStatementHandler.java:46) ~[mybatis-3.5.7.jar:3.5.7]
    at org.apache.ibatis.session.Configuration.newStatementHandler(Configuration.java:658) ~[mybatis-3.5.7.jar:3.5.7]
    at org.apache.ibatis.executor.BatchExecutor.doUpdate(BatchExecutor.java:57) ~[mybatis-3.5.7.jar:3.5.7]
    at org.apache.ibatis.executor.BaseExecutor.update(BaseExecutor.java:117) ~[mybatis-3.5.7.jar:3.5.7]
    at org.apache.ibatis.executor.CachingExecutor.update(CachingExecutor.java:76) ~[mybatis-3.5.7.jar:3.5.7]
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_321]
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_321]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_321]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_321]
    at org.apache.ibatis.plugin.Invocation.proceed(Invocation.java:49) ~[mybatis-3.5.7.jar:3.5.7]
    at com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor.intercept(MybatisPlusInterceptor.java:106) ~[mybatis-plus-extension-3.4.3.jar:3.4.3]
    at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:62) ~[mybatis-3.5.7.jar:3.5.7]
    at com.sun.proxy.$Proxy122.update(Unknown Source) ~[na:na]
    at org.apache.ibatis.session.defaults.DefaultSqlSession.update(DefaultSqlSession.java:194) ~[mybatis-3.5.7.jar:3.5.7]
    ... 34 common frames omitted

这是定时任务执行的方法。

该定时任务的目的是将文章的浏览量从Redis中更新到Mysql中的文章表中
redisCache自定义的一个类,封装了RedisTemplate的一些方法
viewCountMap的key是文章id,value是文章的viewCount

下面程序运行时定时任务多打印一句话,这是昨天的图片,不想换了。

相关知识:Mybatis Plus、Spring Security、Redis、Mysql

本质原因:我们自己将Article字段的update字段做了自动填充导致自动填充时无法获取当前用户(程序刚启动时没有用户登录)。

为了代码更完善一点,我写了自动填充,但是这也导致了一个问题,在执行updateBatchById(articles)的时候,create相关字段会自动填充,看一下自动填充的具体代码。

getUserId()这个方法

看到这里大家可能就明白了,此时我们还没有登录,获取getLoginUser()就会出错,具体是jwt检验token的时候没有将当前用户信息存入存入SecurityContextHolder。其实也没必要存,因为我们做的是定时任务,不需要什么用户信息。

没有执行SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);

所以Authentication没有我们想要的值。没看过源码,不知道有没有默认值(没有默认值,后面看异常出现的位置可以推测)

下图两个位置出现空指针异常(如果Authentication有默认值,1出报异常,没有默认值,2出报异常)

异常信息的下面

上图可见,1处报异常

所以上面讨论的Authentication没有默认值。

解决方案1:去掉Article部分字段的自动填充

测试结果:

解决方案2(推荐):

从根源修改:

保留自动填充,因为这里没有牵扯到用户,所以自动填充的时候要判断是否填充


public class SecurityUtils {
    /**
     * 获取用户
     **/
    public static LoginUser getLoginUser() {
        if (!Objects.isNull(getAuthentication())) {
            return (LoginUser) getAuthentication().getPrincipal();
        }
        return null;
    }

    /**
     * 获取Authentication
     */
    public static Authentication getAuthentication() {
        return SecurityContextHolder.getContext().getAuthentication();
    }

    public static Boolean isAdmin() {
        if (!Objects.isNull(getLoginUser())) {
            Long id = getLoginUser().getUser().getId();
            return SystemConstants.USER_ADMIN_ID.equals(id);
        }
        return false;
    }

    public static Long getUserId() {
        if (!Objects.isNull(getLoginUser())) {
            return getLoginUser().getUser().getId();
        }
        return null;
    }

测试结果:

因为刚才着急测试结果,次数多了几次。

解决方案3(推荐)—— 2023-5-24

如果某个字段已经被填充,那么这个字段不必填充,

我在定时任务中填充这些字段。

下面是定时任务的修改


    @Scheduled(cron = "0/10 * * * * ?")
    public void updateViewCount() {
        //获取redis中的浏览量
        Map<String, Integer> viewCountMap = redisCache.getCacheMap("article:viewCount");
        //获取Id集合
        List<Long> collect = viewCountMap.keySet()
                .stream()
                .map(Long::valueOf)
                .collect(Collectors.toList());
        //从数据库查到的数据
        List<Article> articles = new ArrayList<>(articleService.listByIds(collect));
        //这个主要简化更改内容的,如果直接使用articles也可以,但是很多无效修改
        List<Article> articles1 = new ArrayList<>();
        for (Article temp : articles) {
            Article article = new Article(temp.getId(), temp.getViewCount());
            article.setUpdateBy(temp.getUpdateBy());
            article.setUpdateTime(temp.getUpdateTime());
            articles1.add(article);
        }
        //更新到数据库中
        articleService.updateBatchById(articles1);
    }

为了将控制台中输出的日志减少,我不直接在articles中修改veiwCount,而使用articles1(精简版articles)作为参数

关于减少日志(个人看法)

执行SQL语句的时候,控制台显示SQL语句和参数,文章内容占控制台的地方太多了,而且也会增加日志了日志的无效内容,例如文章内容并没有修改却出现在了控制台上。

其中SecurityUtils使用还是方案2的。

查看效果

我电脑当前的时间和updateTime时间是不相同的。

可能有人会问这个不是输出updateBy is null了吗?

在数据库中这个数据的update_by是null

我这里是判断,如果为空就填充

可能又有人会问,如果数据库中就是有个文章的update_by为null,那这不就是一个Bug吗?

这种情况可能是程序的bug,让update_by为null,或者人为修改。

我只能我的能力有限,如果有大佬有思路,留下宝贵的建议。

解决方案4(推荐) —— 2023-09-12

到现在方案3还是有问题的,现在有些更新功能出现了问题,例如更新文章功能,一般是先获取文章信息,然后再更改部分信息,调用update接口,这时更新时间和更新人是需要改变的,但是因为有值没有改变,这个就出现了bug。(感谢weixin_52319402大佬提醒)

自己写SQL,不使用Mybatis Plus的方法,即不使用自动填充。

其实一开始就应该这样解决,因为牵扯的字段只有两个,我没有必要大费周章。

分析了一下为什么之前没有想到的原因

原因1:太懒,敲得代码较少,思考较少

原因2:没有写经常SQL的习惯,害怕错误、

当然,后来敲代码在加上实习了两个月自己的实力有所进步,所以现在可以想到这些。加油吧

前提条件:我把更新文章viewCount的service接口名改了,原因是本来的接口是更新redis的viewCount,但是我现在要自己写SQL了,这个接口就是更新viewCount,所以做了区分(主要没有名字可以起了,起名太难了哭)。

下面正文

这里就放mapper了,注意第一行是mapper接口中的代码,后面的代码是xml的代码

void updateViewCount(@Param("map") Map<String, Integer> viewCountMap);


<update id="updateViewCount">
    <if test="map != null and map.size > 0">
        <foreach item="value" index="key" collection="map">
            <if test="key != null and key != '' and value != null">
                update power_article
                set view_count = #{value}
                where id = #{key}
                and del_flag = 0;
            </if>
        </foreach>
    </if>
</update>

可能有人看到之前的insertFill是可以修改的,因为SecurityUtils.getUserId()在内部已经处理了为空的情况,不会抛出异常

这次我把这个完善了一下

  • 15
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值