分布式锁 : redisson、悲观乐 观锁

—、redisson

 

1. 与redistempplate区别:

 

 

2. 看门狗机制:

 

 

 

 

 

 


 

一、 简介

Redisson是一个Redis的基础上实现的Java驻内存数据网格,它不仅提供了一系列分布式的Java常用对象,还有一个重要的分布式锁的实现,主要作用为了防止分布式系统中的多个进程之间相互干扰。比如单机模式下的多线程用同步锁synchronized等解决数据一致性的并发操作,而分布式系统中则需要用redisson的lock或其他方式来解决。

二、 原理

 

底层其实就是基于分布式的Redis集群实现的。

key作为是否上锁的标识,当通过getLock(String key)方法获得相应的锁后,这个key即作为一个锁存储到Redis集群中。 之后如果有其他的线程尝试获取名为key的锁时,便会向集群中进行查询,如果能够查到这个锁并发现相应的value的值不为0,则表示已经有其他线程申请了这个锁同时还没有释放,则当前线程进入阻塞,否则由当前线程获取这个锁并将value值加1。

三、 Watch Dog(看门狗机制)

作用就是:自动续期

  1. 解决指定解锁时间的重复解锁问题。(业务执行的时间超过指定时间,redis会自动解锁;当前业务执行完后又要解锁,可能会解锁到另一条线程加的锁或当前锁已失效)

2. 解决死锁。(加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认会在30s内自动过期)

 

 

四、 锁类型

根据不同业务场景需要redisson提供了多种锁的实现类型:可重入锁,公平锁,联锁,红锁,读写锁,信号量,可过期性信号量,闭锁。

这里看下最常用的可重入锁,特点是同一个线程可以重复拿到同一个资源的锁,非常有利于资源的高效利用。

 

 

底层实现:1. Redis存储锁的数据类型是Hash

  1. Hash数据类型的key值包含了当前线程信息

 

 

五、 redlock算法

       底层的一个算法,可以了解一下。

redis宕机时,即使有主从,但是依然会有一个同步间隔,如果造成数据流失,服务器A丢失锁,服务器B就可以获取锁,这样就造成数据错误。

  redlock主要思想是做数据冗余。比如5台独立的集群,当我们发送一个数据的时候,要保证3台(n/2+1)以上的机器接受成功才算成功,否则重试或报错。

 

 

六、 项目配置

  1. 引入依赖

 

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.12.0</version>
</dependency>

 

 

 

 

2.配置类RedissonClient

@Configuration
public class MyRedissonConfig {
    /**
     * 1.所有对redisson的使用都是通过RedissonClient来使用
     * 2.支持4种连接redis方式,分别为单机、主从、Sentinel、Cluster集群 。此处为Sentinel模式
     */
    @Bean(destroyMethod="shutdown")
    public RedissonClient redisson() throws Exception{
        //1 创建配置
        Config config = new Config();

        config.useSentinelServers()
                .addSentinelAddress("redis://192.168.42.97:26379","redis://192.168.42.97:26380","redis://192.168.42.97:26381")
                .setMasterName("mymaster")
                .setDatabase(0);
        //2.根据Config创建出RedissonClient
        return Redisson.create(config);
    }
}

 

 

3.操作示例

 

@Autowired
RedissonClient redissonClient;

 

 

 

 

@RequestMapping("test")
@ResponseBody
public void test() throws InterruptedException {
    // getLock(key)方法 返回的是一个RedissonLock对象
    RLock lock = redissonClient.getLock("my-lock");

    // 1.redisson的自动续期,如果业务超长,运行期间自动续上30s,不用担心业务时间长,锁自动过期被删掉
    // 2.加锁得业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30s后自动删除
    lock.lock();

    // 5秒以后自动解锁,自动解锁时间一定要大于业务时间,在锁时间到了以后,不会自动续期
    //lock.lock(5, TimeUnit.SECONDS);
    try {
        System.out.println("线程"+Thread.currentThread().getId()+"加锁成功,执行业务1");
        Thread.sleep(1000*8);
    } catch (Exception e){
        System.out.println(e);
    } finally{
        //解锁
        System.out.println("线程"+Thread.currentThread().getId()+"释放锁1");
        lock.unlock();
    }
}

 

 

 

 

/**
 * 可重入锁
 */
