分布式锁

Java中的几种锁解决方案

乐观锁与悲观锁

乐观锁

乐观锁呢,它是假设一个线程在取数据的时候不会被其他线程更改数据,但是在更新数据的时候会校验数据有没有被修改过。它是一种比较交换的机制,简称CAS(Compare And Swap)机制。一旦检测到有冲突产生,比如说数据版本号或者最后更新时间不一致,它就会进行重试,直到没有冲突为止。

在JAVA中乐观锁并没有确定的方法,或者关键字,它只是一个处理的流程、策略。

下面以修改数据库数据为例,使用乐观锁:

  • 1、在检索数据,将数据的版本号(version)或者最后更新时间一并检索出来;
  • 2、操作员更改数据以后,点击保存,在数据库执行update操作;
  • 3、执行update操作时,用步骤1检索出的版本号或者最后更新时间与数据库中的记录作比较;
  • 4、如果版本号或最后更新时间一致,则可以更新;
  • 5、如果不一致,就不执行更新;

悲观锁

悲观锁与乐观锁恰恰相反,悲观锁从读取数据的时候就显示的加锁,直到数据更新完成,释放锁为止。在这期间只能有一个线程去操作,其他的线程只能等待。在JAVA中,悲观锁可以使用 synchronized关键字或者ReentrantLock类来实现。

synchronized 关键字

synchronized 锁有两种使用方式, 一种是加在方法上,它表示这个方法是加了锁的,当多个线程同时调用这个方法时,只有获得锁的线程才可以执行:

public synchronized void method(){
    // 方法体。。。
}

另一种是加在代码块上:

synchronized (对象锁){
    ……
}

我们将需要加锁的语句都写在 synchronized 块内,而在对象锁的位置,需要填写加锁的对象,它的含义是,当多个线程并发执行时,只有获得你写的这个对象的锁,才能执行后面的语句,其他的线程只能等待。

synchronized块通常的写法是synchronized(this),这个this是当前类的实例,也就是说获得当前这个类的对象的锁,才能执行这个方法,这样写的效果和synchronized方法是一样的。

ReentrantLock 类
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Test{
    private Lock lock = new ReentrantLock();

    public void method(){
        lock.lock();
        // 业务代码
        lock.unlock();
    }
}

使用lock.lock()进行加锁,使用lock.unlock()进行解锁,加锁后只会有一个线程能够执行加锁后的业务代码。

公平锁与非公平锁

公平锁与非公平锁是从另一个维度对锁的理解。

公平锁

公平锁故名思意,在多线程的情况下,对待每一个线程都是公平的。多个线程同时执行方法,线程A抢到了锁,A可以执行方法。其他线程则在队列里进行排队,A执行完方法后,会从队列里取出下一个线程B,再去执行方法。以此类推,对于每一个线程来说都是公平的,不会存在后加入的线程先执行的情况。

非公平锁

非公平锁与之相反,多个线程同时执行方法,线程A抢到了锁,A可以执行方法。其他的线程并没有排队,A执行完方法,释放锁后,其他的线程谁抢到了锁,谁去执行方法。会存在后加入的线程,反而先抢到锁的情况。

ReentrantLock 类的实现

