什么是分布式锁?

概念

CAP定理

任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。

为什么要有分布式锁?单机锁不能完成么?

图解

分布式锁 zookeeper 分布式锁原理_分布式

单机锁分布式架构下只能锁住当前机器,而不能实现个节点使用同一把锁

如何设计分布式锁

  • 可以保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行。
  • 这把锁要是一把可重入锁(避免死锁)
  • 这把锁最好是一把阻塞锁(根据业务需求考虑要不要这条)
  • 这把锁最好是一把公平锁(根据业务需求考虑要不要这条)
  • 有高可用的获取锁和释放锁功能
  • 获取锁和释放锁的性能要好

底层实现原理

Redis:基于 redis 的 setnx()、expire() 方法

基于ZooKeeper

  • 原理:利用临时节点与 watch 机制。每个锁占用一个普通节点 /lock,当需要获取锁时在 /lock 目录下创建一个临时节点,创建成功则表示获取锁成功,失败则 watch/lock 节点,有删除操作后再去争锁。临时节点好处在于当进程挂掉后能自动上锁的节点自动删除即取消锁。
  • 缺点:所有取锁失败的进程都监听父节点,很容易发生羊群效应,即当释放锁后所有等待进程一起来创建节点,并发量很大。

基于redisson

基于数据库排他锁

mysql:基于乐观锁或者mvcc机制

基于 Redlock 做分布式锁

  • Redlock 是 Redis 的作者 antirez 给出的集群模式的 Redis 分布式锁,它基于 N 个完全独立的 Redis 节点(通常情况下 N 可以设置成 5)。

服务部署及代码实现

Linux容器化部署Redis

docker run -di --name=redis -p 6379:6379 redis

*(若是没有镜像会先自动拉取)

依赖引入

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.example.demo</groupId>
	<artifactId>springboot-redis</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>springboot-redis</name>
	<description>Demo project for Spring Boot</description>

	<parent>
		<groupId>com.example</groupId>
		<artifactId>springboot-master</artifactId>
		<version>0.0.1-SNAPSHOT</version>
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.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-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>
		<!-- redisson 作为分布式锁等功能框架-->
		<dependency>
			<groupId>org.redisson</groupId>
			<artifactId>redisson</artifactId>
			<version>3.12.0</version>
		</dependency>

		<dependency>
			<groupId>redis.clients</groupId>
			<artifactId>jedis</artifactId>
			<version>2.5.0</version>
		</dependency>


		<!--lombok-->
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<scope>provided</scope>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			  <groupId>org.powermock</groupId>
			  <artifactId>powermock-module-junit4</artifactId>
			  <version>2.0.0</version>
			  <scope>test</scope>
		</dependency>
		<!-- PowerMock Mockito2 API -->
		<dependency>
			  <groupId>org.powermock</groupId>
			  <artifactId>powermock-api-mockito2</artifactId>
			  <version>2.0.0</version>
			  <scope>test</scope>
		</dependency>
		<dependency>
			  <groupId>junit</groupId>
			  <artifactId>junit</artifactId>
			  <version>4.12</version>
			  <scope>test</scope>
		</dependency>
		<dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>mybatis-plus-extension</artifactId>
			<version>3.3.1</version>
			<scope>compile</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>8</source>
                    <target>8</target>
                </configuration>
            </plugin>
        </plugins>
	</build>


</project>

配置文件编写

spring:
  redis:
    database: 0
    host: 192.168.31.112
    port: 6379
    timeout: 5000
    commandTimeout: 5000

controller编写

package com.example.demo.controller;

import com.example.demo.service.DistributedLockService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.web.bind.annotation.*;

/**
 * @author Adam
 * @version 1.0
 * @description
 * @date 2022/5/10
 */
@RestController
@RequestMapping("/string/redis")
public class TestStringRedisTemplateController {
    private static final Logger log = LoggerFactory.getLogger(TestController.class);
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private DistributedLockService distributedLockService;

