SpringBoot集成Redisson实现分布式锁的示例代码

 更新时间:2026年04月08日 08:33:44   作者:Zzxy  
本文介绍了使用Redisson实现分布式锁以解决并发预约号源超卖问题,首先添加Redisson依赖并配置RedissonClient,接着在预约服务中引入分布式锁,并自定义扣减号源方法,最后总结了分布式锁的实现原理,包括锁粒度、锁超时、可重入性和WatchDog机制,需要的朋友可以参考下

1、集成 Redisson

Redisson 是 Redis 官方推荐的 Java 客户端,实现了 可靠的分布式锁、可重入锁、读写锁 等,封装了底层复杂性。

1.1 添加 Redisson 依赖

<!-- Redisson(Redis分布式锁客户端) -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.23.4</version>
</dependency>

1.2 配置 RedissonClient

在application.yml中添加:

spring:
  redis:
    host: localhost
    port: 6379
    # Redisson自动配置,无需额外配置

2、分布式锁解决并发预约号源超卖问题

2.1 预约服务中,增加分布式锁

修改AppointmentServiceImpl.bookAppointment方法:

package com.example.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.springBoot.hospital.entity.Appointment;
import com.springBoot.hospital.entity.Department;
import com.springBoot.hospital.mapper.AppointmentMapper;
import com.springBoot.hospital.mapper.DepartmentMapper;
import com.springBoot.hospital.service.AppointmentService;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.text.SimpleDateFormat;
import java.util.concurrent.TimeUnit;
@Service
public class AppointmentServiceImpl implements AppointmentService {
    @Autowired
    private DepartmentMapper departmentMapper;
    @Autowired
    private AppointmentMapper appointmentMapper;
    @Autowired
    private RedissonClient redissonClient;
/*
    //预约操作后清除该用户的预约缓存
    @CacheEvict(value = "appointments", key = "'user:' + #userId + '_page:*'",allEntries = true)
    //Spring Cache 原生不支持通配符 * 在 key 中的模糊匹配。
    //allEntries = true会清空整个缓存名下的所有条目
    @Override
    @Transactional(rollbackFor = Exception.class) //任何异常都回滚
    public boolean bookAppointment(String appointmentNo,Long userId, Long departmentId, String appointmentDate) {
        //1\插入预约记录
        Appointment appointment = new Appointment();
        appointment.setAppointmentNo(appointmentNo);
        appointment.setUserId(userId);
        appointment.setDepartmentId(departmentId);
        //将字符串转为Date(格式:yyyy-mm-dd)
        try {
            //使用SimpleDateFormat 定义日期格式
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-mm-dd");
            Date date = sdf.parse(appointmentDate); //字符串转为Date
            appointment.setAppointmentDate(date);
        } catch (Exception e) {
            throw new RuntimeException("日期格式错误",e);
        }
        appointment.setStatus("PENDING"); //状态:待就诊
        int rows = appointmentMapper.insert(appointment);
        if(rows == 0){
            throw new RuntimeException("插入预约失败");
        }
        //2\扣减号源,department中
        //使用条件更新,避免并发
        LambdaUpdateWrapper<Department> updateWrapper = new LambdaUpdateWrapper<>();
        updateWrapper.eq(Department::getId,departmentId)
                .gt(Department::getRemaining,0)
                .setSql("order_num = order_num - 1");
        int updateRows = departmentMapper.update(null,updateWrapper);
        if(updateRows == 0){
            throw new RuntimeException("号源不足或科室不存在");
        }
        return true;
    }
 */
    @Override
    @Transactional(rollbackFor = Exception.class) //事务管理,任何异常都回滚
    public boolean bookAppointment(String appointmentNo, Long userId, Long departmentId, String appointmentDate) {
        //构建分布式锁的key(科室Id + 预约日期)
        String lockKey = "lock:dept:" + departmentId + ":date:" + appointmentDate;
        //创建对象,Redisson 根据 key 获取一个可重入锁对象。
        RLock lock = redissonClient.getLock(lockKey);
        boolean locked = false;
        try {
            // 尝试加锁,最多等待3秒,上锁后10秒自动释放(防止死锁)
            locked = lock.tryLock(3, 10, TimeUnit.SECONDS);
            if(!locked){
                throw new RuntimeException("系统繁忙,请稍后重试");
            }
            //1 检查号源
            Department dept = departmentMapper.selectById(departmentId);
            if(dept == null || dept.getRemaining() == null || dept.getRemaining() <= 0){
                throw new RuntimeException("号源不足");
            }
            //2 插入预约记录
            Appointment appointment = new Appointment();
            appointment.setAppointmentNo(appointmentNo);
            appointment.setUserId(userId);
            appointment.setDepartmentId(departmentId);
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
            appointment.setAppointmentDate(sdf.parse(appointmentDate));
            appointment.setStatus("PENDING");
            int insertRows = appointmentMapper.insert(appointment);
            if(insertRows == 0){
                throw new RuntimeException("插入预约失败");
            }
            //3 扣减号源
            //使用自定义扣减号源方法,SQL条件更新号源数量
            int updateRows = departmentMapper.decreaseRemaining(departmentId);
            if(updateRows == 0){
                throw new RuntimeException("扣减号源失败,可能已被抢走");
            }
            return true;
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage(),e);
        } finally {
            //释放锁,只释放当前线程持有的锁
            if(locked && lock.isHeldByCurrentThread()){
                lock.unlock();
            }
        }
    }
    //分页查询缓存,分页参数影响key
    @Cacheable(value = "appointments", key = "'user:' + #userId + '_page:' + #pageNum + '_size:' + #pageSize")
    @Override
    public IPage<Appointment> findAppointmentsByUserId(Long userId, Integer pageNum, Integer pageSize) {
        Page<Appointment> page = new Page<>(pageNum,pageSize);
        LambdaQueryWrapper<Appointment> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Appointment::getUserId,userId)
                .orderByDesc(Appointment::getAppointmentDate);
        return appointmentMapper.selectPage(page,queryWrapper);
    }
}

