Redis代金卷(优惠卷)秒杀案例-多应用版
Redis代金卷(优惠卷)秒杀案例-单应用版-CSDN博客
上面这种方案,在多应用时候会出现问题,原因是你通过用户ID加锁
但是在多应用情况下,会出现两个应用的用户都有机会进去
让多个JVM使用同一把锁
这样就需要使用分布式锁
每个JVM都会有一个锁监视器,多个JVM就会有多个锁监视器
那么让所有JVM使用外部同一个锁监视器即可
不同的分布式锁实现方案
Redis分布式锁实现方案一
package com.hmdp.service.impl;
import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisIdWorker;
import com.hmdp.utils.SimpleRedisLock;
import com.hmdp.utils.UserHolder;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
/**
* <p>
* 服务实现类
* </p>
*
* @author 虎哥
* @since 2021-12-22
*/
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Autowired
private ISeckillVoucherService seckillVoucherService;
@Autowired
private RedisIdWorker redisIdWorker;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public Result seckillVoucher(Long voucherId) {
//1.查询优惠券
SeckillVoucher byId = seckillVoucherService.getById(voucherId);
if (byId.getBeginTime().isAfter(LocalDateTime.now())) {
//2.判断秒杀是否开始
return Result.fail("秒杀尚未开始");
}
if (byId.getEndTime().isBefore(LocalDateTime.now())) {
//3.判断秒杀是否结束
return Result.fail("秒杀已经结束");
}
if (byId.getStock() < 1) {
//4.判断库存是否充足
return Result.fail("库存不足");
}
Long id = UserHolder.getUser().getId();//表示获取用户id
SimpleRedisLock simpleRedisLock = new SimpleRedisLock("order:" + id, redisTemplate);
//id.toString()实际是new了一个String对象,然后调用intern()方法,这个方法会去常量池中查找有没有这个对象,如果有就返回这个对象,如果没有就添加到常量池中,然后返回这个对象
//synchronized (id.toString().intern()){
boolean isLock = simpleRedisLock.tryLock(10L);//对于计算机来说10秒已经够了 除非业务非常复杂
if(!isLock){
return Result.fail("不允许重复下单");
}
//如果这样调用,前面有个this 事务使用代理对象去执行的
//return createVoucherOrder(voucherId);
try {
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
}catch (Exception e){
e.printStackTrace();
return Result.fail(e.getMessage());
}finally {
simpleRedisLock.unlock();
}
// }
}
@Transactional(rollbackFor = Exception.class)
public Result createVoucherOrder(Long voucherId) {
Long id = UserHolder.getUser().getId();//
//一人一单的查询
Integer count = query().eq("user_id", id)
.eq("voucher_id", voucherId)
.count();
if(count > 0){
return Result.fail("您已经购买过一次了");
}
//5.扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId)
.gt("stock", 0)
.update();
if(!success){
return Result.fail("库存不足");
}
//6.创建订单
VoucherOrder voucherOrder = new VoucherOrder();
//6.1.订单id
long order = redisIdWorker.nextId("order");
voucherOrder.setId(order);
voucherOrder.setUserId(id);//登录用户先写死
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
Result.ok(order);
}
}
以上代码,在极端情况下会出现问题,例如 业务非常复杂,锁提前释放了,但是业务还没完成
可以通过线程标识 判断下是不是当前线程
原先用的是线程id,而线程ID往往在一个JVM中是递增的,但是考虑到多应用时候,可能出现线程ID相同的情况,因此,缓存UUID更好
改造代码
package com.hmdp.utils;
import cn.hutool.core.lang.UUID;
import com.hmdp.service.ILock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
/**
* @author hrui
* @date 2025/1/31 3:09
*/
public class SimpleRedisLock implements ILock {
private String name;
private RedisTemplate<String,Object> redisTemplate;//需要对RedisTemplate<String,Object>进行配置注入
//import cn.hutool.core.lang.UUID;会把UUID中的"-"去掉
private static final String ID_PREFIX=UUID.randomUUID().toString(true)+"-";//import cn.hutool.core.lang.UUID;
private static final String KEY_PREFIX = "lock:";
public SimpleRedisLock(RedisTemplate<String, Object> redisTemplate, String name) {
this.redisTemplate = redisTemplate;
this.name = name;
}
@Override
public boolean tryLock(long timeoutSec) {
Boolean success = redisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name,
ID_PREFIX + Thread.currentThread().getId(),//用线程ID做为value,可以更加直观些 其实放什么无所谓
timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);//避免空指针
}
@Override
public void unlock() {
String threadId = ID_PREFIX + Thread.currentThread().getId();
String id = (String) redisTemplate.opsForValue().get(KEY_PREFIX + name);
if (threadId.equals(id)) {
//符合 则删除,不符合就删除不了
redisTemplate.delete(KEY_PREFIX + name);
}
}
}
以上代码还是有问题
原因在于GC回收时候,还是有那么一点点可能因业务时长导致锁的自动删除
要保证加锁和解锁的原子性