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

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

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

问题出处:

B站:https://www.bilibili.com/video/BV1hq4y1F7zk?p=83

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() ~[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() ~[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.(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.(BaseStatementHandler.java:69) ~[mybatis-3.5.7.jar:3.5.7]
    at org.apache.ibatis.executor.statement.PreparedStatementHandler.(PreparedStatementHandler.java:41) ~[mybatis-3.5.7.jar:3.5.7]
    at org.apache.ibatis.executor.statement.RoutingStatementHandler.(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数据存入数据库相关问题_第1张图片

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

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

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

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

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

定时任务中将redis数据存入数据库相关问题_第2张图片

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

定时任务中将redis数据存入数据库相关问题_第3张图片

getUserId()这个方法

定时任务中将redis数据存入数据库相关问题_第4张图片

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

定时任务中将redis数据存入数据库相关问题_第5张图片

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

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

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

定时任务中将redis数据存入数据库相关问题_第6张图片

异常信息的下面

定时任务中将redis数据存入数据库相关问题_第7张图片

上图可见,1处报异常

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

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

定时任务中将redis数据存入数据库相关问题_第8张图片

测试结果:

定时任务中将redis数据存入数据库相关问题_第9张图片

解决方案2(推荐):

从根源修改:

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

定时任务中将redis数据存入数据库相关问题_第10张图片

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;
    }

定时任务中将redis数据存入数据库相关问题_第11张图片

测试结果:

定时任务中将redis数据存入数据库相关问题_第12张图片

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

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

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

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

定时任务中将redis数据存入数据库相关问题_第13张图片

下面是定时任务的修改

    @Scheduled(cron = "0/10 * * * * ?")
    public void updateViewCount() {
        //获取redis中的浏览量
        Map viewCountMap = redisCache.getCacheMap("article:viewCount");
        //获取Id集合
        List collect = viewCountMap.keySet()
                .stream()
                .map(Long::valueOf)
                .collect(Collectors.toList());
        //从数据库查到的数据
        List
articles = new ArrayList<>(articleService.listByIds(collect)); //这个主要简化更改内容的,如果直接使用articles也可以,但是很多无效修改 List
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时间是不相同的。

定时任务中将redis数据存入数据库相关问题_第14张图片

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

定时任务中将redis数据存入数据库相关问题_第15张图片

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

定时任务中将redis数据存入数据库相关问题_第16张图片

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

定时任务中将redis数据存入数据库相关问题_第17张图片

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

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

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

定时任务中将redis数据存入数据库相关问题_第18张图片

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

这次我把这个完善了一下

定时任务中将redis数据存入数据库相关问题_第19张图片

你可能感兴趣的:(博客项目遇到的问题及解决方案,java)