redis之:秒杀功能

一、超卖问题

乐观锁解决超卖问题

1、 版本号法

2、CAS法

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Resource
    private ISeckillVoucherService seckillVoucherService;
    @Override
    public Result seckillVoucher(Long voucherId) {
        //1.查询优惠券信息
        SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
        //2.判断秒杀是否开始
        if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
            return Result.fail("秒杀尚未开始");
        }
        //3.判断秒杀是否结束
        if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
            return Result.fail("秒杀已经结束");
        }
        //4.判断库存是否充足
        if (seckillVoucher.getStock() < 1) {
            return Result.fail("库存不足");
        }
        //5.扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1")//set stock = stock - 1
                .eq("voucher_id", voucherId).eq("stock",seckillVoucher.getStock())//where id = ? and stock = ?
                .update();
        if (!success) {
            return Result.fail("库存不足");
        }
        //6.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        //6.1订单id
        voucherOrder.setId(Long.valueOf(RandomUtil.randomNumbers(10)));
        //6.2用户id
        Long id = UserHolder.getUser().getId();
        voucherOrder.setUserId(id);
        //6.3代金券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);
        //7.返回订单id
        return Result.ok();
    }
}

没有超卖,为什么失败太多?

原因:库存充足时,只要改了stock就是取消。所以只要stock > 0就行

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Resource
    private ISeckillVoucherService seckillVoucherService;
    @Override
    public Result seckillVoucher(Long voucherId) {
        //1.查询优惠券信息
        SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
        //2.判断秒杀是否开始
        if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
            return Result.fail("秒杀尚未开始");
        }
        //3.判断秒杀是否结束
        if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
            return Result.fail("秒杀已经结束");
        }
        //4.判断库存是否充足
        if (seckillVoucher.getStock() < 1) {
            return Result.fail("库存不足");
        }
        //5.扣减库存
        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("库存不足");
        }
        //6.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        //6.1订单id
        voucherOrder.setId(Long.valueOf(RandomUtil.randomNumbers(10)));
        //6.2用户id
        Long id = UserHolder.getUser().getId();
        voucherOrder.setUserId(id);
        //6.3代金券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);
        //7.返回订单id
        return Result.ok();
    }
}

二、一人一单

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Resource
    private ISeckillVoucherService seckillVoucherService;
    @Override
    public Result seckillVoucher(Long voucherId) {
        //1.查询优惠券信息
        SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
        //2.判断秒杀是否开始
        if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
            return Result.fail("秒杀尚未开始");
        }
        //3.判断秒杀是否结束
        if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
            return Result.fail("秒杀已经结束");
        }
        //4.判断库存是否充足
        if (seckillVoucher.getStock() < 1) {
            return Result.fail("库存不足");
        }
        //5.一人一单
        Long userId = UserHolder.getUser().getId();
        //5.1判断是否存在订单
        Integer 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
        voucherOrder.setId(Long.valueOf(RandomUtil.randomNumbers(10)));
        //7.2用户id
        Long id = UserHolder.getUser().getId();
        voucherOrder.setUserId(id);
        //7.3代金券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);
        //8.返回订单id
        return Result.ok();
    }
}

 

 200个订单,为什么异常95%(为什么有10个订单)?

原因:多个线程穿插,并发情况(插入操作,不能用乐观锁),只能用悲观锁

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Resource
    private ISeckillVoucherService seckillVoucherService;
    @Override
    public Result seckillVoucher(Long voucherId) {
        //1.查询优惠券信息
        SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
        //2.判断秒杀是否开始
        if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
            return Result.fail("秒杀尚未开始");
        }
        //3.判断秒杀是否结束
        if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
            return Result.fail("秒杀已经结束");
        }
        //4.判断库存是否充足
        if (seckillVoucher.getStock() < 1) {
            return Result.fail("库存不足");
        }
        Long userId = UserHolder.getUser().getId();
        synchronized (userId.toString().intern()) {//锁代码块需要共用同一把锁,所以做字符串转成同一对象
            //获取代理对象
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        }
    }
    //在这个方法上加synchronized,它的范围是整个方法,锁的对象是this(当前类),意味着所有多线程对象都会加锁
    // ,且锁的对象都为同一个,会串行执行,前面的锁不释放,就会一直等待,影响性能
    @Transactional
    public Result createVoucherOrder(Long voucherId) {
        //5.一人一单
        Long userId = UserHolder.getUser().getId();
        //5.1判断是否存在订单
        Integer 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
        voucherOrder.setId(Long.valueOf(RandomUtil.randomNumbers(10)));
        //7.2用户id
        Long id = UserHolder.getUser().getId();
        voucherOrder.setUserId(id);
        //7.3代金券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);
        //8.返回订单id
        return Result.ok();
    }
}

