百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

你在为 Spring Boot 结合 Redis 实现分布式锁而苦恼吗?

mhr18 2025-04-27 14:50 14 浏览 0 评论

你有没有过这样的经历?在互联网大厂后端开发工作中,面对高并发场景时,为了保证数据的一致性和操作的原子性,需要使用分布式锁。而在 Spring Boot 项目里,选择用 Redis 来实现自定义分布式锁,却遇到了一堆难题:锁怎么正确获取和释放?如何避免锁的误释放、死锁等问题?别担心,今天咱们就来好好聊聊这些事儿!

在如今互联网应用高并发的大背景下,分布式系统已经成为主流架构。当多个服务实例同时访问共享资源时,就需要分布式锁来协调访问顺序,保证数据的准确性。Spring Boot 凭借其快速开发、高度集成的特性,深受后端开发者喜爱;Redis 则以其高性能、丰富的数据结构和原子操作能力,成为实现分布式锁的热门选择。二者结合,能为高并发场景下的资源访问控制提供强大的解决方案。但在实际设计和使用分布式锁的过程中,却隐藏着诸多需要考虑的问题。

设计分布式锁需要考虑的问题

单点故障

如果 Redis 是单节点部署,一旦这个节点宕机,那么分布式锁就无法正常使用,整个依赖分布式锁的业务流程都会受到影响,导致服务不可用。

锁误释放

在高并发场景下,可能会出现 A 客户端获取了锁,还未执行完业务逻辑,锁就被 B 客户端误释放的情况。比如,A 客户端设置的锁过期时间到了,此时 B 客户端获取到了锁,而 A 客户端后续操作完成后,在不知情的情况下释放了锁,导致 B 客户端的锁提前被释放。

锁的唯一性

在分布式系统中,多个服务实例可能会同时尝试获取锁,必须保证每个锁的唯一性,否则不同业务场景下可能会出现锁的混淆和错误使用,影响业务逻辑的正常执行。

合理设置过期时间

过期时间设置过短,可能会导致业务还未执行完,锁就被释放,出现多个实例同时访问共享资源的情况;过期时间设置过长,又会导致资源长时间被占用,其他实例无法获取锁,降低系统的并发性能。

避免死锁

如果在获取锁或释放锁的过程中出现异常,没有正确释放锁,就会导致死锁。例如,获取锁后,服务器突然宕机,锁无法正常释放,其他实例永远无法获取到该锁。

锁续期

对于一些长时间运行的任务,如果不进行锁续期,锁可能会在任务执行过程中过期释放,导致任务执行出现问题。

如何解决这些问题

解决单点故障

采用 Redis 集群(如 Redis Cluster)或主从复制架构。Redis Cluster 通过将数据分片存储在多个节点上,实现高可用性和扩展性;主从复制则可以在主节点宕机时,通过手动或自动故障转移,让从节点升级为主节点,保证服务的可用性,从而避免因单点故障导致分布式锁无法使用。

防止锁误释放

在获取锁时,为每个锁设置一个唯一的客户端 ID(例如使用 UUID)作为锁的 value。在释放锁时,通过 Lua 脚本判断当前锁的 value 是否与自己的客户端 ID 一致,只有一致时才允许释放锁。这样就能确保只有持有锁的客户端才能释放锁,避免锁被其他客户端误释放。

保证锁的唯一性

在生成锁的 key 时,结合业务场景和资源标识,确保每个锁的 key 都是唯一的。例如,对于某个订单处理业务,锁的 key 可以设计为 “order:lock:{orderId}”,其中 {orderId} 是具体的订单编号,这样就能保证不同订单的锁相互独立,不会混淆。

合理设置过期时间

根据业务执行的平均时间,结合一定的缓冲时间来设置过期时间。可以通过性能测试和监控,统计业务操作的耗时分布,然后在此基础上增加一定的安全时间,作为锁的过期时间。同时,也可以考虑使用 Redis 的可续约机制,在任务执行过程中,定期检查任务是否还在执行,如果还在执行,就延长锁的过期时间。

避免死锁

