一、什么是分布式锁?

为了保证一个方法或属性在高并发情况下的同一时间只能被同一个线程执行,由于分布式系统多线程、多进程并且分布在不同机器上,需要一种跨机器的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!

二、分布式锁应该具备哪些条件

在分析分布式锁的三种实现方式之前,先了解一下分布式锁应该具备哪些条件:
1、在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;
2、高可用的获取锁与释放锁;
3、高性能的获取锁与释放锁;
4、具备可重入特性;
5、具备锁失效机制,防止死锁;
6、具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。

三、分布式锁的三种实现方式

目前几乎很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题。分布式的CAP理论告诉我们“任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。所以,很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证“最终一致性”,只要这个最终时间是在用户可以接受的范围内即可。
在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。有的时候,我们需要保证一个方法在同一时间内只能被同一个线程执行。


基于数据库实现分布式锁; 基于缓存(Redis等)实现分布式锁; 基于Zookeeper实现分布式锁;


四、基于Redis的实现方式

1、选用Redis实现分布式锁原因:

(1)Redis有很高的性能;
(2)Redis命令对此支持较好,实现起来比较方便

2、使用命令介绍:

(1)SETNX

SETNX key val:当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0。

(2)expire

expire key timeout:为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。

(3)delete

delete key:删除key
在使用Redis实现分布式锁的时候,主要就会使用到这三个命令。

3、实现思想:

(1)获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。
(2)获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
(3)释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。

五、代码实现:

pom文件:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>redisLock</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>redisLock</name>
    <description>redisLock</description>

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.3.7.RELEASE</spring-boot.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!-- redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- spring2.X集成redis所需common-pool2-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
            <version>2.6.0</version>
        </dependency>

        <!-- redisson 分布式锁-->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.11.2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>


    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.3.7.RELEASE</version>
                <configuration>
                    <mainClass>com.example.redislock.RedisLockApplication</mainClass>
                </configuration>
                <executions>
                    <execution>
                        <id>repackage</id>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

yml:

server:
  #port: 8206
spring:
  redis:
    host: 192.168.247.100
    port: 6379
    database: 0
    timeout: 1800000
    lettuce:
      pool:
        max-active: 20 #最大连接数
        max-wait: -1    #最大阻塞等待时间(负数表示没限制)
        max-idle: 5    #最大空闲
        min-idle: 0     #最小空闲

操作reids代码:

package com.example.redislock.service.impl;

import com.example.redislock.service.TestService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Service
public class TestServiceImpl implements TestService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 最初版,单一部署 synchronized锁
     * 在缓存中存储一个num,初始值为0
     * 利用缓存中的StringRedisTemplate,获取到当前的num数据值
     * 如果num不为空,则需要对当前值+1操作
     * 如果num为空,返回即可
     */
//    @Override
//    public synchronized void testLock() {
//        // 利用缓存中的StringRedisTemplate,获取到当前的num数据值
//        String num = redisTemplate.opsForValue().get("num");
//        if (StringUtils.isEmpty(num)) {
//            return;
//        }
//        // 如果num不为空,则需要对当前值+1操作
//        int numValue = Integer.parseInt(num);
//        // 写回缓存
//        redisTemplate.opsForValue().set("num", String.valueOf(++numValue));
//    }

    /**
     * 使用redis分布式锁版
     */
    @Override
    public void testLock() {
        // setnx获取锁时,设置一个指定的唯一值(例如:uuid);释放前获取这个值,判断是否自己的锁。
        String uid = UUID.randomUUID().toString();
        // 1. 使用setnx命令     setnx lock ok,并设置过期时间 3m(设置过期时间,自动释放锁。)
        Boolean flag = redisTemplate.opsForValue().setIfAbsent("lock",uid,3, TimeUnit.MINUTES);
        // 2. 判断flag是否为true;
        if(flag){
            // flag = true 表示获取到锁,执行业务
            String num = redisTemplate.opsForValue().get("num");
            //如果num没有设置初始值,则不执行业务
            if(StringUtils.isEmpty(num)){
                return;
            }
            int newnum = Integer.parseInt(num);
            redisTemplate.opsForValue().set("num", String.valueOf(++newnum));
            // 释放lock锁
            // 定义一个lua脚本,保证删除的原子性
//            String secript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
//            //  准备执行lua 脚本
//            DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
//            // 设置lua脚本
//            redisScript.setScriptText(secript);
//            // 设置DefaultRedisScript 这个对象的泛型
//            redisScript.setResultType(Long.class);
            //  redis调用lua脚本
//            redisTemplate.execute(redisScript, Arrays.asList("lock"), uid);
            // 释放lock锁
            if(uid.equals(redisTemplate.opsForValue().get("lock"))){
                redisTemplate.delete("lock");
            }
        }else {
            // 没有获取到锁
            try {
                Thread.sleep(100);
                // 每隔1秒钟回调一次,再次尝试获取锁(自旋)
                testLock();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

    }

}

最后总结

为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:

互斥性。在任意时刻,只有一个客户端能持有锁;
不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁;
解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了;
加锁和解锁必须具有原子性