总结:

1、synchronized锁不要加在createVoucherOrder方法上,虽然满足要求,但会影响性能。因为它的范围是整个方法,锁的对象是this(当前类),意味着所有多线程对象都会加锁,且锁的对象都为同一个,会串行执行,前面的锁不释放,就会一直等待,影响性能。

2、synchronized代码块不要写在createVoucherOrder方法内部,虽然满足要求,但也会造成并发问题。因为方法上加了@Transactional,在内部加锁会造成事务没有提交就有订单,也会造成并发问题。

3、写synchronized代码块时,括号里面参数要唯一。因为锁代码块需要共用同一把锁,所以做字符串转成同一对象。

4、要调用有@Transactional的方法时,必须要用代理对象调用。因为@Transactional底层是代理对象,而一般调用方法是this调用。

集群模式下的并发安全问题

原因:多个jvm没有使用同一把锁

 解决方法:分布式锁,Redis锁,Redssion

 

总结:

1、Redis分布式锁可以用命令SETNX lock thread1和命令expire lock 10,但要保证是一个事务,所以用命令SET lock thread1 NX EX 10。

2、Redis分布式锁要设置过期时间,防止服务器宕机后死锁,且锁是非阻塞式,保证死锁后线程不会一直等待释放锁。

 1、tryLock和unLock工具类方法

public class SimpleRedisLock {

    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;
    }

    public boolean tryLock(long timeoutSec){
        long value = Thread.currentThread().getId();
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(key_prefix + name, value + "", timeoutSec, TimeUnit.SECONDS);
        //自动拆箱,可能有安全问题,万一是null
        return Boolean.TRUE.equals(success);
    }

    public void  unlock(){
        stringRedisTemplate.delete(key_prefix + name);
    }
}

2、Redis分布式锁实现(部分逻辑)

        Long userId = UserHolder.getUser().getId();
        //创建锁对象
        SimpleRedisLock lock = new SimpleRedisLock(stringRedisTemplate, "order" + userId);
        //获取锁
        boolean isLock = lock.tryLock(1200);
        //判断是否获取锁成功
        if (!isLock) {
            return Result.fail("不允许重复下单");
        }
        //获取代理对象
        try {
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
            //释放锁
            lock.unlock();
        }

1、Redis分布式锁误删问题

产生原因:业务执行时间比锁时间长

线程一尝试获取锁,因为第一个来,所以获取锁成功 。因为某种原因业务产生阻塞,超过超时时间,线程一锁被释放。

线程二尝试获取锁成功,执行业务。线程一业务完成,释放锁(del),线程二锁被释放。

线程三尝试获取锁成功,执行业务。

结果:出现并发问题。

解决方法 :判断是否是自己的锁

public class SimpleRedisLock {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    private String name;
    private static final String key_prefix = "lock:";
    private static final String id_prefix = UUID.randomUUID().toString(true) + "-";


    public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.name = name;
    }

    public boolean tryLock(long timeoutSec){
        //获取线程标识
        String value = id_prefix + Thread.currentThread().getId();
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(key_prefix + name, value, timeoutSec, TimeUnit.SECONDS);
        //自动拆箱,可能有安全问题,万一是null
        return Boolean.TRUE.equals(success);
    }

    public void  unlock(){
        //获取线程标识
        String value = id_prefix + Thread.currentThread().getId();
        //获取锁中标识
        String id = stringRedisTemplate.opsForValue().get(key_prefix + name);
        //判断是否一致
        if (id.equals(value)) {
            //释放锁
            stringRedisTemplate.delete(key_prefix + name);
        }
    }
}