在加锁和解锁的代码逻辑中,使用 try - finally 块,确保无论是否出现异常,锁都能被正确释放。例如,在获取锁后,如果发生异常,在 finally 块中执行释放锁的操作。同时,也可以设置锁的最大持有时间,当超过这个时间后,自动释放锁,避免死锁的发生。

实现锁续期

可以使用 Redis 的发布 / 订阅功能,在获取锁时,启动一个守护线程,定期向 Redis 发送续约请求。当业务执行完成后,停止守护线程,不再续约。另外,也可以使用 Redis 的 setex 命令(设置键值对并指定过期时间),在任务执行过程中,通过原子操作更新锁的过期时间,实现锁的续期。

在 Spring Boot 中结合 Redis 实现自定义分布式锁的具体步骤

添加依赖

在 Spring Boot 项目的 pom.xml 文件中,添加 Spring Data Redis 依赖:

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

配置 Redis 连接

在 application.yml 文件中,配置 Redis 的连接信息,例如:

	spring:
  redis:
    host: localhost
    port: 6379
    password: 
    database: 0

创建 Redis 配置类

创建一个 Redis 配置类,用于配置 Redis 连接工厂等相关信息:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        // 设置key的序列化方式
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        // 设置value的序列化方式
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());

        return redisTemplate;
    }
}

创建 RedisLock 类

创建一个 RedisLock 类,用于实现获取锁和释放锁的逻辑:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

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

@Component
public class RedisLock {

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "EX";
    private static final String RELEASE_SUCCESS = "1";
    private static final String RELEASE_LUA;

    static {
        StringBuilder sb = new StringBuilder();
        sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] ");
        sb.append("then ");
        sb.append("    return redis.call(\"del\",KEYS[1]) ");
        sb.append("else ");
        sb.append("    return 0 ");
        sb.append("end ");
        RELEASE_LUA = sb.toString();
    }

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    private String clientId;

    public RedisLock() {
        this.clientId = UUID.randomUUID().toString();
    }

    // 获取锁
    public boolean tryLock(String key, long timeout, TimeUnit unit) {
        String result = redisTemplate.opsForValue().set(key, clientId, timeout, unit, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME);
        return LOCK_SUCCESS.equals(result);
    }

    // 释放锁
    public boolean releaseLock(String key) {
        Object result = redisTemplate.execute(
                (connection) -> connection.eval(
                        RELEASE_LUA.getBytes(),
                        org.springframework.data.redis.connection.RedisStringCommands.LuaScript.KEEP_SCRIPT,
                        1,
                        key.getBytes(),
                        clientId.getBytes()
                )
        );
        return RELEASE_SUCCESS.equals(result);
    }
}

使用 RedisLock

在业务代码中,使用 RedisLock 来获取锁和释放锁:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {

    @Autowired
    private RedisLock redisLock;

    private static final String LOCK_KEY = "test_lock";

    @GetMapping("/test")
    public String test() {
        boolean locked = redisLock.tryLock(LOCK_KEY, 10, TimeUnit.SECONDS);
        if (locked) {
            try {
                // 执行业务逻辑
                Thread.sleep(5000);
                return "业务逻辑执行完成,锁已释放";
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                redisLock.releaseLock(LOCK_KEY);
            }
        } else {
            return "获取锁失败,请稍后重试";
        }
        return "操作异常";
    }
}

总结

以上就是在 Spring Boot 中结合 Redis 实现自定义分布式锁,以及解决设计分布式锁过程中常见问题的详细内容。希望这篇文章能帮助你在后端开发工作中,更好地运用分布式锁,提升系统的稳定性和并发性能。如果你在实际操作过程中还有其他问题,欢迎在评论区留言讨论,也别忘了点赞、分享给其他有需要的小伙伴!

相关推荐

【推荐】一个开源免费、AI 驱动的智能数据管理系统,支持多数据库

如果您对源码&技术感兴趣,请点赞+收藏+转发+关注,大家的支持是我分享最大的动力!!!.前言在当今数据驱动的时代,高效、智能地管理数据已成为企业和个人不可或缺的能力。为了满足这一需求,我们推出了这款开...