公平锁与非公平锁都在ReentrantLock类里给出了实现,我们看一下ReentrantLock的源码。

    /**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

ReentrantLock有两个构造方法,默认的构造方法中,sync = new NonfairSync();我们可以从字面意思看出它是一个非公平锁。再看看第二个构造方法,它需要传入一个参数,参数是一个布尔型,true是公平锁,false是非公平锁。

分布式锁

尚文提到的锁,都是有JDK官方提供的锁的解决方案,也就是说这些锁只能在一个JVM进程内有效,我们把这种锁叫做单体应用锁。然而在我们的分布式系统中,这种单体应用锁就无法满足我们需求了,因为分布式系统是由多应用组成,会有多个JVM进程,单应用锁无法跨JVM、跨进程。那么分布式锁的定义就出来了,分布式锁就是可以跨越多个JVM、跨越多个进程的锁,这种锁就叫做分布式锁。

目前存在的分布式锁的方案

通过第三方的组件实现跨JVM、跨进程的分布式锁。这就是分布式锁的解决思路,找到所有JVM可以共同访问的第三方组件,通过第三方组件实现分布式锁。
image

目前比较流行的分布式锁的解决方案有:

  • 数据库,通过数据库可以实现分布式锁,但是在高并发的情况下对数据库压力较大,所以很少使用;
  • Redis,借助Redis也可以实现分布式锁,而且Redis的Java客户端种类很多,使用的方法也不尽相同;
  • Zookeeper,Zookeeper也可以实现分布式锁,同样Zookeeper也存在多个Java客户端,使用方法也不相同。

1、基于数据库悲观锁的分布式锁

通过for update语句实现锁:

select .... for update

for update是在数据库中上锁用的,可以为数据库中的行上一个排它锁。当一个事务的操作未完成时候,其他事务可以读取但是不能写入或更新。

2、基于Redis的Setnx实现分布式锁

获取锁的redis命令
SET resource_name my_random_value NX PX 30000

resource_name(key):资源名称,可根据不同的业务区分不同的锁。
my_random_value(value):随机值,每个线程的随机值都不相同,用于释放锁时的校验。
NX:表示 key不存在时设置成功,key存在则设置不成功。是一个原子性的操作,多线程并发时只有一个线程可以设置成功。设置成功才可以执行业务代码,不成功就不给执行。
PX:自动失效时间,即使出现异常情况,锁也可以通过过期来失效。

释放锁

释放锁时要校验之前设置的随机值(my_random_value),相同才能释放。因为需要校验,所以普通的redis的delete key命令不能满足,我们需要使用LUA脚本:

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end
代码实现

封装RedisLock

import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.core.types.Expiration;

import java.util.Arrays;
import java.util.List;
import java.util.UUID;

public class RedisLock implements AutoCloseable {

    private RedisTemplate redisTemplate;
    private String key;
    private String value;
    //单位:秒
    private int expireTime;

    public RedisLock(RedisTemplate redisTemplate,String key,int expireTime){
        this.redisTemplate = redisTemplate;
        this.key = key;
        this.expireTime=expireTime;
        this.value = UUID.randomUUID().toString();
    }

    /**
     * 获取分布式锁
     * @return
     */
    public boolean getLock(){
        RedisCallback<Boolean> redisCallback = connection -> {
            //设置NX
            RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent();
            //设置过期时间
            Expiration expiration = Expiration.seconds(expireTime);
            //序列化key
            byte[] redisKey = redisTemplate.getKeySerializer().serialize(key);
            //序列化value
            byte[] redisValue = redisTemplate.getValueSerializer().serialize(value);
            //执行setnx操作
            Boolean result = connection.set(redisKey, redisValue, expiration, setOption);
            return result;
        };

        //获取分布式锁
        Boolean lock = (Boolean)redisTemplate.execute(redisCallback);
        return lock;
    }

    public boolean unLock() {
        String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
                "    return redis.call(\"del\",KEYS[1])\n" +
                "else\n" +
                "    return 0\n" +
                "end";
        RedisScript<Boolean> redisScript = RedisScript.of(script,Boolean.class);
        List<String> keys = Arrays.asList(key);

        Boolean result = (Boolean)redisTemplate.execute(redisScript, keys, value);
        return result;
    }


    @Override
    public void close() throws Exception {
        unLock();
    }
}

使用RedisLock

import com.distribute.lock.lock.RedisLock;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
public class RedisLockController {

    @Autowired
    private RedisTemplate redisTemplate;