@RequestMapping("test2")
@ResponseBody
public void test2() throws InterruptedException {
    RLock lock = redissonClient.getLock("my-lock");
    lock.lock();
    try {
        System.out.println("线程"+Thread.currentThread().getId()+"加锁成功,执行业务2");
        Thread.sleep(1000*8);
        lock.lock();
        System.out.println("线程"+Thread.currentThread().getId()+"加锁成功,执行业务2.2");
        Thread.sleep(1000*8);
        System.out.println("线程"+Thread.currentThread().getId()+"释放锁2.2");
        lock.unlock();
    } catch (Exception e){
        System.out.println(e);
    } finally{
        System.out.println("线程"+Thread.currentThread().getId()+"释放锁2");
        lock.unlock();
    }
}

 

 


 

 

 二、数据库锁:

 

 Mysql 并发事务 会引起更新丢失问题,解决办法是锁。所以本文将对锁(乐观锁、悲观锁)进行分析。

 

1. 悲观锁:

1 概念(来自百科)

悲观锁,正如其名,指数据被 外界(包括当前系统的其它事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提                                        供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排它性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)

还可以理解,就是Java中的 Synchronized 关键字。只要对代码加了 Synchronized 关键字,JVM 底层就能保证其线程安全性。

 

2 命令行演示

2.1 准备数据

 

2.2 测试

测试准备:

  • 两个会话(终端),左边会话是白色背景、右边会话是黑色背景

开始测试:

第一步:两个终端均关闭自动提交

左边:

右边:

 

第二步:左边利用 select .... for update 的悲观锁语法锁住记录

select * from employee where id = 1 for update; 

第三步:右边也尝试利用 select .... for update 的悲观锁语法锁住记录

可以看到,Sql语句被挂起(被阻塞)!

提示:如果被阻塞的时间太长,会提示如下:

 

第四步:左边执行更新操作并提交事务

Sql语句:

update employee set money = 0 + 1 where id = 1;
commit; 

结果:

分析:

  • Money 的旧值为0,所以更新时 Money=0+1
  • 一执行 commit 后,注意查看右边Sql语句的变化

第五步:查看右边Sql语句的变化

分析:

  • 被左边悲观锁阻塞了 11.33 秒
  • Money=1,这是左边更新后的结果

2.3 结论

可以看到,当左边(事务A)使用了 select ... for update 的悲观锁后,右边(事务B)再想使用将被阻塞,同时,阻塞被解除后事务B能看到事务A对数据的修改,所以,这就可以很好地解决并发事务的更新丢失问题啦(诚然,这也是人家悲观锁的分内事)

 

2.乐观锁:

1 概念

   理解方式一:

      乐观锁认为一般情况下数据不会造成冲突,所以在数据进行提交更新时才会对数据的冲突与否进行检测。如果没有冲突那就OK;如果出现冲突了,则返回错误信息并让用户决定如何去做。

  理解方式二:

      乐观锁的特点是先进行业务操作,不到万不得已不会去拿锁。乐观地认为拿锁多半会是成功的,因此在完成业务操作需要实际更新数据的最后一步再去拿一下锁。

  我的理解

      理解一:就是 CAS 操作

      理解二:类似于 SVN、GIt 这些版本管理系统,当修改了某个文件需要提交的时候,它会检查文件的当前版本是否与服务器上的一致,如果一致那就可以直接提交,如果不一致,那就必须先                                          更新服务器上的最新代码然后再提交(也就是先将这个文件的版本更新成和服务器一样的版本)

 

表设计

  表task,分别有三个字段id,value、version

3 具体实现

  -首先读取task表中的数据,得到version的值为versionValue

  -在每次更新task表value字段时,因为要防止可能发生的冲突,我们需要这样操作select (value,version) from task where id=#{id}

  update task

    set value=newValue,version=versionValue+ 1

  whereid=#{id} and version=versionValue;

  只有当这条语句执行成功了,本次更新value字段的值才会表示成功。

 

  我们假设有两个节点A与B都需要更新task表中的value字段值,在相同时刻,A和B节点从task表中读到的version值都为2,那么A节点和B节点在更新value字段值的时候,都需要操作

    update task set value = newValue,version = 3 where version = 2;

  实际上其实只有1个节点执行该SQL语句成功,我们假设A节点执行成功,那么此时task表的version字段的值是3,B节点再操作

   update task

    set value = newValue,version = 3

   where version = 2;

  这条SQL语句是不执行的,这样就保证了更新task表时不发生冲突

 

 

3.总结、对比

 

  悲观锁 乐观锁
概念 查询时 直接锁住记录 使其它事务不能查询,更不能更新 提交更新时 检查 版本或时间戳 是否相等
语法 select ... for update 使用 version 或者 timestamp 进行比较
实现者 数据库本身 开发者
适用场景 并发量大 并发量小
类比Java Synchronized关键字 CAS 算法
posted @ 2021-12-27 16:12  真理不真  阅读(1103)  评论(0编辑  收藏  举报