Pure Storage推出统一数据管理云平台及新闪存阵列

PureStorage公司今日推出企业数据云(EnterpriseDataCloud),称其为组织在混合环境中存储、管理和使用数据方式的全面架构升级。该公司表示,EDC使组织能够在本地、云端和混...

对Java学习的10条建议(对java课程的建议)

不少Java的初学者一开始都是信心满满准备迎接挑战,但是经过一段时间的学习之后,多少都会碰到各种挫败,以下北风网就总结一些对于初学者非常有用的建议,希望能够给他们解决现实中的问题。Java编程的准备:...

SQLShift 重大更新:Oracle→PostgreSQL 存储过程转换功能上线!

官网:https://sqlshift.cn/6月,SQLShift迎来重大版本更新!作为国内首个支持Oracle->OceanBase存储过程智能转换的工具,SQLShift在过去一...

JDK21有没有什么稳定、简单又强势的特性?

佳未阿里云开发者2025年03月05日08:30浙江阿里妹导读这篇文章主要介绍了Java虚拟线程的发展及其在AJDK中的实现和优化。阅前声明:本文介绍的内容基于AJDK21.0.5[1]以及以上...

「松勤软件测试」网站总出现404 bug?总结8个原因,不信解决不了

在进行网站测试的时候,有没有碰到过网站崩溃,打不开,出现404错误等各种现象,如果你碰到了,那么恭喜你,你的网站出问题了,是什么原因导致网站出问题呢,根据松勤软件测试的总结如下:01数据库中的表空间不...

Java面试题及答案最全总结(2025版)

大家好,我是Java面试陪考员最近很多小伙伴在忙着找工作,给大家整理了一份非常全面的Java面试题及答案。涉及的内容非常全面,包含:Spring、MySQL、JVM、Redis、Linux、Sprin...

数据库日常运维工作内容(数据库日常运维 工作内容)

#数据库日常运维工作包括哪些内容?#数据库日常运维工作是一个涵盖多个层面的综合性任务,以下是详细的分类和内容说明:一、数据库运维核心工作监控与告警性能监控:实时监控CPU、内存、I/O、连接数、锁等待...

分布式之系统底层原理(上)(底层分布式技术)

作者:allanpan,腾讯IEG高级后台工程师导言分布式事务是分布式系统必不可少的组成部分,基本上只要实现一个分布式系统就逃不开对分布式事务的支持。本文从分布式事务这个概念切入,尝试对分布式事务...

oracle 死锁了怎么办?kill 进程 直接上干货

1、查看死锁是否存在selectusername,lockwait,status,machine,programfromv$sessionwheresidin(selectsession...

SpringBoot 各种分页查询方式详解(全网最全)

一、分页查询基础概念与原理1.1什么是分页查询分页查询是指将大量数据分割成多个小块(页)进行展示的技术,它是现代Web应用中必不可少的功能。想象一下你去图书馆找书,如果所有书都堆在一张桌子上,你很难...

《战场兄弟》全事件攻略 一般事件合同事件红装及隐藏职业攻略

《战场兄弟》全事件攻略,一般事件合同事件红装及隐藏职业攻略。《战场兄弟》事件奖励,事件条件。《战场兄弟》是OverhypeStudios制作发行的一款由xcom和桌游为灵感来源,以中世纪、低魔奇幻为...

LoadRunner(loadrunner录制不到脚本)

一、核心组件与工作流程LoadRunner性能测试工具-并发测试-正版软件下载-使用教程-价格-官方代理商的架构围绕三大核心组件构建,形成完整测试闭环:VirtualUserGenerator(...

Redis数据类型介绍(redis 数据类型)

介绍Redis支持五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)及Zset(sortedset:有序集合)。1、字符串类型概述1.1、数据类型Redis支持...

RMAN备份监控及优化总结(rman备份原理)

今天主要介绍一下如何对RMAN备份监控及优化,这里就不讲rman备份的一些原理了,仅供参考。一、监控RMAN备份1、确定备份源与备份设备的最大速度从磁盘读的速度和磁带写的带度、备份的速度不可能超出这两...

取消回复欢迎 发表评论: