Spring Boot 集成 Redisson 实现分布式锁

1、集成 Redisson

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

1.1 添加 Redisson 依赖

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

1.2 配置 RedissonClient

在application.yml中添加:

yml 复制代码
spring:
  redis:
    host: localhost
    port: 6379
    # Redisson自动配置,无需额外配置

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

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

修改AppointmentServiceImpl.bookAppointment方法:

java 复制代码
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中添加自定义扣减方法

java 复制代码
// 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秒),防止业务未完成锁过期
相关推荐
Java陈序员11 小时前
企业级!一个基于 Java 开发的开源 AI 应用开发平台!
spring boot·agent·mcp
杨运交19 小时前
[041][公共模块]分布式唯一ID生成器设计与实现:一款灵活可扩展的雪花算法框架
spring boot
用户3074596982071 天前
Redis 延时队列详解
redis
烤代码的吐司君2 天前
Redis 数据结构 ZSet, BIT, HyperLogLog,Geo 空间数据
redis·后端
Flittly2 天前
【AgentScope Java新手村系列】(14)人机交互
java·spring boot·spring
Flynt3 天前
从Spring Boot 4.0升到4.1,我在Maven和gRPC上栽了跟头
java·spring boot·后端
掉鱼的猫4 天前
Spring Boot → Solon 注解迁移实战指南:一张对照表说清楚
java·spring boot
leeyi4 天前
Checkpoint 机制:Agent 怎么在断电后接着跑
redis·aigc·agent
人活一口气4 天前
Spring Boot与AIGC的完美结合:从零搭建智能内容生成平台
java·spring boot·aigc
云技纵横5 天前
一个 @Async 让循环依赖暴雷:Spring 代理的暗坑
redis