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之函数式接口解读

    java之函数式接口解读

    这篇文章主要介绍了java之函数式接口,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-03-03
  • springboot结合mybatis-plus基于session模拟短信注册功能

    springboot结合mybatis-plus基于session模拟短信注册功能

    本文主要介绍了springboot结合mybatis-plus基于session模拟短信注册功能,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-11-11
  • Java死锁避免的五种方法举例总结

    Java死锁避免的五种方法举例总结

    这篇文章主要介绍了Java死锁避免的五种方法,包括按固定顺序加锁、使用tryLock+超时、一次性申请所有资源、使用Lock替代synchronized以及检测与恢复,需要的朋友可以参考下
    2025-05-05
  • ArrayList在for循环中使用remove方法移除元素方法介绍

    ArrayList在for循环中使用remove方法移除元素方法介绍

    这篇文章主要介绍了ArrayList在for循环中使用remove方法移除元素的内容,介绍了具体代码实现,需要的朋友可以参考下。
    2017-09-09
  • Mybatis @SelectKey用法解读

    Mybatis @SelectKey用法解读

    这篇文章主要介绍了Mybatis @SelectKey用法解读,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-09-09
  • 详解SpringBoot缓存的实例代码(EhCache 2.x 篇)

    详解SpringBoot缓存的实例代码(EhCache 2.x 篇)

    这篇文章主要介绍了详解SpringBoot缓存的实例代码(EhCache 2.x 篇),具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-07-07
  • Java多线程 volatile关键字详解

    Java多线程 volatile关键字详解

    这篇文章主要介绍了Java多线程 volatile关键字详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-09-09
  • Springboot集成spring data elasticsearch过程详解

    Springboot集成spring data elasticsearch过程详解

    这篇文章主要介绍了springboot集成spring data elasticsearch过程详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-04-04
  • JavaWeb中HttpSession中表单的重复提交示例

    JavaWeb中HttpSession中表单的重复提交示例

    这篇文章主要介绍了JavaWeb中HttpSession中表单的重复提交,非常不错,具有参考借鉴价值,需要的朋友可以参考下
    2017-03-03
  • java连接mysql数据库学习示例

    java连接mysql数据库学习示例

    这篇文章主要介绍了java连接mysql数据库学习示例,需要的朋友可以参考下
    2014-03-03

最新评论