关于springboot:spring-boot-实现锁的几种方式

101次阅读

共计 4600 个字符,预计需要花费 12 分钟才能阅读完成。

因为以后的我的项目中因为多线程操作同一个实体,会呈现数据笼罩的问题,后保留的实体把先保留的实体的数据给笼罩了。

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

但写到最初发现,其实本人能够写 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);
  }

测试后果:为 15

阐明在高并发量的状况下,呈现了数据笼罩的状况,线程并没有期待上一个线程实现再进行操作,100 个线程进行 + 1 操作,最终只加了 15.

应用 synchronized 时

service:

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

测试后果:

测试后果为 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 同一个办法一起应用的状况下会呈现,锁曾经去除,然而事务还没提交的状况,造成脏读和数据不一致性等状况.

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

所以会呈现这种状况

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

解决:能够把 @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…

正文完
 0