分布式锁解决秒杀超卖问题

https://blog.51cto.com/u_16099300/10379143

分布式锁,redis相关学习资料:

 https://edu.51cto.com/video/4196.html

前言

本章节主要实现限时、限量优惠券秒杀功能,并利用分布式锁解决《超卖问题》、《一人一单问题》。

一.优惠券下单基本功能实现

1.功能介绍及流程图

redission分布式锁解决超卖问题 redis 分布式锁 秒杀_redis

redission分布式锁解决超卖问题 redis 分布式锁 秒杀_分布式_02

2.代码实现

登录后复制

@Resource
    private ISeckillVoucherService seckillVoucherService;

    @Resource
    private RedisIdworker redisIdworker;

    @Override
    @Transactional
    public Result seckillVoucher(Long voucherId) {
        // 1.查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        if (voucher==null){
            return Result.fail("优惠券不存在!");
        }
        // 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_id", voucherId).update();
        // 6.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        // 6.1 订单id
        long orderId = redisIdworker.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);
    }

一.超卖问题

1.问题分析

redission分布式锁解决超卖问题 redis 分布式锁 秒杀_redis_03

线程1,执行完,查询,查到库存为1,判断库存大于0,然后进行扣减库存操作,在这之间,线程2,线程3,也都进行了查询,查询到的库存也都是1,判断库存也都大于0,都进行了扣减库存操作,导致库存只有1个,卖出了3次。

2.解决方案(悲观锁、乐观锁)

悲观锁: 添加同步锁,让线程串行执行
    优点: 简单粗暴
    缺点: 性能一般
乐观锁: 不加锁,在更新时判断是否有其它线程在修改
    优点: 性能好
    缺点: 存在成功率低的问题

redission分布式锁解决超卖问题 redis 分布式锁 秒杀_redission分布式锁解决超卖问题_04

悲观锁是通过加锁的方式,让原本并发执行的变成串行执行,保证了线程安全,但是大大降低了执行效率,悲观锁实现较为简单,本文主要研究乐观锁的实现方式。

2.1 乐观锁实现方式-版本号法

redission分布式锁解决超卖问题 redis 分布式锁 秒杀_redis_05

版本号法: 数据库冗余一个版本字段,每次查询库存的时候,就将这个版本字段也查询出来,在更新的时候,版本号加一,条件加上版本号等于查询到的版本,如果版本被根据了,数据库的update语句就会执行失败。

如上图,线程1,已查询到的库存为1,版本号为1,同时线程2也查询到库存为1,版本号为1,线程1在更新的时候,将版本加1 ,version = version + 1,同时更新条件加上 and version =1,更新完成,version 值变成2,线程2更新操作的时候,同样会将版本加1,version = version + 1,更新条件加上and version =1,但是此时的version已经被线程1更新为2,导致线程2的更新操作会失败,保证了线程安全。

2.2 乐观锁实现方式-CSA法

redission分布式锁解决超卖问题 redis 分布式锁 秒杀_java_06

CSA法: 用需要修改的数据本身来判断数据是否已修改,利用库存本身的数据,来代替了版本,如上图线程1查到的库存是1,更新库存时,update语句加上and stock =1,执行成功,库存变成0,线程2,查到库存是1,同样更新库存时,update语句加上and stock =1,但此时数据已经被线程1改为0,导致线程2的更新操作会失败,保证了线程安全。

2.2.1 代码实现

只需要对更新数据库的语句进行修改

// 5.扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1") // set stock=stock -1
                .eq("voucher_id", voucherId).eq("stock",voucher.getStock())//where id=? and stock=?
                .update();

该方法解决了线程安全问题,但是带来了新的问题,失败率将会大大提升,如库存为100,100个线程并发执行,同时查到了库存为100,更新时,99个线程都会失败,只有一个会成功,按照正常的业务流程,100个库存,100个线程并发执行,应该都会成功,下面对扣减库存的逻辑进一步优化,解决失败率高的问题。
执行update语句时不用and stock =查到的值,只需要将条件改为 and stock >0,就解决了失败率高的问题。

// 5.扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1") // set stock=stock -1
                .eq("voucher_id", voucherId).gt("stock",voucher.getStock())//where id=? and stock > 0
                .update();

二.一人一单

需求:修改秒杀业务,要求同一个优惠券,一个用户只能下一单

1.单机应用下通过synchronized解决一人一单

引入依赖

<dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
        </dependency>

启动类开启暴露代理对象

@EnableAspectJAutoProxy(exposeProxy = true) //开启暴露代理对象
@MapperScan("com.hmdp.mapper")
@SpringBootApplication
public class HmDianPingApplication {
    public static void main(String[] args) {
        SpringApplication.run(HmDianPingApplication.class, args);
    }
}

