全局唯一Id生成器
为实现唯一性、递增性、安全性、高可用、高性能,能支持未来大量订单业务的订单id的快速生成,给出一种id生成的方法。
代码实现
package com.hmdp.utils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
/**
* @Description TODO
* @Author ygw
* @Date 2022/10/18 9:13
* @Version 1.0
*/
@Component
public class RedisIdWorker {
/**
* 开始的时间戳
*/
private static final long BEGIN_TIMESTAMP = 1640995200L;
@Resource
StringRedisTemplate stringRedisTemplate;
public long nextId(String key){
//1、生成时间戳
LocalDateTime time = LocalDateTime.now();
long nowTimestamp = time.toEpochSecond(ZoneOffset.UTC);
long timestamp = nowTimestamp - BEGIN_TIMESTAMP;
//2、生成序列号,由于给递增序列号设置为32位,需要在生成序列号时给出限制
DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("yyyy:MM:dd");
String date = time.format(timeFormatter);
/**
* 重点关注此处redis数据结构的设置,inc:deal:20221018 - value,也就是说一天允许生成2^32个订单
*/
Long count = stringRedisTemplate.opsForValue().increment("inc:" + key + ":" + date);
//3、合并生成id
long id = (timestamp << 32) | count;
return id;
}
}
秒杀业务流程
业务流程
说明:
1、抢购前,需要判断当前时间活动是否开始或结束
2、如果当前为活动进行时间段,对库存进行判断,存量减一(需要考虑并发)
3、抢到了消费券之后,就直接创建对应的id
注意:
需要开启事务,Spring默认使用数据库的隔离级别,mysql的默认隔离级别为可重复读(Repeated Read)(可重复读。在同一个事务内的查询都是事务开始时刻一致的,InnoDB默认级别。在SQL标准中,该隔离级别消除了不可重复读,但是还存在幻象读,但是innoDB解决了幻读),也就是说不用考虑脏读、不可重复读的问题。
代码实现
SeckillVoucherServiceImpl.seckillVoucher()
@Service
@Slf4j
public class SeckillVoucherServiceImpl extends ServiceImpl<SeckillVoucherMapper, SeckillVoucher> implements ISeckillVoucherService {
@Resource
private IVoucherOrderService voucherOrderService;
@Resource
private RedisIdWorker redisIdWorker;
@Override
@Transactional
public Result seckillVoucher(Long voucherId) {
SeckillVoucher voucher = query().eq("voucher_id", voucherId).one();
LocalDateTime now = LocalDateTime.now();
//1、判断当前时间是否处在活动时间段内
if (now.isBefore(voucher.getBeginTime()) || now.isAfter(voucher.getEndTime())) {
//如果不再活动的时间段内,之间返回
return Result.fail("请等待活动开放后重试");
}
//2、如果处在活动时间段内,对库存进行判断
if(voucher.getStock() < 1){
return Result.fail("优惠券已经抢完了");
}
//3、如果库存大于0,优惠券数量减一并创建订单
boolean success = update().setSql("stock = stock - 1").eq("voucher_id", voucherId).gt("stock", 0).update();
if(!success){
return Result.fail("优惠券被抢完了");
}
//4、创建订单返回订单的id
long orderId = redisIdWorker.nextId("order");
Long userId = UserHolder.getUser().getId();
VoucherOrder voucherOrder = new VoucherOrder();
voucherOrder.setUserId(userId);
voucherOrder.setId(orderId);
voucherOrder.setVoucherId(voucherId);
voucherOrderService.save(voucherOrder);
return Result.ok(orderId);
}
}
乐观锁解决超卖问题(并发)
也可以利用串行化的事务隔离级别来解决超卖的问题,但是效率很低
超卖问题可以看成是一种略高于不可重复读的问题,需要在可重复读的事务隔离级别上加锁
问题分析
说明:版本号法就是在select的时候获取版本号,此时如果有多个线程获取到同样的数据;那么最先修改数据的线程在修改数据的同时修改版本号,其他线程在修改数据的时候应该先比较版本号;如果版本号发生改变则无法修改数据,事务回滚。
说明:由版本号法的图中可以发现,仅在版本号为1时进行修改;利用这个特性刚好可以卡住使用stock为1时的状态,即在修改时对stock的值进行判断
代码实现
根据以上分析,可以就用stock的状态来作为一个乐观锁,代码部分只改变更新时的操作
boolean success = update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId)
.gt("stock", 0) //增加比较条件,即stock > 0
.update();
悲观锁解决一人一单的问题(并发)
问题分析
说明:
在基本秒杀业务的基础上,增加了一个判断优惠券订单是否存在的分支,如果该用户已经抢到了优惠券就直接返回
代码实现
SeckillVoucherServiceImpl.java
@Service
@Slf4j
public class SeckillVoucherServiceImpl extends ServiceImpl<SeckillVoucherMapper, SeckillVoucher> implements ISeckillVoucherService {
@Resource
private IVoucherOrderService voucherOrderService;
@Resource
private RedisIdWorker redisIdWorker;
@Override
public Result seckillVoucher(Long voucherId) {
SeckillVoucher voucher = query().eq("voucher_id", voucherId).one();
LocalDateTime now = LocalDateTime.now();
//1、判断当前时间是否处在活动时间段内
if (now.isBefore(voucher.getBeginTime()) || now.isAfter(voucher.getEndTime())) {
//如果不再活动的时间段内,之间返回
return Result.fail("请等待活动开放后重试");
}
//2、如果处在活动时间段内,对库存进行判断
if(voucher.getStock() < 1){
return Result.fail("优惠券已经抢完了");
}
//3、如果库存大于0,优惠券数量减一并创建订单
Long userId = UserHolder.getUser().getId();
//使用intern()是为了确保锁住的是toString后常量池中的值,而不是引用
synchronized (userId.toString().intern()){
/**
* 关于事务失效的说明
* 在spring中我们将SeckillVoucherServiceImpl交由proxy来进行代理
* 也就是说SeckillVoucherServiceImpl中的事务实际上由proxy来完成
* 直接调用createVoucherOrder()事务实际上调用的是SeckillVoucherServiceImpl.createVoucherOrder()
* 会引起事务的失效,因此下面的操作是为了防止事务失效
*/
//需要加上aspectjweaver依赖,并在启动程序上开启@EnableAspectJAutoProxy(exposeProxy = true)
ISeckillVoucherService seckillVoucherService = (ISeckillVoucherService) AopContext.currentProxy();
return seckillVoucherService.createVoucherOrder(voucherId);
}
}
/**
* 我所理解的此处的事务为:
* 事务确保的要么都成功,要么都失败然后进行回滚->是对事务的完整性进行保证
* 锁是锁住变量然后针对这个变量进行的一系列操作->是对并发安全进行保证
* @param voucherId
* @return
*/
@Transactional
public Result createVoucherOrder(Long voucherId){
Long userId = UserHolder.getUser().getId();
/**
* TODO 需要判断该用户是否已经抢到了优惠券
* 并发情况下,可能有多个线程同时进入查询,获得相同的数据,同时满足了更新条件
* 因此我们在查询时,需要对用户的id进行加锁
* 此种方法选用的是悲观锁,直接加synchronized即可
*/
Integer count = voucherOrderService.query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if(count > 0){
return Result.fail("优惠券每人限领一张");
}
boolean success = update().setSql("stock = stock - 1").eq("voucher_id", voucherId).gt("stock", 0).update();
if(!success){
return Result.fail("优惠券被抢完了");
}
//4、创建订单返回订单的id
long orderId = redisIdWorker.nextId("order");
VoucherOrder voucherOrder = new VoucherOrder();
voucherOrder.setUserId(userId);
voucherOrder.setId(orderId);
voucherOrder.setVoucherId(voucherId);
voucherOrderService.save(voucherOrder);
return Result.ok(orderId);
}
}
标签:优惠券,Redis,voucherId,秒杀,voucher,Result,id,stock
From: https://www.cnblogs.com/Gw-CodingWorld/p/16804087.html