    @RequestMapping("redisLock")
    public String redisLock(){
        log.info("我进入了方法!");
        try (RedisLock redisLock = new RedisLock(redisTemplate,"redisKey",30)){
            if (redisLock.getLock()) {
                log.info("我进入了锁!!");
                Thread.sleep(15000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
        log.info("方法执行完成");
        return "方法执行完成";
    }
}

3、基于Zookeeper的分布式锁

原理

利用了zookeeper的瞬时有序节点的特性,多线程并发创建顺势节点时,得到有序的序列,我们规定序号最小的线程获得锁。其他线程则监听自己序号的前一个序号,前一个线程执行完成,删除自己的序号的节点,下一个节点的线程得到通知,继续执行,以此类推。

代码实现

封装锁

import lombok.extern.slf4j.Slf4j;
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;

import java.io.IOException;
import java.util.Collections;
import java.util.List;

@Slf4j
public class ZkLock implements Watcher,AutoCloseable {

    private ZooKeeper zooKeeper;
    private String businessName;
    private String znode;

    public ZkLock(String connectString,String businessName) throws IOException {
        this.zooKeeper = new ZooKeeper(connectString,30000,this);
        this.businessName = businessName;
    }

    public boolean getLock() throws KeeperException, InterruptedException {
        //创建业务 根节点
        Stat existsNode = zooKeeper.exists("/" + businessName, false);
        if (existsNode == null){
            zooKeeper.create("/" + businessName,businessName.getBytes(),
                    ZooDefs.Ids.OPEN_ACL_UNSAFE,
                    CreateMode.PERSISTENT);
        }
        //创建瞬时有序节点  /order/order_00000001
        znode = zooKeeper.create("/" + businessName + "/" + businessName + "_", businessName.getBytes(),
                ZooDefs.Ids.OPEN_ACL_UNSAFE,
                CreateMode.EPHEMERAL_SEQUENTIAL);
        znode = znode.substring(znode.lastIndexOf("/")+1);
        //获取业务节点下 所有的子节点
        List<String> childrenNodes = zooKeeper.getChildren("/" + businessName, false);
        //子节点排序
        Collections.sort(childrenNodes);
        //获取序号最小的(第一个)子节点
        String firstNode = childrenNodes.get(0);
        //如果创建的节点是第一个子节点,则获得锁
        //不是第一个子节点,则监听前一个节点
        if (!firstNode.equals(znode)){
            String lastNode = firstNode;
            for (String node:childrenNodes){
                if (!znode.equals(node)){
                    lastNode = node;
                }else {
                    zooKeeper.exists("/"+businessName+"/"+lastNode,true);
                    break;
                }
            }
            synchronized (this){
                wait();
            }
        }
        return true;
    }

    @Override
    public void process(WatchedEvent watchedEvent) {
        if (watchedEvent.getType() == Event.EventType.NodeDeleted){
            synchronized (this){
                notify();
            }
        }
    }


    @Override
    public void close() throws Exception {
        zooKeeper.delete("/"+businessName+"/"+znode,-1);
        zooKeeper.close();
        log.info("我释放了锁");
    }
}

调用

import com.example.distributelock.lock.ZkLock;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
public class ZkLockController {

    @RequestMapping("zkLock")
    public String zkLock(){
        log.info("我进入了方法!");
        try (ZkLock zkLock = new ZkLock("localhost:2181","order")){
            if (zkLock.getLock()) {
                log.info("我进入了锁!!");
                Thread.sleep(15000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
        log.info("方法执行完成");
        return "方法执行完成";
    }
}

Redisson 实现分布式锁

Redisson是一个具有内存数据网格功能的Redis Java客户端,功能很多很强大,这里只介绍下它的分布式锁功能使用,在spring boot项目下。

我们日常开发中,推荐使用Redisson实现分布式锁。

https://github.com/redisson/redisson/tree/master/redisson-spring-boot-starter

pom

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.16.2</version>
</dependency>

yml 连接redis

spring:
  redis:
    database: 0
    host: 192.168.10.125
    port: 6379
    password: 123456

使用分布式锁

@Autowired
private RedissonClient redisson;

public void redissonLock() {
	// 设置锁的业务分类
	RLock rLock = redisson.getLock("xxx");
	// 设置过期时间
	rLock.lock(30, TimeUnit.SECONDS);
	// 业务代码
	rLock.unlock();
}
posted @ 2021-09-08 22:30  金盛年华  阅读(54)  评论(0)    收藏  举报