秒杀业务流程(实现一人一单):
1. 检查秒杀是否已经开始 ,或者秒杀活动是否结束
2. 判断库存是否充足
3. 扣减库存
4. 创建订单
5.返回 订单ID
事务提交前和提交后释放锁的问题。
1.
@Transactional
public Result createVoucherOrder1(Long voucherId) {
// 5.一人一单
Long userId = UserHolder.getUser().getId();
// 创建锁对象 获取 分布式锁
SimpleRedisLock redisLock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
// 尝试获取锁
boolean isLock = redisLock.tryLock(1200);
// 判断
if(!isLock){
// 获取锁失败,直接返回失败或者重试
return Result.fail("不允许重复下单!");
}
try {
// 5.1.查询订单
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
// 5.2.判断是否存在
if (count > 0) {
// 用户已经购买过了
return Result.fail("用户已经购买过一次!");
}
// 6.扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1") // set stock = stock - 1
.eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0
.update();
if (!success) {
// 扣减失败
return Result.fail("库存不足!");
}
// 7.创建订单
VoucherOrder voucherOrder = new VoucherOrder();
// 7.1.订单id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
// 7.2.用户id
voucherOrder.setUserId(userId);
// 7.3.代金券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
// 7.返回订单id
return Result.ok(orderId);
} finally {
// 释放锁
redisLock.unlock();
}
}
2. 获取事务的代理对象,防止事务的失效,
@Override
public Result seckillVoucher(Long voucherId) {
// 1.查询优惠券
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
// 2.判断秒杀是否开始
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
// 尚未开始
return Result.fail("秒杀尚未开始!");
}
// 3.判断秒杀是否已经结束
if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
// 尚未开始
return Result.fail("秒杀已经结束!");
}
// 4.判断库存是否充足
if (voucher.getStock() < 1) {
// 库存不足
return Result.fail("库存不足!");
}
Long userId = UserHolder.getUser().getId();
// 创建锁对象 获取 分布式锁
// SimpleRedisLock redisLock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
RLock lock = redissonClient.getLock("lock:order"+userId);
// 获取锁对象
boolean isLock = lock.tryLock();
// 获取锁对象
// boolean isLock = redisLock.tryLock(1200);
// 判断是否 获取锁 成功
if(!isLock){
// 一个人只运行一单 2011f4dd7cd042678b5ae1bcb8c95fdb-76
return Result.fail("1200");
}
设置线程互斥 创建订单业务 的 实现
//
try {
synchronized (userId.toString().intern()) {
// 获取代理对象 (事务)
// 这里涉及到 Spring事务失效的几种可能性之一 使用代理对象 进行解决
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
}
}finally {
// 锁的释放
lock.unlock();
// redisLock.unlock();
}
}
同时暴露代理对象
package com.hmdp;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.scheduling.annotation.EnableAsync;
@MapperScan("com.hmdp.mapper")
@EnableAspectJAutoProxy(exposeProxy = true) // 暴露与aop 实现事务有关的代理对象
@SpringBootApplication
@EnableAsync
public class HmDianPingApplication {
public static void main(String[] args) {
SpringApplication.run(HmDianPingApplication.class);
}
}
解决超卖问题: 超卖问题是典型的多线程安全问题,针对这一问题的常见解决方案就是加锁:
加锁方案:
悲观锁: 认为线程安全问题一定会发生,因此在操作数据之前先获取锁,确保线程串行执行。例如Synchronized、Lock都属于悲观锁
乐观锁:认为线程安全问题不一定会发生,因此不加锁,只是在更新数据时去判断有没有其它线程对数据做了修改。如果没有修改则认为是安全的,自己才更新数据。如果已经被其它线程修改说明发生了安全问题,此时可以重试或异常。
分布式锁: 使用setnx 实现 多jvm 的锁监视器,(多进程可见)
redis 实现 分布式锁的操作:
# 获取锁的操作 (ex: 加上过期时间) 非阻塞方式的等待
set lock thread1 nx ex 10
# 释放锁
del lock
分布式锁的实现方案
1. 有阻塞方案 (使用重试机制)
/**
* 阻塞的分布式锁
* @param name
* @param expire
* @param timeout 重试时间限制
* @return
*/
private boolean Lock(String name,long expire,long timeout){
long startTime= System.currentTimeMillis();
boolean ans = false;
do{
ans = tryLock2(name,expire);
if(!ans) {
if (System.currentTimeMillis() - startTime > timeout) {
break;
}
try {
Thread.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}while (!ans) ;
return false;
}
2. 无阻塞方案
/**
* 获取 互斥锁
* 无阻塞的分布式锁
* @param key
* @return
*/
private boolean tryLock(String key) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
1-1 .Redis锁的实现
第一版本: 使用 stringRestTemplate 实现 分布式锁
/**
* 秒杀业务
* 1. 判断用户是否可以进行秒杀
* 2. 扣减优惠券的库存 (扣库存)
* 秒杀时间的 判断
* 3. 将用户优惠券的信息写入到订单 (创建订单) --> 进行订单的创健
* 一人一单的判断 是否买过
* 使用分布式锁 获取成功
* */
@Override
public Result seckillVoucher(Long voucherId) {
// 1.查询优惠券
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
// 2.判断秒杀是否开始
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
// 尚未开始
return Result.fail("秒杀尚未开始!");
}
// 3.判断秒杀是否已经结束
if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
// 尚未开始
return Result.fail("秒杀已经结束!");
}
// 4.判断库存是否充足
if (voucher.getStock() < 1) {
// 库存不足
return Result.fail("库存不足!");
}
Long userId = UserHolder.getUser().getId();
// 创建锁对象 获取 分布式锁 将 业务标识和 userId 的 拼接作为锁的 key
SimpleRedisLock redisLock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
// 过期时间
boolean isLock = redisLock.tryLock(1200);
// // 2. 使用 Redission的方式获取锁
// RLock lock = redissonClient.getLock("lock:order"+userId);
// boolean isLock = lock.tryLock();
// 获取锁对象
// boolean isLock = redisLock.tryLock(1200);
// 判断是否 获取锁 成功
if(!isLock){
// 一个人只允许下一单
return Result.fail("一个人只允许下一单 ");
}
设置线程互斥 创建订单业务 的 实现
//
try {
synchronized (userId.toString().intern()) {
// 获取代理对象 (事务)
// 这里涉及到 Spring事务失效的几种可能性之一 使用代理对象 进行解决
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
}
}finally {
// 锁的释放
redisLock.unlock();
// lock.unlock();
}
}
1- 2. 第一版本的分布式锁会出现的问题。
在释放锁时可能会出现误删的问题
代码如下:
import cn.hutool.core.lang.UUID;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
/**
* ILock redis 锁机制 分布式锁的实现
* */
public class SimpleRedisLock implements ILock {
private String name;
private StringRedisTemplate stringRedisTemplate;
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
private static final String KEY_PREFIX = "lock:";
// 使用 UUID 拼接线程ID
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
// 定义锁的lua 脚本 返回值类型为 指定泛型
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
// redis 的操作
UNLOCK_SCRIPT = new DefaultRedisScript<>();
// 从 classPath中查找并读取 lua脚本内容
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);
}
/**
* .
*
* @author zgy
* @date 2:43
* @param timeoutSec
* @return 返回 是否
* @methodName tryLock
* 改进后的分布式锁的实现方案
**/
@Override
public boolean tryLock(long timeoutSec) {
// 获取线程标示
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 获取锁
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent
(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
/**
* 释放锁的逻辑 通过 lua脚本进行优化
*/
// @Override
// public void unlock() {
// // 调用lua脚本 保证判断删除 锁 时 操作的 原子性
// stringRedisTemplate.execute( // 判断锁是否是本线程创建的
// UNLOCK_SCRIPT,
// Collections.singletonList(KEY_PREFIX + name),
// // 即 线程标示
// ID_PREFIX + Thread.currentThread().getId());
// }
/*
分布式锁情况下 释放锁的逻辑
*/
@Override
public void unlock() {
// 获取线程标示
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 获取锁中的标示
String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
// 判断标示是否一致
if(threadId.equals(id)) {
// 释放锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
1-3: Redis分布式锁的遇到的问题 : 进行了锁标识的判断,防止了锁的误删问题,此时仍然有可能会在Full GC 时出现 业务代码执行的阻塞导致锁的超时释放问题。此时需要做的是保证判断锁标识的操作和释放锁的操作的原子性。
使用lua脚本命令对Redis锁的释放逻辑进行优化,保证判断线程标识和删除锁操作的原子性操作。
-- 释放锁的 lua 脚本
if (redis.call('GET',keys[1]==argv[1])) then
-- 删除 第一个键值对
return redis.call('del',keys[1])
end
return 0;
判断和删除是原子性的操作,
/**
* 释放锁的逻辑 通过 lua脚本进行优化
*/
@Override
public void unlock() {
// 调用lua脚本 保证判断删除 锁 时 操作的 原子性
stringRedisTemplate.execute( // 判断锁是否是本线程创建的
UNLOCK_SCRIPT,
// 指定锁的key
Collections.singletonList(KEY_PREFIX + name),
// 即 线程标示
ID_PREFIX + Thread.currentThread().getId());
}
上述分布式锁的实现存在的问题。
1. 不可重入
2. 不可重试
3. 超时释放
4. 主从一致
2 Redission实现 分布式的可重入锁机制.及实现原理
/**
* 释放锁的逻辑 通过 lua脚本进行优化
*/
@Override
public void unlock() {
// 调用lua脚本 保证判断删除 锁 时 操作的 原子性
stringRedisTemplate.execute( // 判断锁是否是本线程创建的
UNLOCK_SCRIPT,
// 指定锁的key
Collections.singletonList(KEY_PREFIX + name),
// 即 线程标示
ID_PREFIX + Thread.currentThread().getId());
}
Redission配置类
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.apache.tomcat.jni.SSL.setPassword;
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient(){
// 配置
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379")
// 当 密码 没有时的情况 不用设置
.setPassword("132132");
// 创建RedissonClient对象
return Redisson.create(config);
}
}
使用 Redission对一人一单业务进行改造,实现分布式锁方案
/**
* 秒杀业务
* 1. 判断用户是否可以进行秒杀
* 2. 扣减优惠券的库存 (扣库存)
* 秒杀时间的 判断
* 3. 将用户优惠券的信息写入到订单 (创建订单) --> 进行订单的创健
* 一人一单的判断 是否买过
* 使用分布式锁 获取成功
* */
@Override
public Result seckillVoucher(Long voucherId) {
// 1.查询优惠券
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
// 2.判断秒杀是否开始
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
// 尚未开始
return Result.fail("秒杀尚未开始!");
}
// 3.判断秒杀是否已经结束
if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
// 尚未开始
return Result.fail("秒杀已经结束!");
}
// 4.判断库存是否充足
if (voucher.getStock() < 1) {
// 库存不足
return Result.fail("库存不足!");
}
Long userId = UserHolder.getUser().getId();
// 创建锁对象 获取 分布式锁 将 业务标识和 userId 的 拼接作为锁的 key
// SimpleRedisLock redisLock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
// 过期时间
// boolean isLock = redisLock.tryLock(1200);
// // 2. 使用 Redission的方式获取锁
RLock lock = redissonClient.getLock("lock:order"+userId);
boolean isLock = lock.tryLock();
// 获取锁对象
// boolean isLock = redisLock.tryLock(1200);
// 判断是否 获取锁 成功
if(!isLock){
// 一个人只允许下一单
return Result.fail("一个人只允许下一单 ");
}
设置线程互斥 创建订单业务 的 实现
//
try {
synchronized (userId.toString().intern()) {
// 获取代理对象 (事务)
// 这里涉及到 Spring事务失效的几种可能性之一 使用代理对象 进行解决
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
}
}finally {
// 锁的释放
// redisLock.unlock();
lock.unlock();
}
}
1.首先判断锁是否存在,
2. 如果存在 则 判断锁的标识是否是自己 ,发现不是则返回,表示锁获取失败
3. 如果判断锁标识是自己的,锁计数+1, 设置锁的有效期, 而后执行业务,
4. 执行业务时判断锁是否是自己的,如果是则锁计数-1 ,表示退出,而后判断锁计数是否为0
如果为0 则 释放锁,否则重置锁的有效期,执行业务。一旦判断锁不是自己的则马上进行释放锁的操作。