具体实现

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

    @Resource
    private ISeckillVoucherService seckillVoucherService;

    @Resource
    private RedisIdworker redisIdworker;

    @Override
    public Result seckillVoucher(Long voucherId) {
        // 1.查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        if (voucher == null) {
            return Result.fail("优惠券不存在!");
        }
        // 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.1一人一单
        Long userId = UserHolder.getUser().getId();
        // 同一个用户加锁,不同用户加不同的锁,toString()底层每次都new了一个新的对象,
        // 会造成同一个用户加的是不同的锁
        // intern()方法是去常量池找跟字符串值一样的地址,避免同一个用户加了不同的锁
        synchronized (userId.toString().intern()) {
            // 事务要生效,需要spring对当前类做了动态代理,拿到代理对象,用代理对象做了事务处理,如果用this调用方法,就是用的是
            // 当前对象,造成实务无效,需要获取代理对象(事务)
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId, voucher, userId);
        }
    }

    @Transactional
    public Result createVoucherOrder(Long voucherId, SeckillVoucher voucher, Long userId) {
        // 5.2 查询订单
        int count = query().eq("user_id", userId).eq("voucher_id", voucher).count();
        // 5.3 判断订单是否存在
        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", voucher.getStock())//where id=? and stock > 0
                .update();
        // 7.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        // 7.1 订单id
        long orderId = redisIdworker.nextId("order");
        voucherOrder.setId(orderId);
        voucherOrder.setUserId(userId);
        // 7.3 设置代金券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);
        // 8.返回订单id
        return Result.ok(orderId);
    }
}
2.分布式系统下通过Redis分布式锁解决一人一单

分布式系统每个服务会部署很多个实例,每个实例对一个一个独立的JVM,在每个实例内部能通过synchronized实现线程的互斥,但是实例和实例直接就无法试下线程互斥,只能通过分布式锁来解决。

redission分布式锁解决超卖问题 redis 分布式锁 秒杀_redission分布式锁解决超卖问题_07

redission分布式锁解决超卖问题 redis 分布式锁 秒杀_版本号_08

2.1 代码实现

锁的接口类

public interface ILock {

    /**
     * 尝试获取锁
     * @param timeoutSec
     * @return
     */
    boolean tryLock(long timeoutSec);
    
    /**
     * 释放锁
     */
    void unlock();
}

实现类

package com.hmdp.utils;

import cn.hutool.core.lang.UUID;
import org.springframework.data.redis.core.StringRedisTemplate;

import java.util.concurrent.TimeUnit;

/**
 * @auther Kou
 * @date 2022/7/11 22:33
 */
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:";
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";

    @Override
    public boolean tryLock(long timeoutSec) {
        // 1.获取线程标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        // 2.获取锁
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
        // 3.success是Boolean包装类型,而方法的返回值是基本类型boolean,直接返回success会进行拆箱,
        // 如果success是null,就会报空指针
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {
        // 1.获取线程标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        // 2.获取锁中的标识
        String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
        // 3.判断线程标识和锁标识是否一样
        if (threadId.equals(id)) {
            stringRedisTemplate.delete(KEY_PREFIX + name);
        }
    }
}

业务代码改造

@Resource
    private StringRedisTemplate stringRedisTemplate;
    public Result seckillVoucher01(Long voucherId) {
        // 1.查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        if (voucher == null) {
            return Result.fail("优惠券不存在!");
        }
        // 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.1一人一单
        Long userId = UserHolder.getUser().getId();
        // 6.创建锁对象
        SimpleRedisLock lock = new SimpleRedisLock("order" + userId, stringRedisTemplate);
        // 7.获取锁
        boolean isLock = lock.tryLock(1200);
        // 8.判断是否获取锁成功
        if (!isLock) {
            return Result.fail("不允许重复下单");
        }
        try {
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId, voucher, userId);
        } finally {
            // 9.释放锁
            lock.unlock();
        }
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
图像识别技术在病虫害检测中的应用是一个快速发展的领域,它结合了计算机视觉和机器学习算法来自动识别和分类植物上的病虫害。以下是这一技术的一些关键步骤和组成部分: 1. **数据收集**:首先需要收集大量的植物图像数据,这些数据包括健康植物的图像以及受不同病虫害影响的植物图像。 2. **图像预处理**:对收集到的图像进行处理,以提高后续分析的准确性。这可能包括调整亮度、对比度、去噪、裁剪、缩放等。 3. **特征提取**:从图像中提取有助于识别病虫害的特征。这些特征可能包括颜色、纹理、形状、边缘等。 4. **模型训练**:使用机器学习算法(如支持向量机、随机森林、卷积神经网络等)来训练模型。训练过程中,算法会学习如何根据提取的特征来识别不同的病虫害。 5. **模型验证和测试**:在独立的测试集上验证模型的性能,以确保其准确性和泛化能力。 6. **部署和应用**:将训练好的模型部署到实际的病虫害检测系统中,可以是移动应用、网页服务或集成到智能农业设备中。 7. **实时监测**:在实际应用中,系统可以实时接收植物图像,并快速给出病虫害的检测结果。 8. **持续学习**:随着时间的推移,系统可以不断学习新的病虫害样本,以提高其识别能力。 9. **用户界面**:为了方便用户使用,通常会有一个用户友好的界面,显示检测结果,并提供进一步的指导或建议。 这项技术的优势在于它可以快速、准确地识别出病虫害,甚至在早期阶段就能发现问题,从而及时采取措施。此外,它还可以减少对化学农药的依赖,支持可持续农业发展。随着技术的不断进步,图像识别在病虫害检测中的应用将越来越广泛。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值