订单ID必须是唯一
唯一ID构成:
代码生成唯一ID:
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
//基于redis自增长的生成策略
@Component
public class RedisUUID {
//起始时间时间秒数
private static final long BEGIN_TIMESTAMP=1640995200L;
//使用Redis自增策略
private StringRedisTemplate stringRedisTemplate;
public RedisUUID(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
//参数是业务的类型
public long nextid(String keyType){
//1,生成时间戳
LocalDateTime now = LocalDateTime.now();
long nowsecend = now.toEpochSecond(ZoneOffset.UTC);
//当前时间的秒数减去起始时间的秒数得到时间戳
long nowtime_stamp = nowsecend - BEGIN_TIMESTAMP;
//2,生成序列号
String nowdate = now.format(DateTimeFormatter.ofPattern("yyyy:mm:dd"));//使得每天都会生成新的一轮ID
long count = stringRedisTemplate.opsForValue().increment("icr:" + keyType + ":" + nowdate);
//3,拼接返回
return nowtime_stamp << 32 | count;
}
}
商品下单操作
业务逻辑:
思路:主要是要了解以及掌握整个业务的流程:①先看商品是不是在秒杀的时间范围内②然后还要去看库存中是否还有该商品③如果有的话就扣减库存④然后就会生成订单,订单ID为唯一ID⑤把订单写入数据库中,再返回数据给前端
代码实现:
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Autowired
private ISeckillVoucherService iSeckillVoucherService;
@Autowired
private RedisUUID redisUUID;
@Override
@Transactional
public Result seckillVoucher(Long voucherId) {
//1,查询商品的信息
SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);
//2,看是否在秒杀时间范围内
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
return Result.fail("尚未开始!");
}
if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
return Result.fail("已经结束啦!");
}
//3,再看库存是否还有
if (voucher.getStock()<1) {
return Result.fail("库存不足!");
}
//4,如果有就减扣库存
boolean sucess = iSeckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).update();
if (!sucess) {
return Result.fail("库存不足!");
}
//5,然后就创建订单信息
VoucherOrder voucherOrder = new VoucherOrder();
//5.1,订单id----id生成器
long order = redisUUID.nextid("order");
voucherOrder.setVoucherId(order);
//5.2,用户id
Long id = UserHolder.getUser().getId();
voucherOrder.setUserId(id);
//5.3,商品id
voucherOrder.setVoucherId(voucherId);
//6,保存进数据库
save(voucherOrder);
//7,返回数据
return Result.ok(order);
}
}
库存超卖问题
先看看什么是库存超卖问题:
正常情况:
但是涉及到高并发的时候一定会出问题:
所以我们要想办法去解决这个问题,锁!!!
悲观锁认为一定发生并发问题,所以每一次操作都会加锁,是线程串行进行,不会出现并发问题,但是这样的话就导致性能降低,所以我们使用乐观锁,乐观锁是先让你操作,等你要修改数据库的时候再判断与你查到的数据是否是一样,如果是一样的才可以修改,否则不可以减库存。
乐观锁的两种实现判断法:
第一种:版本号法,就是通过查询两次版本号来判断是否被修改过库存
第二种:CAS法,是在版本号法上做的改进方法,既然要判断两次版本是否相同,为啥不判断库存量是否相同呢,所以CSA法就是去判断前后两次查询到的库存量是否一样,如果一样就可以改
用乐观锁CAS法来解决超卖问题:
//4,如果有就减扣库存
boolean sucess = iSeckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId)
.eq("stock",voucher.getStock())
.update();
if (!sucess) {
return Result.fail("库存不足!");
}
但是这样任然还不能解决超卖问题,因为如果两个线程同时来查到100,线程1做完修改还剩99,线程2查到不是100就会不执行修改,这样也会有问题,所以又要进行改进策略
//4,如果有就减扣库存
boolean sucess = iSeckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId)
.gt("stock",0)
.update();
if (!sucess) {
return Result.fail("库存不足!");
}
一人一单问题
使用悲观锁处理单体服务下的多线程问题:
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Autowired
private ISeckillVoucherService iSeckillVoucherService;
@Autowired
private RedisUUID redisUUID;
@Override
public Result seckillVoucher(Long voucherId) {
//1,查询商品的信息
SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);
//2,看是否在秒杀时间范围内
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
return Result.fail("尚未开始!");
}
if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
return Result.fail("已经结束啦!");
}
//3,再看库存是否还有
if (voucher.getStock()<1) {
return Result.fail("库存不足!");
}
//实现单体服务下的一人一单的多线程安全问题
Long id = UserHolder.getUser().getId();
//先获取锁,再提交事务,保证线程安全
synchronized (id.toString().intern()){
//获得Spring的代理对象(事务)
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
}
}
@Transactional
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("你已经购买过!");
}
//4,如果有就减扣库存
boolean sucess = iSeckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId)
.gt("stock",0)
.update();
if (!sucess) {
return Result.fail("库存不足!");
}
//5,然后就创建订单信息
VoucherOrder voucherOrder = new VoucherOrder();
//5.1,订单id----id生成器
long order = redisUUID.nextid("order");
voucherOrder.setVoucherId(order);
//5.2,用户id
voucherOrder.setUserId(id);
//5.3,商品id
voucherOrder.setVoucherId(voucherId);
//6,保存进数据库
save(voucherOrder);
//7,返回数据
return Result.ok(order);
}
}
添加依赖:
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
在启动类上添加:
@EnableAspectJAutoProxy(exposeProxy = true)
分布式集群模式下的多线程问题:
当我们是处理分布式集群模式下,两个JVM不是共用一把锁,导致每个JVM都有自己的锁导致我们之前的锁锁不住,每个JVM都有一个线程会获得锁。
分布式锁:满足分布式系统或者集群模式下多进程可见并且互斥的锁
基于Redis实现分布式锁:
创建锁对象:
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.TimeUnit;
public class SimpleRedisLock implements ILock{
private String name;
private StringRedisTemplate stringRedisTemplate;
private static final String KEY_PREFXY="lock:";
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean trylock(long timeoutSec) {
//获取线程ID作为标识
long ThreadId = Thread.currentThread().getId();
//获取锁
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFXY + name, ThreadId + "", timeoutSec, TimeUnit.MINUTES);
//避免空指针
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
stringRedisTemplate.delete(KEY_PREFXY + name);
}
}
代码实现Redis分布式锁的应用:
①先创建锁的对象,然后先是去获取锁②没有获取到锁就直接返回错误③获取到锁就可以进行对数据库的操作④操作完之后进行释放锁
Long id = UserHolder.getUser().getId();
//创建锁对象
SimpleRedisLock simpleRedisLock = new SimpleRedisLock("order" + id, stringRedisTemplate);
//获取锁
boolean trylock = simpleRedisLock.trylock(1200);
//判断是否获得锁成功
if (!trylock) {
//获取锁失败
return Result.fail("不允许重复下单!");
}
//获得Spring的代理对象(事务)
try {
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
} finally {
//释放锁
simpleRedisLock.unlock();
}
但是就上面的处理还不够严谨,因为如果一个线程发生阻塞的话,其他线程可能会获得锁并且释放锁,导致锁误删问题,
解决锁误删问题:
import cn.hutool.core.lang.UUID;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.TimeUnit;
public class SimpleRedisLock implements ILock{
private String name;
private StringRedisTemplate stringRedisTemplate;
private static final String KEY_PREFXY="lock:";
//得到一个唯一锁的标识
private static final String ID_PREFXY= UUID.randomUUID(true)+"-";
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean trylock(long timeoutSec) {
//获取线程标识
String ThreadId = ID_PREFXY+Thread.currentThread().getId();
//获取锁
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFXY + name, ThreadId, timeoutSec, TimeUnit.MINUTES);
//避免空指针
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
//获取线程标识
String ThreadId = ID_PREFXY+Thread.currentThread().getId();
//判断要来修改的进程跟锁的标识是否一致
String s = stringRedisTemplate.opsForValue().get(KEY_PREFXY + name);
if(ThreadId.equals(s)){
//释放锁
stringRedisTemplate.delete(KEY_PREFXY + name);
}
}
}