    @PostMapping("/setKey")
    public void setKey(@RequestParam String key,@RequestParam String value) {
        ValueOperations<String, String> s = stringRedisTemplate.opsForValue();
        s.set(key,value);
    }

    @GetMapping("/getKey/{key}")
    public Object getKey(@PathVariable("key") String key) {
        return stringRedisTemplate.opsForValue().get(key);
    }

    /**
     * 使用Redis实现分布式锁
     */
    @GetMapping("/schedule")
    public void distributedLock (){
        distributedLockService.getScheduleResultRedisLock();
    }
}

Service编写

package com.example.demo.service;

/**
 * @author Adam
 * @version 1.0
 * @description
 * @date 2022/5/15
 */
public interface DistributedLockService{
    /**
     * Service
     */
    void getScheduleResultRedisLock();
}

ServiceImpl编写

package com.example.demo.service.impl;

import com.example.demo.service.DistributedLockService;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
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 java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * @author Adam
 * @version 1.0
 * @description
 * @date 2022/5/15
 */
@Slf4j
@Service("DistributedLockService")
public class DistributedLockServiceImpl implements DistributedLockService {
    final static String lockName = "scheduleLock";
    final static String luaScript = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
            "then\n" +
            "    return redis.call(\"del\",KEYS[1])\n" +
            "else\n" +
            "    return 0\n" +
            "end";

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private RedissonClient redissonClient;

    @Override
    public void getScheduleResultRedisLock() {
        //redis value 存储UUID防止删错锁
        String uuid = UUID.randomUUID().toString();
        int i = 0;
        //占锁(设置过期时间及单位)
        Boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent(lockName, uuid, 10, TimeUnit.SECONDS);
        if (isLock) {
            //true表示没有锁,此次创建锁并执行业务处理
            log.info("分布式锁创建成功,uuid:" + uuid);
            try {
                //业务逻辑处理
                System.out.println("业务处理");
            } finally {
                //直接删除锁
                /**
                 *  stringRedisTemplate.delete(lockName);
                 */

                //判断是否是自己加的锁(uuid)再删
                /**
                 *  if (uuid.equals(stringRedisTemplate.opsForValue().get(lockName))){
                 *      stringRedisTemplate.delete(lockName);
                 *  }
                 */

                //使用Redis支持的lua脚本方式删除锁,保证操作的原子性,预防死锁
                stringRedisTemplate.execute(new DefaultRedisScript<Long>(luaScript, Long.class),
                        Arrays.asList(lockName),
                        uuid);
                log.info("删除锁成功");
            }
        } else {
            //false 则加锁失败自旋重试
            i++;
            log.info("加锁失败,第{}次加锁", i);
            try {
                TimeUnit.MILLISECONDS.sleep(200);
            } catch (Exception e) {
            }
            //重试
            if (i < 10) {
                getScheduleResultRedisLock();
            }
        }
    }

    /**
     * Redisson实现分布式锁
     */
    public void redissonLock(){
        //获取锁
        final RLock lock = redissonClient.getLock(lockName);
        //加锁
        lock.lock();
        log.info("加锁成功!");
        try {
            log.info("执行业务逻辑……");
        }finally {
            lock.unlock();
            log.info("解锁成功!");
        }
    }
}

各方法实现优缺点对比

实现方式

优点

缺点

数据库排他锁

简单,易于理解

操作数据库需要一定的开销,行级锁并不一定靠谱,性能不靠谱

RedLock

性能高

失效时间设置导致的并发问题

ZooKeeper

解决了单点问题、不可重入问题、非阻塞问题以及锁无法释放的问题,实现简单。

性能上可能并没有缓存服务那么高,因为每次在创建锁和释放锁的过程中,都要动态创建、销毁临时节点来实现锁功能。ZK 中创建和删除节点只能通过 Leader 服务器来执行,然后将数据同步到所有的 Follower 机器上。还需要对 ZK的原理有所了解。

Redisson

性能高,实现简单

脏数据和数据一致性问题

Redis

设计性开放

繁琐,删除锁设计时要考虑原子性