1.实战-优惠券秒杀
1.全局唯一id
1.1.设计
1.2.实现
/**
* 基于redis实现全局id
* */
@Component
public class RedisIdWork {
/**
* 开始时间戳
* */
private StringRedisTemplate stringRedisTemplate;
/**
* 序列号的位数
*/
private static final int COUNT_BITS=32;
public RedisIdWork(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
private static final long BEGIN_TIMESTAMP=1640995200L;
public long nextId(String keyPrefix){
//1.生成时间戳
LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long timestamp=nowSecond-BEGIN_TIMESTAMP;
//2.生成序列号
//2.1.获取当前日期,精准到天
String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
//2.2.自增长
Long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
//3.拼接并返回
return timestamp<<COUNT_BITS | count; //左移32位
}
}
1.3.总结
2.实现优惠券秒杀下单
2.1.设计
2.2.实现
@Override
@Transactional//保证原子性
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("库存不足");
}
//5.扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock=stock-1")
.eq("voucher", voucherId).update();
if(!success){
//扣减失败
return Result.fail("库存不足");
}
//6.创建订单
VoucherOrder voucherOrder = new VoucherOrder();
//6.1创建订单id
long orderId = redisIdWork.nextId("order");
voucherOrder.setId(orderId);
//6.2用户id
Long userId = UserHolder.getUser().getId();
voucherOrder.setUserId(userId);
//6.3代金券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);//插入表数据方法
//7.返回订单id
return Result.ok(orderId);
}
}
3.超卖问题
3.1.问题原因
3.2.解决方式:
3.3.乐观锁实现方式:
3.3.1.版本号法:
3.3.2.CAS法(Compare And Swap)
//5.扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock=stock-1")
.eq("voucher", voucherId).gt("stock",0)//where 库存>0,来实现CAS
.update();
4.一人一单
4.1.设计:
4.2.实现
为了保证高并发下高可用的性能需要加悲观锁
@Transactional
public Result creatVoucherOrder(Long voucherId) {
//5.一人一单
Long userId = UserHolder.getUser().getId();
synchronized (userId.toString().intern()){//保证同一个用户加锁,intern方法返回字符串对象的规范化表示形式
//5.1.查询订单
Integer count = query().eq("user_id", userId).eq("vocher_id", voucherId).count();
//5.2.判断是否存在
if(count>0){
//用户已经购买了
return Result.fail("用户已经购买过一次!");
}
//6.扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock=stock-1")
.eq("voucher", voucherId).gt("stock",0)
.update();
if(!success){
//扣减失败
return Result.fail("库存不足");
}
//7.创建订单
VoucherOrder voucherOrder = new VoucherOrder();
//7.1创建订单id
long orderId = redisIdWork.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);
}
}
}
4.3.安全问题
4.4.问题原因分析
集群部署时出现问题,会存在同一客户同时查询同时下单的情况
原因:每个新的集群会有新的tomcat 新的JVM
synchronized悲观锁只能保证单机jvm多个线程间的互斥,而不能保证集群下多个JVM线程间互斥,想要解决这个问题,要使用分布式锁
5.分布式锁
5.1.什么是分布式锁
满足分布式系统或集群模式下多进程可见并且互斥的锁.
5.2.原理:
5.3.分布式锁的实现
将setnx lock thread1和 expire lock 10放在一起保证原子性
最终
5.4.代码实现:
public interface ILock {
/**
* 尝试获取锁
* @param timeoutSec 锁持有的超时时间,过期时间
* @return true代表成功
*/
boolean tryLock(long timeoutSec);
/**
* 释放锁
*/
void unLock();
}
public class SimpleRedisLock implements ILock{
private StringRedisTemplate stringRedisTemplate;
private String name;
private static final String KEY_PREFIX ="lock:";
public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
this.stringRedisTemplate = stringRedisTemplate;
this.name = name;
}
@Override
public boolean tryLock(long timeoutSec) {
//获取线程标识
long id = Thread.currentThread().getId();
//获取锁
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, id + "", timeoutSec, TimeUnit.MINUTES);
return Boolean.TRUE.equals(success);//防止success为null 自动拆箱错误
}
@Override
public void unLock() {
//释放锁
stringRedisTemplate.delete(KEY_PREFIX+name);
}
}
@Resource
private StringRedisTemplate stringRedisTemplate;
@Transactional
public Result creatVoucherOrder(Long voucherId) {
//5.一人一单
Long userId = UserHolder.getUser().getId();
SimpleRedisLock simpleRedisLock = new SimpleRedisLock(stringRedisTemplate, "order:" + userId.toString().intern());
boolean isLock = simpleRedisLock.tryLock(1200);
if(!isLock){
return Result.fail("不能重复下单");
}
try {
//5.1.查询订单
Integer count = query().eq("user_id", userId).eq("vocher_id", voucherId).count();
//5.2.判断是否存在
if(count>0){
//用户已经购买了
return Result.fail("用户已经购买过一次!");
}
//6.扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock=stock-1")
.eq("voucher", voucherId).gt("stock",0)
.update();
if(!success){
//扣减失败
return Result.fail("库存不足");
}
//7.创建订单
VoucherOrder voucherOrder = new VoucherOrder();
//7.1创建订单id
long orderId = redisIdWork.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 {
//释放锁
simpleRedisLock.unLock();
}
}
5.5.分布式锁误删锁问题
5.6.代码优化,uuid作为唯一标识
@Override
public boolean tryLock(long timeoutSec) {
//获取线程标识
String id =ID_PREFIX+ Thread.currentThread().getId();
//获取锁
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, id , timeoutSec, TimeUnit.MINUTES);
return Boolean.TRUE.equals(success);//防止success为null 自动拆箱错误
}
@Override
public void unLock() {
//获取线程标识
String id =ID_PREFIX+ Thread.currentThread().getId();
//获取锁中的标识
String lockId = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
//判断锁是否一致
if(id.equals(lockId)){
//释放锁
stringRedisTemplate.delete(KEY_PREFIX+name);
}
}
lua的语法网站:https://www.runoob.com/lua/lua-tutorial.html
5.6.新建lua脚本文件
--比较线程标识与锁中的标示是否一致
if(redis.call('get',KEY[1]) == ARGV[1]) then
--释放锁 del KEY
return redis.call('del',KEY[1])
end
return 0
引入并初始化脚本
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
//初始化脚本
static {
UNLOCK_SCRIPT=new DefaultRedisScript<>();
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);
}
调用脚本
@Override
public void unLock() {
//调用lua脚本 一行代码 脚本保证了原子性
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX+name),
ID_PREFIX+Thread.currentThread().getId()
);
}