2.2 自定义扣减号源方法

在DepartmentMapper中添加自定义扣减方法

// DepartmentMapper.java
@Mapper
public interface DepartmentMapper extends BaseMapper<Department> {
    
    // 自定义扣减号源(使用乐观锁防止超卖)
    @Update("UPDATE department SET order_num = order_num - 1 where id = #{departmentId}")
    int decreaseRemaining(@Param("departmentId") Long departmentId);
}

3、分布式锁原理总结

  • 锁粒度:应该细化到 科室+日期,而不是整个系统
  • 锁超时:设置合理超时时间,避免业务执行过长导致锁自动释放
  • 可重入性:Redisson支持可重入,同一线程可重复获取
  • Watch Dog机制:Redisson的锁会自动续期(默认30秒),防止业务未完成锁过期

以上就是SpringBoot集成Redisson实现分布式锁的示例代码的详细内容,更多关于SpringBoot Redisson分布式锁的资料请关注脚本之家其它相关文章!

相关文章

  • Java程序的初始化顺序,static{}静态代码块和实例语句块的使用方式

    Java程序的初始化顺序,static{}静态代码块和实例语句块的使用方式

    这篇文章主要介绍了Java程序的初始化顺序,static{}静态代码块和实例语句块的使用方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-01-01
  • 在Spring Boot使用Undertow服务的方法

    在Spring Boot使用Undertow服务的方法

    Undertow是RedHAT红帽公司开源的产品,采用JAVA开发,是一款灵活,高性能的web服务器,提供了NIO的阻塞/非阻塞API,也是Wildfly的默认Web容器,这篇文章给大家介绍了在Spring Boot使用Undertow服务的方法,感兴趣的朋友跟随小编一起看看吧
    2023-05-05
  • 通过Java实现zip文件与rar文件解压缩的详细步骤

    通过Java实现zip文件与rar文件解压缩的详细步骤

    这篇文章主要给大家介绍了如何通过 Java 来完成 zip 文件与 rar 文件的解压缩,文中通过代码示例讲解的非常详细,对大家的学习或工作有一定的帮助,需要的朋友可以参考下
    2024-07-07
  • springboot 拦截器执行两次的解决方案

    springboot 拦截器执行两次的解决方案

    这篇文章主要介绍了springboot 拦截器执行两次的解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-09-09
  • Maven添加reactor依赖失败的解决方案

    Maven添加reactor依赖失败的解决方案

    起初是自己在学spring boot3,结果到了reactor这一部分的时候,在项目的pom.xml文件中添加下列依赖报错,接下来通过本文给大家介绍Maven添加reactor依赖失败的解决方案,需要的朋友可以参考下
    2024-06-06
  • 解决SpringMVC接收不到ajaxPOST参数的问题

    解决SpringMVC接收不到ajaxPOST参数的问题

    今天小编就为大家分享一篇解决SpringMVC接收不到ajaxPOST参数的问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2018-08-08
  • logback日志级别设置无效问题及解决

    logback日志级别设置无效问题及解决

    文章总结:文章介绍了在Spring Boot项目中配置日志级别时,优先级问题,通过查阅资料发现,配置文件中的logging.level.xx也能配置日志级别,且优先级比logback.xml高,文章还提供了源码分析,说明了Spring Boot如何处理日志级别配置
    2025-11-11
  • Maven中optional和scope元素的使用弄明白了吗

    Maven中optional和scope元素的使用弄明白了吗

    这篇文章主要介绍了Maven中optional和scope元素的使用弄明白了吗,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-12-12
  • 关于feign接口动态代理源码解析

    关于feign接口动态代理源码解析

    这篇文章主要介绍了关于feign接口动态代理源码解析,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-03-03
  • java微信公众号发送消息模板

    java微信公众号发送消息模板

    这篇文章主要为大家详细介绍了java微信公众号发送消息模板,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-08-08

最新评论