Java锁、悲观乐观锁、分布式锁?细说那年我们用过的锁 - Go语言中文社区

Java锁、悲观乐观锁、分布式锁?细说那年我们用过的锁


Java锁、悲观乐观锁、分布式锁?细说那年我们用过的锁

一、概述

Java锁,指的是应用中使用的锁;应用中在处理线程安全的问题时,常常使用synchronized 或者ReentrantLock等锁来保证线程安全。

悲观锁(Pessimistic Lock),顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。一般是指数据库中的行锁;

乐观锁(Optimistic Lock),顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在提交更新的时候会判断一下在此期间别人有没有去更新这个数据。乐观锁适用于读多写少的应用场景,这样可以提高吞吐量。一般是指数据库中携带version字段进行更新;

分布式锁是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。

首发地址:
品茗IT-同步发布

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

二、Java锁

Java锁还有很多种,但只能在应用内使用,在多机部署的应用间就无法保证数据的一致性。

根据锁的设计,可以分为下面这些(并不是一种设计就对应一种实现,比如ConcurrentHashMap,其并发的实现就是通过分段锁的形式,但它并不是锁):

  • 公平锁/非公平锁
  • 可重入锁
  • 独享锁/共享锁
  • 互斥锁/读写锁
  • 乐观锁/悲观锁
  • 分段锁
  • 偏向锁/轻量级锁/重量级锁
  • 自旋锁

我这里简单使用synchronized 或者ReentrantLock来介绍下JAVA锁。

2.1 synchronized

Java锁中最简单的一种锁,最常见的就是拿它来做线程安全的工厂方法。只需要在方法上加上synchronized即可保证应用内线程安全。


// 单例对象
private static SingletonExample instance = null;

// 静态的工厂方法
public static synchronized SingletonExample getInstance() {
  if (instance == null) {
    instance = new SingletonExample();
  }
  return instance;
}

synchronized准确来说也是悲观锁的一种,因为加了synchronized的方法在应用内同一时间只允许一个线程操作,其他线程将会被阻塞。

2.2 ReentrantLock

ReentrantLock,顾名思义,它是支持可重入锁的锁,是一种递归无阻塞的同步机制。除此之外,该锁还支持获取锁时的公平和非公平选择。ReentrantLock默认构造函数是NonfairSync(非公平锁)。


private ReentrantLock lock = new ReentrantLock();

public void print(int str) {
  try {
    lock.lock();
    System.out.println(str + "获得");
    Thread.sleep((int) (Math.random() * 1000));
  } catch (Exception e) {
    e.printStackTrace();
  } finally {
    System.out.println(str + "释放");
    lock.unlock();
  }
}

公平锁就算,在绝对时间上,先对锁进行获取的请求你一定先被满足,那么这个锁是公平的,反之,是不公平的。

三、悲观锁

上面说到的那两个Java锁,广义上也是悲观锁;然而,现在大多数人提到悲观锁和乐观锁,常常都是指数据库操作过程中的数据一致性处理方式。

下面对Mybatis和Spring-Data-Jpa的悲观锁写法做简单说明。

3.1 Mybatis

Mybatis是需要自己写Sql的,因此,我们只需要在需要使用悲观锁的地方,比如要对某一条数据做更新,可以先select 当前语句,并加上for update,此时这条数据就被加锁。

@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);

Mysql的MyISAM引擎不支持行锁,它的for update是表锁。

参考这篇文章:SpringBoot入门建站全系列(二十一)Mybatis使用乐观锁与悲观锁

3.2 Spring-Data-Jpa

Spring-Data-Jpa可以不手写SQL,当然你也可以手写SQL。要用悲观锁的话,你手写SQL也是没用的。必须使用@Lock注解才能实现悲观锁。

public interface UserInfoDao extends CrudRepository<UserInfo, String> {
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    UserInfo findByUserName(String userName);
}

参考这篇文章:SpringBoot入门建站全系列(二十)SpringDataJpa使用乐观锁与悲观锁

四、乐观锁

在Java锁中,也是有乐观锁的实现的,比如CAS操作,就是乐观锁,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。

但是,我们常说乐观锁,一般都是指数据库的乐观锁使用。下面对Mybatis和Spring-Data-Jpa的乐观锁写法做简单说明。

4.1 Mybatis

