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秒),防止业务未完成锁过期