美文网首页SpringBoot专题
SpringBoot入门建站全系列(二十一)Mybatis使用乐

SpringBoot入门建站全系列(二十一)Mybatis使用乐

作者: 逍遥天扬 | 来源:发表于2019-08-12 09:34 被阅读11次

    SpringBoot入门建站全系列(二十一)Mybatis使用乐观锁与悲观锁

    一、概述

    之前有两篇《SpringBoot入门建站全系列(三)Mybatis操作数据库》《SpringBoot入门建站全系列(四)Mybatis使用进阶篇:动态SQL与分页》介绍了Springboot如何结合Mybatis进行数据库访问操作。这一篇介绍下springboot环境下Mybatis如何进行乐观锁、悲观锁的使用。

    悲观锁和乐观锁的概念:

    • 悲观锁:就是独占锁,不管读写都上锁了。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。

    •  

    • 乐观锁:不上锁,读取的时候带版本号,写入的时候带着这个版本号,如果不一致就失败,乐观锁适用于多读的应用类型,因为写多的时候会经常失败。

    代码可以在Springboot组件化构建https://www.pomit.cn/java/spring/springboot.html中的MybatisLock组件中查看,并下载。

    首发地址:

      品茗IT-同步发布

    品茗IT提供在线支持:

      一键快速构建Spring项目工具

      一键快速构建SpringBoot项目工具

      一键快速构建SpringCloud项目工具

      一站式Springboot项目生成

      Mysql一键生成Mybatis注解Mapper

      Mysql一键生成SpringDataRest项目

    如果大家正在寻找一个java的学习环境,或者在开发中遇到困难,可以加入我们的java学习圈,点击即可加入,共同学习,节约学习时间,减少很多在学习中遇到的难题。

    二、配置

    本文假设你已经引入spring-boot-starter-web。已经是个SpringBoot项目了,如果不会搭建,可以打开这篇文章看一看《SpringBoot入门建站全系列(一)项目建立》

    2.1 Maven依赖

    需要引入spring-boot-starter-data-jpa,这里要访问数据库,所以要依赖数据库相关jar包。

            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
            </dependency>
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
            </dependency>
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-dbcp2</artifactId>
            </dependency>
    

    2.2 配置文件

    在application.properties 中需要添加下面的配置:

    spring.datasource.type=org.apache.commons.dbcp2.BasicDataSource
    spring.datasource.dbcp2.max-wait-millis=60000
    spring.datasource.dbcp2.min-idle=20
    spring.datasource.dbcp2.initial-size=2
    spring.datasource.dbcp2.validation-query=SELECT 1
    spring.datasource.dbcp2.connection-properties=characterEncoding=utf8
    spring.datasource.dbcp2.validation-query=SELECT 1
    spring.datasource.dbcp2.test-while-idle=true
    spring.datasource.dbcp2.test-on-borrow=true
    spring.datasource.dbcp2.test-on-return=false
    
    spring.datasource.driverClassName = com.mysql.jdbc.Driver
    spring.datasource.url=jdbc:mysql://127.0.0.1:3306/boot?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
    spring.datasource.username=cff
    spring.datasource.password=123456
    
    mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
    
    

    这里面,包含了数据库连接信息、数据源的连接池配置信息、mybatis配置信息。

    • spring.datasource.dbcp2是配置dbcp2的连接池信息;
    • spring.datasource.type指明数据源的类型;
    • 最上面的spring.datasource.xxx指明数据库连接池信息;
    • mybatis.configuration.log-impl指明mybatis的日志打印方式

    三、悲观锁

    悲观锁在数据库的访问中使用,表现为:前一次请求没执行完,后面一个请求就一直在等待。

    3.1 Dao层

    数据库要实现悲观锁,就是将sql语句带上for update即可。 for update 是行锁

    所在mybatis的查询sql加上for update,就实现了对当前记录的锁定,就实现了悲观锁。

    UserInfoDao :

    package com.cff.springbootwork.mybatislock.dao;
    
    import org.apache.ibatis.annotations.Insert;
    import org.apache.ibatis.annotations.Mapper;
    import org.apache.ibatis.annotations.Param;
    import org.apache.ibatis.annotations.Select;
    import org.apache.ibatis.annotations.Update;
    
    import cn.pomit.springwork.mybatislock.domain.UserInfo;
    
    @Mapper
    public interface UserInfoDao {
        
        @Select({
            "<script>",
                "SELECT ",
                "user_name as userName,passwd,name,mobile,valid, user_type as userType, version as version",
                "FROM user_info_test",
                "WHERE user_name = #{userName,jdbcType=VARCHAR} for update",
           "</script>"})
        UserInfo findByUserNameForUpdate(@Param("userName") String userName);
        
        @Update({
            "<script>",
            " update user_info_test set",
            " name = #{name, jdbcType=VARCHAR}, mobile = #{mobile, jdbcType=VARCHAR},version=version+1 ",
            " where user_name=#{userName}",
            "</script>"
        })
        int update(UserInfo userInfo);
    
        @Insert({
            "<script>",
            "INSERT INTO user_info_test",
            "( user_name,",
            "name ,",
            "mobile,",
            "passwd,",
            "version",
             ") ",
            " values ",
             "( #{userName},",
             "#{name},",
             "#{mobile},",
             "#{passwd},",
             "#{version}",
            " ) ",
            "</script>"
        })
        int save(UserInfo entity);
    }
    
    

    这里,findByUserNameForUpdate的sql中加上了for update。update就是普通的更新而已。

    3.2 Service层

    更新数据库前,先调用findByUserNameForUpdate方法,使上面的配置的悲观锁锁定表记录,然后再更新。

    UserInfoService :

    package com.cff.springbootwork.mybatislock.service;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    import org.springframework.util.StringUtils;
    
    import cn.pomit.springwork.mybatislock.domain.UserInfo;
    import cn.pomit.springwork.mybatislock.mapper.UserInfoDao;
    
    @Service
    public class UserInfoService {
        @Autowired
        UserInfoDao userInfoDao;
    
        public void save(UserInfo entity) {
            entity.setVersion(0);
            userInfoDao.save(entity);
        }
        
    
        @Transactional
        public UserInfo getUserInfoByUserNamePessimistic(String userName) {
            return userInfoDao.findByUserNameForUpdate(userName);
        }
        
        @Transactional
        public void updateWithTimePessimistic(UserInfo entity, int time) throws InterruptedException {      
            UserInfo userInfo = userInfoDao.findByUserNameForUpdate(entity.getUserName());
            if (userInfo == null)
                return;
    
            if (!StringUtils.isEmpty(entity.getMobile())) {
                userInfo.setMobile(entity.getMobile());
            }
            if (!StringUtils.isEmpty(entity.getName())) {
                userInfo.setName(entity.getName());
            }
            Thread.sleep(time * 1000L);
    
            userInfoDao.update(userInfo);
        }
        
        @Transactional
        public void updatePessimistic(UserInfo entity) {
            UserInfo userInfo = userInfoDao.findByUserNameForUpdate(entity.getUserName());
            if (userInfo == null)
                return;
    
            if (!StringUtils.isEmpty(entity.getMobile())) {
                userInfo.setMobile(entity.getMobile());
            }
            if (!StringUtils.isEmpty(entity.getName())) {
                userInfo.setName(entity.getName());
            }
    
            userInfoDao.update(userInfo);
        }
    
    }
    
    

    测试中,我们在update方法中sleep几秒,其他线程的update将一直等待。

    3.3 测试Web层

    可以先调用/update/{time}接口,延迟执行,然后马上调用/update接口,会发现,/update接口一直在等待/update/{time}接口执行完成。

    MybatisPessLockRest :

    package com.cff.springbootwork.mybatislock.web;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RestController;
    
    import cn.pomit.springwork.mybatislock.domain.UserInfo;
    import cn.pomit.springwork.mybatislock.service.UserInfoService;
    
    /**
     * 测试悲观锁
     * 
     * @author fufei
     *
     */
    @RestController
    @RequestMapping("/mybatispesslock")
    public class MybatisPessLockRest {
    
        @Autowired
        UserInfoService userInfoService;
    
        @RequestMapping(value = "/detail/{name}", method = { RequestMethod.GET })
        public UserInfo detail(@PathVariable("name") String name) {
            return userInfoService.getUserInfoByUserNamePessimistic(name);
        }
    
        @RequestMapping(value = "/save")
        public String save(@RequestBody UserInfo userInfo) throws InterruptedException {
            userInfoService.save(userInfo);
            return "0000";
        }
    
        @RequestMapping(value = "/update/{time}")
        public String update(@RequestBody UserInfo userInfo, @PathVariable("time") int time) throws InterruptedException {
            userInfoService.updateWithTimePessimistic(userInfo, time);
    
            return "0000";
        }
    
        @RequestMapping(value = "/update")
        public String update(@RequestBody UserInfo userInfo) throws InterruptedException {
            userInfoService.updatePessimistic(userInfo);
            return "0000";
        }
    }
    
    
    

    四、乐观锁

    数据库访问dao层还是3.1那个UserInfoDao。

    4.1 Dao层

    UserInfoDao更新时,需要携带version字段进行更新:and version = #{version}。如果version不一致,是不会更新成功的,这时候,我们的select查询是不能带锁的。

    UserInfoDao :

    package com.cff.springbootwork.mybatislock.dao;
    
    import org.apache.ibatis.annotations.Insert;
    import org.apache.ibatis.annotations.Mapper;
    import org.apache.ibatis.annotations.Param;
    import org.apache.ibatis.annotations.Select;
    import org.apache.ibatis.annotations.Update;
    
    import cn.pomit.springwork.mybatislock.domain.UserInfo;
    
    @Mapper
    public interface UserInfoDao {
        @Select({
            "<script>",
                "SELECT ",
                "user_name as userName,passwd,name,mobile,valid, user_type as userType, version as version",
                "FROM user_info_test",
                "WHERE user_name = #{userName,jdbcType=VARCHAR}",
           "</script>"})
        UserInfo findByUserName(@Param("userName") String userName);
        
        @Update({
            "<script>",
            " update user_info_test set",
            " name = #{name, jdbcType=VARCHAR}, mobile = #{mobile, jdbcType=VARCHAR},version=version+1 ",
            " where user_name=#{userName} and version = #{version}",
            "</script>"
        })
        int updateWithVersion(UserInfo userInfo);
    
        @Insert({
            "<script>",
            "INSERT INTO user_info_test",
            "( user_name,",
            "name ,",
            "mobile,",
            "passwd,",
            "version",
             ") ",
            " values ",
             "( #{userName},",
             "#{name},",
             "#{mobile},",
             "#{passwd},",
             "#{version}",
            " ) ",
            "</script>"
        })
        int save(UserInfo entity);
    }
    
    

    4.2 Service层

    service层我们做一下简单的调整。更新数据库前,先调用findByUserName方法,查询出当前的版本号,然后再更新。

    UserInfoService :

    package com.cff.springbootwork.mybatislock.service;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    import org.springframework.util.StringUtils;
    
    import cn.pomit.springwork.mybatislock.domain.UserInfo;
    import cn.pomit.springwork.mybatislock.mapper.UserInfoDao;
    
    @Service
    public class UserInfoService {
        @Autowired
        UserInfoDao userInfoDao;
        public UserInfo getUserInfoByUserName(String userName){
            return userInfoDao.findByUserName(userName);
        }
    
        public void save(UserInfo entity) {
            entity.setVersion(0);
            userInfoDao.save(entity);
        }
        @Transactional
        public void updateWithTimeOptimistic(UserInfo entity, int time) throws Exception {      
            UserInfo userInfo = userInfoDao.findByUserName(entity.getUserName());
            if (userInfo == null)
                return;
    
            if (!StringUtils.isEmpty(entity.getMobile())) {
                userInfo.setMobile(entity.getMobile());
            }
            if (!StringUtils.isEmpty(entity.getName())) {
                userInfo.setName(entity.getName());
            }
            Thread.sleep(time * 1000L);
    
            int ret = userInfoDao.updateWithVersion(userInfo);
            if(ret < 1)throw new Exception("乐观锁导致保存失败");
        }
    
        @Transactional
        public void updateOptimistic(UserInfo entity) throws Exception {
            UserInfo userInfo = userInfoDao.findByUserName(entity.getUserName());
            if (userInfo == null)
                return;
    
            if (!StringUtils.isEmpty(entity.getMobile())) {
                userInfo.setMobile(entity.getMobile());
            }
            if (!StringUtils.isEmpty(entity.getName())) {
                userInfo.setName(entity.getName());
            }
    
            int ret = userInfoDao.updateWithVersion(userInfo);
            if(ret < 1)throw new Exception("乐观锁导致保存失败");
        }
    
    }
    
    

    4.2 测试Web层

    可以先调用/update/{time}接口,延迟执行,然后马上调用/update接口,会发现,/update接口不会等待/update/{time}接口执行完成,读取完版本号能够成功更新数据,但是/update/{time}接口等待足够时间以后,更新的时候会失败,因为它的版本和数据库的已经不一致了。

    注意: 这里更新失败不会抛异常,但是返回值会是0,即更新不成功,需要自行判断。jpa的乐观锁可以抛出异常,手动catch到再自行处理。

    MybatisOptiLockRest :

    package com.cff.springbootwork.mybatislock.web;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RestController;
    
    import cn.pomit.springwork.mybatislock.domain.UserInfo;
    import cn.pomit.springwork.mybatislock.service.UserInfoService;
    
    /**
     * 测试乐观锁
     * @author fufei
     *
     */
    @RestController
    @RequestMapping("/mybatislock")
    public class MybatisOptiLockRest {
    
        @Autowired
        UserInfoService userInfoService;
    
        @RequestMapping(value = "/detail/{name}", method = { RequestMethod.GET })
        public UserInfo detail(@PathVariable("name") String name) {
            return userInfoService.getUserInfoByUserName(name);
        }
    
        @RequestMapping(value = "/save")
        public String save(@RequestBody UserInfo userInfo) throws InterruptedException {
            userInfoService.save(userInfo);
            return "0000";
        }
    
        @RequestMapping(value = "/update/{time}")
        public String update(@RequestBody UserInfo userInfo, @PathVariable("time") int time) throws Exception {
            userInfoService.updateWithTimeOptimistic(userInfo, time);
    
            return "0000";
        }
    
        @RequestMapping(value = "/update")
        public String update(@RequestBody UserInfo userInfo) throws Exception {
            userInfoService.updateOptimistic(userInfo);
            return "0000";
        }
    }
    
    

    五、过程中用到的完整实体和Service

    UserInfo:

    
    
    

    UserInfoService :

    
    

    UserInfoDao:

    
    

    详细完整的实体及代码,可以访问品茗IT-博客《SpringBoot入门建站全系列(二十一)Mybatis使用乐观锁与悲观锁》进行查看

    品茗IT-博客专题:https://www.pomit.cn/lecture.html汇总了Spring专题Springboot专题SpringCloud专题web基础配置专题。

    快速构建项目

    Spring组件化构建

    SpringBoot组件化构建

    SpringCloud服务化构建

    喜欢这篇文章么,喜欢就加入我们一起讨论SpringBoot使用吧!


    品茗IT交流群

    相关文章

      网友评论

        本文标题:SpringBoot入门建站全系列(二十一)Mybatis使用乐

        本文链接:https://www.haomeiwen.com/subject/svlyjctx.html