乐观锁是需要表中额外增加字段做配合的,一般是version字段。如:


@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);

Mybatis对某条数据做更新时,需要带处理业务前查询出的version字段,如果数据库中version字段值和更新时携带的version值不同,则更新失败(注意:Mybatis更新失败不会报错,只是返回0)。

参考这篇文章:SpringBoot入门建站全系列(二十一)Mybatis使用乐观锁与悲观锁

4.2 Spring-Data-Jpa

Spring-Data-Jpa使用@Version注解来实现乐观锁,同时数据库表中要有version字段。如在实体上加上@Version注解:

@Version
private Integer version;

Spring-Data-Jpa对某条数据做更新时,如果实体的字段上有@Version注解,会检测version字段值和数据库是否一致,不一致,会抛出optimistic locking failed异常。这时需要自己捕获异常进行处理。

参考这篇文章:SpringBoot入门建站全系列(二十)SpringDataJpa使用乐观锁与悲观锁

五、分布式锁

数据库的悲观锁和乐观锁也能保证不同主机共享数据的一致性。但是却存在以下问题:

  • 悲观锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。

  • 一旦悲观锁解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁

  • 乐观锁适合读多写少的场景,如果在分布式场景下使用乐观锁,就会导致总是更新失败,效率极低。

下面对Redis和Zookeeper的分布式锁写法做简单说明。

5.1 Redis

Redis实现分布式锁,可以使用redisson来实现。示例:

@Autowired
protected RedissonClient redissonClient;

public ResultModel tryLock(LockWorker lockWorker) {
  try {
    RLock lock = redissonClient.getFairLock(LOCKER_PREFIX + LOCKER_GOODS_CONSUME);
    // (公平锁)最多等待10秒,锁定后经过lockTime秒后自动解锁
    boolean success = lock.tryLock(LOCKER_WAITE_TIME, LOCKER_LOCK_TIME, TimeUnit.SECONDS);
    if (success) {
      try {
        return lockWorker.invoke();
      } finally {
        lock.unlock();
      }
    }
    return ResultModel.error("获取分布式锁失败!");  
  } catch (Exception e) {
    return ResultModel.error("获取分布式锁过程异常!");
  }
}

示例中,当我们配置好redis之后,使用RedissonClient来获取一个公平锁(也可以是其他种类锁),设置超时时间和自动解锁时间。获取到锁之后,这个锁就是在集群环境下唯一的。当我们执行LockWorker的业务逻辑时,就能保证集群在同一时间只有一个线程在执行LockWorker的业务。

5.2 Zookeeper

spring-integration对redis和zookeeper都做了整合,但是上面我并没有使用spring-integration,这里我将使用spring-integration-zookeeper来实现基于zookeeper的分布式锁。示例:

@Autowired
protected ZookeeperLockRegistry zookeeperLockRegistry;

public ResultModel tryLock(LockWorker lockWorker) {
  try {
    if (useLock) {
      Lock lock = zookeeperLockRegistry.obtain(LOCKER_GOODS_CONSUME);
      // (公平锁)最多等待10秒,锁定后经过lockTime秒后自动解锁
      boolean success = lock.tryLock(10, TimeUnit.SECONDS);
      if (success) {
        try {
          return lockWorker.invoke();
        } finally {
          lock.unlock();
        }
      }
      return ResultModel.error("获取分布式锁失败!");
    } else {
      return lockWorker.invoke();
    }
  } catch (Exception e) {
    return ResultModel.error("获取分布式锁过程异常!");
  }
}

示例中,使用了ZookeeperLockRegistry,这个位于spring-integration-zookeeper中,需要我们@Bean声明。获取锁和执行业务的逻辑和上面的redis一模一样。一样能保证集群在同一时间只有一个线程在执行LockWorker的业务。

快速构建项目

Spring项目快速开发工具:

一键快速构建Spring项目工具

一键快速构建SpringBoot项目工具

一键快速构建SpringCloud项目工具

一站式Springboot项目生成

Mysql一键生成Mybatis注解Mapper

Spring组件化构建

SpringBoot组件化构建

SpringCloud服务化构建

喜欢这篇文章么,喜欢就加入我们一起讨论JAVA吧!
品茗IT交流群

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/feiyangtianyao/article/details/100726937
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-02-13 12:02:53
  • 阅读 ( 1807 )
  • 分类:

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