美文网首页Spring之路
Spring整合SpringDataJpa的乐观锁与悲观锁详情

Spring整合SpringDataJpa的乐观锁与悲观锁详情

作者: 逍遥天扬 | 来源:发表于2019-07-30 09:21 被阅读2次

    Spring整合SpringDataJpa的乐观锁与悲观锁详情

    一、概述

    上一篇《Spring和SpringDataJpa整合详解》介绍了Spring如何结合Spring-data-jpa进行数据库访问操作。这一篇介绍下springmvc环境下spring-data-jpa如何进行乐观锁、悲观锁的使用。

    悲观锁和乐观锁的概念:

    • 悲观锁:就是独占锁,不管读写都上锁了。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
    • 乐观锁:不上锁,读取的时候带版本号,写入的时候带着这个版本号,如果不一致就失败,乐观锁适用于多读的应用类型,因为写多的时候会经常失败。

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

    首发地址:

    二、环境配置

    本文假设你已经引入Spring必备的一切了,已经是个Spring项目了,如果不会搭建,可以打开这篇文章看一看《Spring和Spring Mvc 5整合详解》

    2.1 maven依赖

    和上一篇《Spring和SpringDataJpa整合详解》的配置一样,
    使用Spring-data-jpa需要引入spring-data-jpa,因为是非Springboot项目,我们不能通过starter引入,需要引入spring-data-jpa、javax.transaction-api、hibernate-core。

    <?xml version="1.0"?>
    <project
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
        xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
        <modelVersion>4.0.0</modelVersion>
        <parent>
            <groupId>cn.pomit</groupId>
            <artifactId>SpringWork</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </parent>
        <artifactId>JpaLock</artifactId>
        <packaging>jar</packaging>
        <name>JpaLock</name>
        <url>http://maven.apache.org</url>
        <dependencies>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-context</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-orm</artifactId>
            </dependency>
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
            </dependency>
            <dependency>
                <groupId>log4j</groupId>
                <artifactId>log4j</artifactId>
            </dependency>
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-dbcp2</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.data</groupId>
                <artifactId>spring-data-jpa</artifactId>
                <version>2.0.10.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>javax.transaction</groupId>
                <artifactId>javax.transaction-api</artifactId>
                <version>1.2</version>
            </dependency>
            <dependency>
                <groupId>org.hibernate</groupId>
                <artifactId>hibernate-core</artifactId>
                <version>5.2.17.Final</version>
                <scope>compile</scope>
                <exclusions>
                    <exclusion>
                        <artifactId>jboss-transaction-api_1.2_spec</artifactId>
                        <groupId>org.jboss.spec.javax.transaction</groupId>
                    </exclusion>
                </exclusions>
            </dependency>
        </dependencies>
        <build>
            <finalName>JpaLock</finalName>
        </build>
    </project>
    
    

    父模块可以在https://www.pomit.cn/spring/SpringWork/pom.xml获取。

    2.2 Spring配置

    需要配置数据源、jdbcTemplate、entityManagerFactory、transactionManager和jpa:repositories。

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
        xmlns:tx="http://www.springframework.org/schema/tx" xmlns:jpa="http://www.springframework.org/schema/data/jpa"
        xmlns:context="http://www.springframework.org/schema/context"
        xsi:schemaLocation="
                        http://www.springframework.org/schema/beans
                        http://www.springframework.org/schema/beans/spring-beans.xsd
                        http://www.springframework.org/schema/tx 
                        http://www.springframework.org/schema/tx/spring-tx.xsd
                        http://www.springframework.org/schema/aop 
                        http://www.springframework.org/schema/aop/spring-aop.xsd
                        http://www.springframework.org/schema/context      
                        http://www.springframework.org/schema/context/spring-context.xsd
                        http://www.springframework.org/schema/data/jpa
                        http://www.springframework.org/schema/data/jpa/spring-jpa.xsd">
    
        <context:annotation-config />
        <context:component-scan base-package="cn.pomit.springwork.springdatajpa">
        </context:component-scan>
    
        <bean id="annotationPropertyConfigurerJpaLock"
            class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
            <property name="order" value="1" />
            <property name="ignoreUnresolvablePlaceholders" value="true" />
            <property name="locations">
                <list>
                    <value>classpath:db.properties</value>
                </list>
            </property>
        </bean>
    
    
        <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource"
            destroy-method="close">
            <property name="driverClassName" value="${db.dirverClass}"></property>
            <property name="url" value="${db.url}" />
            <property name="username" value="${db.username}" />
            <property name="password" value="${db.password}" />
    
            <property name="initialSize" value="1" />
            <property name="minIdle" value="1" />
            <property name="maxTotal" value="20" />
    
            <property name="validationQuery" value="SELECT 1" />
            <property name="testWhileIdle" value="true" />
            <property name="testOnBorrow" value="false" />
            <property name="testOnReturn" value="false" />
        </bean>
    
        <!-- jdbcTemplate -->
        <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
            <property name="dataSource" ref="dataSource"></property>
        </bean>
    
        <bean id="entityManagerFactory"
            class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
            <property name="dataSource" ref="dataSource"></property>
            <property name="packagesToScan" value="cn.pomit.springwork.springdatajpa.domain"></property>
            <property name="jpaVendorAdapter">
                <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
                    <property name="databasePlatform" value="org.hibernate.dialect.MySQL5Dialect">
                </property>
                </bean>
            </property>
        </bean>
        <jpa:repositories base-package="cn.pomit.springwork.springdatajpa.dao" />
        
        <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
            <property name="entityManagerFactory" ref="entityManagerFactory" />
        </bean>
        <!-- 使用annotation定义事务 -->
        <tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true" />
    </beans>
    

    这里面,需要注意的是:

    • entityManagerFactory,是实体和数据库选择信息。

    • jpa:repositories,指明Spring-data-jpa的repositories地址。就是我们的数据库交互层。

    • transactionManager,事务处理器。

    • tx:annotation-driven:开启事务注解。

    db.properties中存放数据库的地址端口等连接信息。

    db.properties:

    db.url=jdbc:mysql://127.0.0.1:3306/boot?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
    db.username=cff
    db.password=123456
    db.dirverClass=com.mysql.cj.jdbc.Driver
    
    

    三、悲观锁

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

    3.1 Dao层

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

    在Jpa的Repository这一层,直接在方法上加上@Lock(LockModeType.PESSIMISTIC_WRITE),就实现了悲观锁。

    UserInfoDao :

    package cn.pomit.springwork.springdatajpa.dao;
    
    import javax.persistence.LockModeType;
    
    import org.springframework.data.jpa.repository.Lock;
    import org.springframework.data.repository.CrudRepository;
    import org.springframework.stereotype.Repository;
    
    import cn.pomit.springwork.springdatajpa.domain.UserInfo;
    
    @Repository
    public interface UserInfoDao extends CrudRepository<UserInfo, String> {
        @Lock(LockModeType.PESSIMISTIC_WRITE)
        UserInfo findByUserName(String userName);
    }
    
    
    

    注意加上@Repository注解。实体要加上@Entity和@Table注解。

    3.2 Service层

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

    UserInfoService :

    package cn.pomit.springwork.springdatajpa.service;
    
    import javax.transaction.Transactional;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.util.StringUtils;
    
    import cn.pomit.springwork.springdatajpa.dao.UserInfoDao;
    import cn.pomit.springwork.springdatajpa.domain.UserInfo;
    
    @Service
    public class UserInfoService {
        @Autowired
        UserInfoDao userInfoDao;
    
        public void delete(String userName) {
            userInfoDao.deleteById(userName);
        }
    
            public void save(UserInfo entity) {
            userInfoDao.save(entity);
        }
    
        @Transactional
        public UserInfo getUserInfoByUserNamePessimistic(String userName) {
            return userInfoDao.findByUserName(userName);
        }
        
        @Transactional
        public void updateWithTimePessimistic(UserInfo entity, int time) throws InterruptedException {      
            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);
    
            userInfoDao.save(userInfo);
        }
        
        @Transactional
        public void updatePessimistic(UserInfo entity) {
            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());
            }
    
            userInfoDao.save(userInfo);
        }
    }
    
    

    3.3 测试Web层

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

    JpaPessLockRest :

    package cn.pomit.springwork.springdatajpa.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.springdatajpa.domain.UserInfo;
    import cn.pomit.springwork.springdatajpa.service.UserInfoService;
    
    /**
     * 测试悲观锁
     * 
     * @author fufei
     *
     */
    @RestController
    @RequestMapping("/jpapesslock")
    public class JpaPessLockRest {
    
        @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那个dao。

    4.1 实体添加@Version

    UserInfo实体增加字段version,并添加注解@Version。当然,数据库也要加上version字段,普通字段就行,别设置成主键自增啥的。

    @Version
    private Integer version;
    

    4.2 Service层

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

    UserInfoService :

    package cn.pomit.springwork.springdatajpa.service;
    
    import javax.transaction.Transactional;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.util.StringUtils;
    
    import cn.pomit.springwork.springdatajpa.dao.UserInfoDao;
    import cn.pomit.springwork.springdatajpa.domain.UserInfo;
    
    @Service
    public class UserInfoService {
        @Autowired
        UserInfoDao userInfoDao;
    
        public UserInfo getUserInfoByUserName(String userName) {
            return userInfoDao.findById(userName).orElse(null);
        }
    
        public void save(UserInfo entity) {
            userInfoDao.save(entity);
        }
        @Transactional
        public void updateWithTimeOptimistic(UserInfo entity, int time) throws InterruptedException {       
            UserInfo userInfo = userInfoDao.findById(entity.getUserName()).orElse(null);
            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.save(userInfo);
        }
    
        public void delete(String userName) {
            userInfoDao.deleteById(userName);
        }
        @Transactional
        public void updateOptimistic(UserInfo entity) {
            UserInfo userInfo = userInfoDao.findById(entity.getUserName()).orElse(null);
            if (userInfo == null)
                return;
    
            if (!StringUtils.isEmpty(entity.getMobile())) {
                userInfo.setMobile(entity.getMobile());
            }
            if (!StringUtils.isEmpty(entity.getName())) {
                userInfo.setName(entity.getName());
            }
    
            userInfoDao.save(userInfo);
        }
    
    
    }
    
    
    

    4.2 测试Web层

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

    JpaOptiLockRest :

    package cn.pomit.springwork.springdatajpa.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.springdatajpa.domain.UserInfo;
    import cn.pomit.springwork.springdatajpa.service.UserInfoService;
    
    /**
     * 测试乐观锁
     * @author fufei
     *
     */
    @RestController
    @RequestMapping("/jpalock")
    public class JpaOptiLockRest {
    
        @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 InterruptedException {
            userInfoService.updateWithTimeOptimistic(userInfo, time);
    
            return "0000";
        }
    
        @RequestMapping(value = "/update")
        public String update(@RequestBody UserInfo userInfo) throws InterruptedException {
            userInfoService.updateOptimistic(userInfo);
            return "0000";
        }
    }
    
    

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

    UserInfo:

    
    

    UserInfoService :

    
    
    

    详细完整的实体,可以访问品茗IT-博客《Spring和SpringDataJpa整合的乐观锁与悲观锁详情》进行查看

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

    快速构建项目

    Spring组件化构建

    SpringBoot组件化构建

    SpringCloud服务化构建

    喜欢这篇文章么,喜欢就加入我们一起讨论Spring技术吧!


    品茗IT交流群

    相关文章

      网友评论

        本文标题:Spring整合SpringDataJpa的乐观锁与悲观锁详情

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