一、什么是分布式锁
  分布式锁(Distributed Lock):是一种在分布式系统中控制多个进程或服务对共享资源访问的机制,保证在同一时刻,只有一个客户端可以操作某个资源,避免数据竞争和并发冲突。
 单机锁:像Java的synchronized或者C#的lock,只能在单机jvm/进程里生效。
 分布式锁:能在多个机器、多个进程间生效,保证全局唯一性。

二、为什么用Redis做分布式锁
 Redis是一个高性能的内存数据库,支持原子操作,如SEINX、DEL、EXPIRE等,非常适合做分布式锁。
 优点:简单高效,Redis操作速度快;支持过期时间,防止死锁;易于扩展,适用于多节点部署。

三、Redis分布式锁的原理
 基本思路:
  1.客户端尝试向Redis写入一个键(key)作为锁标识。
  2.使用原子操作确保同一时间只有一个客户端成功写入。
  3.锁设置过期时间,避免客户端异常导致锁永远存在。(锁的时间>业务逻辑处理时间)。
  4.操作完成后,删除锁。

在这里插入图片描述

四、实现用例

线程池创建用来模拟并发

package com.test.redisdemotest.config;


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;

@EnableAsync //开启异步任务
@Configuration //配置类
public class ThreadPoolConfig {
    @Bean
    public Executor taskExecutor() {
        // 创建线程池
        ThreadPoolTaskScheduler scheduler=new ThreadPoolTaskScheduler();
        // 设置线程池大小 获取可用处理器乘于二
        scheduler.setPoolSize(Runtime.getRuntime().availableProcessors()*2);
        // 设置线程名称前缀
        scheduler.setThreadNamePrefix("my-scheduler-");
        // 尝试等待线程池所有任务完成,最多等待60秒关闭
        scheduler.setAwaitTerminationSeconds(60);
        // 拒绝策略,CallerRunsPolicy 表示由调用线程处理该任务
        scheduler.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        return scheduler;
    }
}

操作Redis分布式工具类

package com.test.redisdemotest.util;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.util.concurrent.TimeUnit;

/**
 * 分布式锁工具类
 */
@SuppressWarnings({"unchecked", "rawtypes"})
@Component
@Slf4j
@RequiredArgsConstructor //构造器注入
public class DistributedLockUtil {

    //注入redisTemplate Redis模板
    public final RedisTemplate redisTemplate;

    /**
     * 分布式锁处理操作方法,当前抢占到资源的线程可以实现数据的锁定
     *
     * @param key        数据锁标记,不同业务可能有不同的标记
     * @param userId     操作用户ID,依据该数据可以判断是否重复获取锁
     * @param expireTime 分布式锁有效时间(牵扯到自动失效问题)
     * @param timeUnit   分布式锁有效时间单位
     * @return 获取分布式锁成功返回true,否则返回false
     */
    public boolean lock(String key, String userId, long expireTime, TimeUnit timeUnit) {
        // 保存的数据
        String value = userId + ":" + System.currentTimeMillis();

        // 判断当前redis是否有指定的key数据,如果没有则进行保存 返回true ,如果存在就则不保存 返回false
        if (Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(key, value, expireTime, timeUnit))) {
            return true;
        }
        // 如果存在则获取当前key的值,判断是否是当前用户获取的锁,如果是则进行续期
        var lockValue = redisTemplate.opsForValue().get(key);
        String executor = lockValue != null ? lockValue.toString() : null;
        if(StringUtils.hasText(executor)){
            if(executor.startsWith(userId)){
                // 续期 同一个人又重复抢到了
                redisTemplate.expire(key,expireTime,timeUnit);
                return true;
            }
        }
        return false;
    }

    /**
     * 释放分布式锁
     * @param key 锁的key
     * @param userId 操作用户ID
     */
    public void unlock(String key, String userId) {
        var lockValue = redisTemplate.opsForValue().get(key);
        String executor = lockValue != null ? lockValue.toString() : null;
        if(StringUtils.hasText(executor)){
            if(executor.startsWith(userId)){
                // 删除数据
                redisTemplate.delete(key);
            }
        } else {
            log.info("释放锁失败,锁不存在");
        }
    }
}

模拟业务操作和进行上锁解锁

package com.test.redisdemotest.task;

import com.test.redisdemotest.util.DistributedLockUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@Component
@Slf4j
@RequiredArgsConstructor
public class ResourceTask {
    // Key 名称 实际操作时候key根据业务来进行,比方调度发车,key就是车牌号,防止多个调度台给同一辆车同一个时间派发任务
    private static final String BUSINESS_KEY = "business_key";
    // 分布式工具
    private final DistributedLockUtil distributedLockUtil;

    @Async
    public void handle(String userId) {
        // 获取分布式锁
        boolean lock = distributedLockUtil.lock(BUSINESS_KEY, userId, 300, TimeUnit.MILLISECONDS);
        if (lock) {
            log.info("用户{}获取到锁 ", userId);
            // 模拟业务处理
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                log.error("错误异常:{}", e.getMessage());
            }finally {
                // 释放分布式锁
                distributedLockUtil.unlock(BUSINESS_KEY, userId);
                log.info("用户{}释放锁", userId);
            }
        } else{
            log.info("用户{}未获取到锁", userId);
        }
    }
}

进行测试

package com.day03.redisdemotest;

import com.day03.redisdemotest.task.ResourceTask;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;


@SpringBootTest(classes = RedisDemoTestApplication.class)
@ExtendWith({SpringExtension.class})
@WebAppConfiguration
@Slf4j
public class ResourceTaskTest {
    @Autowired
    private ResourceTask resourceTask;

    @Test
    public void testHandle() throws InterruptedException {
        log.info("===========几乎三个同时进行===========");
        for (int i = 0; i < 3; i++) {
            resourceTask.handle("X-X" + (i + 1));
        }
        // 模拟中间10秒没有执行
        Thread.sleep(1000 * 10);
        log.info("===========三个中间间隔100毫秒===========");
        for (int i = 0; i < 8; i++) {
            Thread.sleep(100);
            resourceTask.handle("Y-Y" + (i + 1));
        }
    }
}

在这里插入图片描述
由此可以看到,在没有释放锁或者锁到期的时间内只能有一个用户进行操作。

更多推荐