spring boot 锁

由于当前的项目中由于多线程操作同一个实体,会出现数据覆盖的问题,后保存的实体把先保存的实体的数据给覆盖了。

于是查找了锁的实现的几种方式。

但写到最后发现,其实自己可以写sql 更新需要更新的字段即可,这个操作放在文章尾部。

先来说一下实现锁的几种方式。

对方法加锁

不同于对数据库数据加锁,这种方式是对类中的某个方法加锁

synchronized

java中已经有了内置锁:synchronized, 它的特点是使用简单,一切交给JVM去处理,不需要显示释放

示例:

public class SynchronizedMethod {
    public synchronized void method() {
        System.out.println("Hello World!");
    }
}

如代码可见,只要方法上加上synchronized关键字就可以了。

现在来测试一下

不使用synchronized时

测试: test字段初始化为0,通过new Thread测试100个线程高并发量下test每次 + 1。

controller:

public void batchSave() {
        for (int i = 0; i < 100; i++) {
            int finalI = i;
            new Thread(() -> {
                logService.test();
            }).start();
        }

service:

 @Override
  public void test() {
    Client client = clientRepository.findById(9L).get();
    client.setTest(client.getTest() + 1);
    clientRepository.save(client);
  }

测试结果:为15image.png

说明在高并发量的情况下,出现了数据覆盖的情况,线程并没有等待上一个线程完成再进行操作,100个线程进行+1操作,最终只加了15.

使用synchronized时

service:

 @Override
  public synchronized void test() {
    Client client = clientRepository.findById(9L).get();
    client.setTest(client.getTest() + 1);
    clientRepository.save(client);
  }

测试结果:image.png

测试结果为100符合预期,高并发量下,每个线程都排队进行访问修改,100个线程进行+1操作,最终加了100.

原理:从语法上讲,Synchronized可以把任何一个非null对象作为"锁",这个锁的名字是monitor。

当synchronized作用在实例方法时,监视器锁(monitor)便是对象实例(this);
当synchronized作用在静态方法时,监视器锁(monitor)便是对象的Class实例,因为Class数据存在于永久代,因此静态方法锁相当于该类的一个全局锁;
当synchronized作用在某一个对象实例时,监视器锁(monitor)便是括号括起来的对象实例;

简单来说,该方法的monitor只能被一个线程占用,如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor不被占用。这就完成了锁的实现。

ReentrantLock

java.util.concurrent.locks包提供的ReentrantLock加锁。

ReentrantLock示例:

private final Lock lock = new ReentrantLock();
    public void add() {
        lock.lock();
        try {
          // 代码
        } finally {
            lock.unlock();
        }
    }

加锁之后,我们需要在finally中释放锁

这种锁的其实就是在类中设置一个变量,我们可以对它加锁和解锁。当锁定状态时,其他线程需要等待。

相比synchronized,显示锁可以用非阻塞的方式获取锁,可以响应程序中断,可以设定程序的阻塞时间,拥有更加灵活的操作。相关具体操作可以去查询,在此不过多展示。

例如: 可以设置tryLock,如果1秒未获取到锁则不执行

if (lock.tryLock(1, TimeUnit.SECONDS)) {
    try {
        ...
    } finally {
        lock.unlock();
    }
}

ReentrantLock有两个构造方法。

public ReentrantLock()
public ReentrantLock(boolean fair)

参数fair表示是否保证公平,在不指定的情况下默认值为false,表示不保证公平。

公平表示:等待时间最长的线程优先获取锁。


事务和锁发生的异常

注意:这两种锁在在和@Transational同一个方法一起使用的情况下会出现,锁已经去除,但是事务还没提交的情况,造成脏读和数据不一致性等情况.
image.png

由于事务的提交在方法运行结束之后,并且事务真正启动在执行到它们之后的第一个操作 InnoDB 表的语句的时候

所以会出现这种情况
image.png

例如上一个线程还没来得及提交事务,所以当前线程访问到的数据库还是原来的数据,这就造成了读取的库存数据不是最新的。

解决:可以把@Transational和业务逻辑的代码单独提到一个service里。

public void test() {
    lock.lock();
    try {
      service.update
    } finally {
      lock.unlock();
    }
  }
   @Transactional
    public void update(int id) {
        /*
          业务代码
         */
    }

总结:

锁的实现:
Synchronized是依赖于JVM实现的,而ReenTrantLock是JDK实现的,有什么区别,说白了就类似于操作系统来控制实现和用户自己敲代码实现的区别。前者的实现是比较难见到的,后者有直接的源码可供阅读。

性能的区别:在Synchronized优化后,其实性能差不多。

ReenTrantLock独有的能力:
ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。还可以使用tryLock等操作。

对这两种来说,我比较推荐用ReenTrantLock,更灵活,并且代码可读性更高。如果用synchronized,别人读代码可能容易忽略。

对数据库数据加锁

相比上面的对方法加锁来说,这种方式是对数据库加锁。对于多个不同的service都操作同一数据的话,我们就需要对数据库上锁了。

又分为悲观锁和乐观锁:

悲观锁

悲观锁顾名思义就是悲观的认为自己操作的数据都会被其他线程操作,所以就必须自己独占这个数据,可以理解为”独占锁“。上面提到的中synchronized和ReentrantLock等锁就是悲观锁,数据库中表锁、行锁、读写锁等也是悲观锁。

实现原理很简单: 在 where语句后面加上for update就行了。这样就能锁住这条数据。不过要注意的是,注意查询条件必须要是索引列(这里设置的是id),如果不是索引就会变成表锁,把整个表都锁住。

@Query(value = "select * from client a where a.id = :id for update", nativeQuery = true)
Optional<Client> findClientForUpdate(Long id);

JPA有提供一个更简洁的方式,就是@Lock注解

    /**
     * 查询时加上悲观锁
     * 在我们没有将其提交事务之前,其他线程是不能获取修改的,需要等待
     * @param id clientId
     * @return
     */
    @Lock(value = LockModeType.PESSIMISTIC_WRITE)
    @Query("select a from Client a where a.id = :id")
    Optional<Client> findClientByIdWithPessimisticLock(Long id);

原理还是for update,只不过是JPA帮我们加了而已。

具体测试悲观锁的过程可以看我这篇文章:https://segmentfault.com/a/11...

乐观锁

原理就是在实体中加一个字段当作版本号,比如我们加个字段version。

@Data
@Entity
public class Client {
    ...
    private Long version;
}

当我们client进行更新的时候,例如我们拿到version是1,操作完成之后要插入的时候,发现version变成2了,哎这就不对了,肯定是有其他人更改了数据,那这时候我就不能插入了。

@Query(value = "update client set name = :name, version = version + 1 where id = :id and version = :version", nativeQuery = true)
int updateClinetWithVersion(Long id, String naem, Long version);

可以看到update的where有一个判断version的条件,并且会set version = version + 1。这就保证了只有当数据库里的版本号和要更新的实体类的版本号相同的时候才会更新数据。

这个返回值代表更新了的数据库行数,如果值为0的时候没有更新,说明版本号发生错误。

与悲观锁相同,jpa也提供了乐观锁的实现方式。

@Data
@Entity
public class Article {
    ...
    
    @Version
    private Long version;
}

使用了@version之后,我们不需要再自己写仓库层, 正常使用findById,save方法即可。

public void update(Long id, String name) {
    Client client = repository.findById(id).get();
    client.setName(name);
    repository.save(client);
}

实现乐观锁之后,如果没有成功更新数据则抛出异常回滚保证数据

乐观锁适合写少读多的场景,写多的情况会经常回滚,消耗性能。

悲观锁适合写多读少的场景,使用的时候该线程会独占这个资源。

参考文章:https://segmentfault.com/a/11...

回到开头

开头讲到,数据覆盖的时候不一定需要使用锁,也可以自己写sql,更新需要更新的字段即可。

例如一个线程更新的是age字段,一个是name字段,那我们可以写更新语句,只更新实体的名字。

@Modifying
@Query (value = "update client set name = :name where id = :id")
public void updateNameById(String name, int id);

public update(int id){
 
   Client client = repository.findById(id);
   client.setName("name");//注意不要这么写
   repository.updateNameById("name",id);    

注意这里不要使用set,否则发现数据还是会被覆盖。

这是jpa自己的特性,自动更新

当实体对象属于托管状态下时,往这个对象里面的某个属性set新的值,jpa检测到有变化,就会自动更新entity 的数据到db中。

具体可以查看这篇文章:https://blog.csdn.net/janet11...

622 声望
104 粉丝
0 条评论
推荐阅读
为什么要有Java 内存模型,是什么, 解决了什么
Java内存模型(Java Memory Model,JMM) 定义了 java 运行时如何与硬件内存进行交互,比如规定了一个线程如何看到其他内存修改后共享变量的值。一些高级特性也建立在JMM的基础上,比如volatile 关键字。

weiweiyi3阅读 468

Spring Aop 动态代理
为了保持行为的一致性,代理类和委托类通常会实现相同的接口,所以在访问者看来两者没有丝毫的区别。通过代理类这中间一层,能有效控制对委托类对象的直接访问,也可以很好地隐藏和保护委托类对象,同时也为实施...

KerryWu5阅读 8.7k评论 1

SpringBoot集成LibreOffice+jodconverter做文件预览(office转pdf)
LibreOffice 是一款开放源代码的自由免费全能办公软件,可运行于 Microsoft Windows, GNU/Linux 以及 macOS 等操作系统上。它包含了 Writer, Calc, Impress, Draw, Math 以及 Base 等组件,可分别用于文本文档、...

Zeran2阅读 6.2k

Spring系列-实战篇(5)-数据库的事务和锁
大学里面数据库课考试,事务和锁的相关知识绝对是要划的重点。数据库的事务要遵循ACID(原子性、一致性、隔离性、持久性)四要素,锁又有悲观锁和乐观锁的划分方式。那么今天我们讲讲,如何基于SpringBoot+Mybati...

KerryWu2阅读 6k评论 1

之前很火给女朋友推送微信服务号消息是怎么做的?
经过了几天的奋战,终于把微信服务号的模板消息给写完了。后端其实没花多少时间,因为之前已经有同学提过pull request了,我在这基础之上简单优化下就完事了,主要的时间都是花在前端上,对前端页面和参数的适配...

Java3y3阅读 1.2k

简单使用spring cloud 服务注册做一个请求转发中心
背景上篇文章 记录多项目共用一个公众号逻辑修改, 实现了多个项目共用一个公众号。 但是也存在几点问题,比如:中间服务器拦截了微信的请求,虽然方便了项目不再需要写微信授权的代码,但如果以后需要再拓展新的...

weiweiyi2阅读 745

消息推送平台终于要上线啦!
我的开源项目消息推送平台Austin终于要上线了,迎来在线演示的第一版!🔥项目在线演示地址:[链接]消息推送平台🔥推送下发【邮件】【短信】【微信服务号】【微信小程序】【企业微信】【钉钉】等消息类型。[链接][链...

Java3y3阅读 1k

622 声望
104 粉丝
宣传栏