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秒),防止业务未完成锁过期
相关推荐
用户8307196840824 小时前
Spring也会“选择困难”?五种方案帮你搞定@Autowired多bean注入
spring boot
2402_881319305 小时前
引入 Redis 分布式锁解决并发脏写 (Dirty Write)-AI模拟面试的构建rag部分
redis·分布式·面试
刘~浪地球6 小时前
Redis 从入门到精通(九):事务详解
数据库·redis·缓存
__土块__6 小时前
一次电商秒杀系统架构评审:从本地锁到分布式锁的演进与取舍
java·redis·高并发·分布式锁·redisson·架构设计·秒杀系统
她说..6 小时前
Java 注解核心面试题
java·spring boot·spring·spring cloud·自定义注解
用户8307196840826 小时前
Spring Boot @Qualifier深度解密:从“按名查找”到“分组批量注入”,一文掌握它的全部“隐藏技能”。
java·spring boot
小旭95277 小时前
Spring Data Redis 从入门到实战:简化 Redis 操作全解析
java·开发语言·spring boot·redis·spring
希望永不加班7 小时前
SpringBoot 多数据源配置(读写分离基础)
java·spring boot·后端·spring
无责任此方_修行中8 小时前
Redis 的"三面"人生:开源世界的权力转移
redis·后端·程序员