总结:在获取锁可以用UUID + 线程号表示,防止集群中出现相同标识。

 2、Redis锁的原子性问题

产生原因:当将要释放锁时产生阻塞。例如:jvm垃圾回收调用gc时会产生阻塞。

解决方法: Lua脚本

 

 

1、在Resource里编写Lua脚本 

--比较线程标识和锁中的标识是否一致
if (redis.call('get', KEYS[1]) == ARGV[1]) then
    --释放锁
    return redis.call('del',KEYS[1])
end
return 0

 2、调用Lua脚本

public class SimpleRedisLock {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    private String name;
    private static final String key_prefix = "lock:";
    private static final String id_prefix = UUID.randomUUID().toString(true) + "-";
    //读取Lua脚本
    private static final DefaultRedisScript<Long> unlock_script;
    static {
        unlock_script = new DefaultRedisScript<>();
        unlock_script.setLocation(new ClassPathResource("unlock.lua"));
        unlock_script.setResultType(Long.class);
    }


    public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.name = name;
    }

    public boolean tryLock(long timeoutSec){
        //获取线程标识
        String value = id_prefix + Thread.currentThread().getId();
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(key_prefix + name, value, timeoutSec, TimeUnit.SECONDS);
        //自动拆箱,可能有安全问题,万一是null
        return Boolean.TRUE.equals(success);
    }

    public void  unlock(){
        //调用Lua脚本
            stringRedisTemplate.execute(unlock_script,
            Collections.singletonList(key_prefix + name),
            id_prefix + Thread.currentThread().getId());
        }
    }

Redission锁(不用自己写setnx,底层已经封装)

 1、引入redission依赖

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>2.9.0</version>
</dependency>

2、配置redission 

@Configuration
public class RedissionConfig {

    public RedissonClient redissonClient(){
        //配置
        Config config = new Config();
        //useSingleServer()单节点模式
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");//.setPassword("")
        //创建RedissonClient对象
        return Redisson.create(config);
    }
}

3、代码

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private ISeckillVoucherService seckillVoucherService;
    @Resource
    private RedissonClient redissonClient;
    @Override
    public Result seckillVoucher(Long voucherId) {
        //1.查询优惠券信息
        SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
        //2.判断秒杀是否开始
        if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
            return Result.fail("秒杀尚未开始");
        }
        //3.判断秒杀是否结束
        if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
            return Result.fail("秒杀已经结束");
        }
        //4.判断库存是否充足
        if (seckillVoucher.getStock() < 1) {
            return Result.fail("库存不足");
        }
        Long userId = UserHolder.getUser().getId();
        //创建锁对象
//        SimpleRedisLock lock = new SimpleRedisLock(stringRedisTemplate, "order" + userId);
        RLock lock = redissonClient.getLock("order" + userId);
        //获取锁
//        boolean isLock = lock.tryLock(1200);
        boolean isLock = lock.tryLock();
        //判断是否获取锁成功
        if (!isLock) {
            return Result.fail("不允许重复下单");
        }
        //获取代理对象
        try {
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
            //释放锁
            lock.unlock();
        }

    }
    //在这个方法上加synchronized,它的范围是整个方法,锁的对象是this(当前类),意味着所有多线程对象都会加锁
    // ,且锁的对象都为同一个,会串行执行,前面的锁不释放,就会一直等待,影响性能
    @Transactional
    public Result createVoucherOrder(Long voucherId) {
        //5.一人一单
        Long userId = UserHolder.getUser().getId();
        //5.1判断是否存在订单
        Integer 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
        voucherOrder.setId(Long.valueOf(RandomUtil.randomNumbers(10)));
        //7.2用户id
        Long id = UserHolder.getUser().getId();
        voucherOrder.setUserId(id);
        //7.3代金券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);
        //8.返回订单id
        return Result.ok();
    }
}

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值