分布式锁(redisson、悲观乐观锁)

—、redisson

一、 简介

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 进行比较
实现者数据库本身开发者
适用场景并发量大并发量小
类比JavaSynchronized关键字CAS 算法
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值