黑马点评项目-抢购优惠券--单服务器版

全局唯一id

解释:为什么需要全局唯一Id呢?因为我们的下单的账单id 避免重复。
如果 这张券商品 id重复了,对应了多个用户,到时候你发货就会白白亏损。

全局唯一id的特点

  • 唯一性
  • 高可用性
  • 高性能
  • 递增性:确保会有递增性,这样放到数据库的时候生成索引的索引,能够更好的提高查询效率
  • 安全性:不能简单的让别人推测出你的id的规律。

这里采取的 全局唯一id生成的如下

即全局唯一ID(long类型) = 符号位(1位) + 时间戳(31位) + 自动生成的id(32位) 

代码编写

@Component
public class RedisIdWorker {
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    private static final  long BEGIN_TIMESTAMP = 1640995200L;// 2022年1月1日的时间戳

    public  Long nextId(String userPrefix){
        // 这个Id 如何生成?
        // 第一步确定什么?redis,会根据  key的不同生成 序列号,这个序列号 最大能够达到2的64次方。所以我们最好让这个key变化起来,而不是固定写死。
            //虽然很难达到2的64次方。
        // 所以获取全局唯一id的时候,要组合成一个id才行。 【自身功能(icr):调用者模块:日期】,这样设计的好处
            // 调用者模块,根据当前日期,生成了多少个id,这样就能够统计,每年或者每月,每天生成了多少个id,这样就能够统计每天出售了多少东西。
            // 记得把日期也调成 年:月:日
        //1.根据 redis的string类型的increment方法,生成自增的序列号。
        String date = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        Long increment = stringRedisTemplate.opsForValue().increment("inr:" + userPrefix + ":" + date);
        //2.生成 31位的时间戳
        // 2.1定义一个基准时间(BEGIN_TIMESTAMP ),那样就能够让这个方法被使用时间,减去基地时间,就能得到时间间隔,
        // 然后转成时间戳即可。这也是递增的。为什么要冲洗
        long now = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC);//当前时间的时间戳
        long timeSpace = now - BEGIN_TIMESTAMP;
        //3.生成全局唯一id
        return (timeSpace<< 32) | increment;
    }
}

优惠券秒杀下单

> 请求参数:优惠券id

> 请求路劲:http://localhost:8080/api/voucher-order/seckill/{id}

 > 请求方式:post请求

> 返回值:     订单id(返回给前端看,是否下单成功)

实现流程

下单时:需要注意点的东西。

> 一定要确保秒杀券,当前时间,在秒杀券的有效时间段内,否则全段将不会展示这张票据(这是前端的校验),

> 但是别人完全可以通过postman或者其他什么途径访问你的controller,所以必须要在接收到请求之后,一定要去判断这个优惠券是否在有效时间内。

> 最后别人完全可能访问不存在的优惠券Id,导致缓存穿透问题,所以要做好预防缓存穿透的准备,可以选择缓存空数据,或者使用布隆过滤器来完成

执行流程

> 这样的执行流程可能会出现超卖问题

代码实现

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
    @Resource
    private ISeckillVoucherService seckillVoucherService;
    @Resource
    private RedisIdWorker redisIdWorker;

    @Override
    @Transactional//最后是添加事物,设计到两张表的操作。这样一旦出现问题就可以回滚。
    public Result seckillVoucher(Long voucherId) {
        //1.查询 优惠券。 这个优惠券,差的不是普通的优惠券,而是秒杀券,所以要从秒杀券对应的库中找。
        // 空指针判断,否则别人绕过前端,通过postman来访问你这个接口
        SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
        //2.判断抢券活动是否开始
        if (seckillVoucher == null) {
            return Result.fail("优惠券不存在");//防止别人直接绕过前端,通过postman来访问你这个借口
        }

        //3.判断抢券活动是否结束
        LocalDateTime beginTime = seckillVoucher.getBeginTime();
        if (LocalDateTime.now().isBefore(beginTime)) { //当前时间在开始时间之前。
            return Result.fail("秒杀尚未开始");
        }
        LocalDateTime endTime = seckillVoucher.getEndTime();
        if (LocalDateTime.now().isAfter(endTime)) { //当前时间是在结束时间之后。
            return Result.fail("秒杀已经结束了");
        }

        //4.判断库存是否充足
        if (seckillVoucher.getStock() < 1) {
            return Result.fail("库存不足了");
        }

        //5.扣减库存
        boolean success = seckillVoucherService.update().setSql("stock = stock -1").eq("voucher_id", voucherId).update();

        if (!success) {
            //更新失败,一般来说就是库存不组
            return Result.fail("库存不足");
        }

        //6.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        // 订单id ,使用id生成器自动生成的
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        // 用户id,从登录拦截器中获取用户id
        UserDTO user = UserHolder.getUser();
        voucherOrder.setUserId(user.getId());
        // 代金券id
        voucherOrder.setVoucherId(voucherId);
        //将订单写入数据库
        save(voucherOrder);

        //7.返回订单id
        return Result.ok(orderId);

    }
}

超卖问题

 > 就是剩下最后一张飘的时候,更好有多少个线程来,线程A查询数据得到数据A,线程B查询数据库得到B,线程C查询得到C。 然后去判断 A B C 结果都是大于,然后对数据库执行扣减,就导致了超卖问题。

解决方案:锁

乐观锁和悲观锁:

悲观锁:让为线程安全问题一定会发生,因此在操作数据之前会先获取锁,确保线程的串行执行,就不会出现并发的问题。比如Synchronized 、Lock

悲观锁:认为线程安全问题不一定会发生,因此不加锁,但是在数据更新的时候) 判断有没有其他线程对数据进行了修改。例如:执行Update语句的时候,store 查询出来是1,当我update的时候,where 条件是 store = 查询出来的store才进行修改,如果 数据库的store跟查出来的store不一样,说明它被人修改过了,那么我就不修改了。

乐观锁

乐观锁的关键就是判断之前查询得到的数据是否被修改过。常见的判断方式是:

- 版本号法和 CAS方法

> 版本号法:给表多添加一个字段version,每执行一次修改,就让version字段+1;判断version有没有变化。


 

 > 版本号法:给表多添加一个字段version,每次执行修改操作,都让version++

 代码实现

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
    @Resource
    private ISeckillVoucherService seckillVoucherService;
    @Resource
    private RedisIdWorker redisIdWorker;

    @Override
    @Transactional//最后是添加事物,设计到两张表的操作。这样一旦出现问题就可以回滚。
    public Result seckillVoucher(Long voucherId) {
        //1.查询 优惠券。 这个优惠券,差的不是普通的优惠券,而是秒杀券,所以要从秒杀券对应的库中找。
        // 空指针判断,否则别人绕过前端,通过postman来访问你这个接口
        SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
        //2.判断抢券活动是否开始
        if (seckillVoucher == null) {
            return Result.fail("优惠券不存在");//防止别人直接绕过前端,通过postman来访问你这个借口
        }

        //3.判断抢券活动是否结束
        LocalDateTime beginTime = seckillVoucher.getBeginTime();
        if (LocalDateTime.now().isBefore(beginTime)) { //当前时间在开始时间之前。
            return Result.fail("秒杀尚未开始");
        }
        LocalDateTime endTime = seckillVoucher.getEndTime();
        if (LocalDateTime.now().isAfter(endTime)) { //当前时间是在结束时间之后。
            return Result.fail("秒杀已经结束了");
        }

        //4.判断库存是否充足
        if (seckillVoucher.getStock() < 1) {
            return Result.fail("库存不足了");
        }

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

        if (!success) {
            //更新失败,一般来说就是库存不组
            return Result.fail("库存不足");
        }

        //6.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        // 订单id ,使用id生成器自动生成的
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        // 用户id,从登录拦截器中获取用户id
        UserDTO user = UserHolder.getUser();
        voucherOrder.setUserId(user.getId());
        // 代金券id
        voucherOrder.setVoucherId(voucherId);
        //将订单写入数据库
        save(voucherOrder);

        //7.返回订单id
        return Result.ok(orderId);

    }
}

>  乐观锁并没有真正的加锁,只是多加了一个sql的判断条件。

乐观锁的弊端

> 如果我们修改数据的时候:只让stock = stock 的时候才能修改,那么成功率会十分低。

> 改进方案就是:双stock校验。

第一个使用查询出来的stock 校验,如果<=0 直接返回卖完了

第二个修改库存的时候,判断数据库中的stock > 0,如果是才能进行修改。

一人一单的问题

> 就是秒杀券,因为使用价值很高,所以一般来说只会让一个人买一张。所以有什么办法吗?

意思就是:让同一个用户只能操作数据库一次。

注意点

  1. 高并发的情况下,我完全可能一个用户,开启外挂,同时大量的去抢购这个票对吧。所以避免避免并发的情况,我们只能采取锁了,使用乐观锁可以吗?不行,因为我还没有下单,数据库中都没有我这条数据,没办法根据版本号法或者CAS方法,来进行判断是否更新数据库。所以只能采取 悲观锁。
  2. 锁住之后,执行业务。
    1. 一定要去先【订单表中】根据 当前用户id,和 优惠券id去数据库找,如果找到了说明数据库中有数据,就被让它再下单了。
    2. 如果订单表中没有找到,就生成订单数据,并存储到数据库中
    3. 并且将 【优惠表】对应的优惠券id那个库存自减,这就是超卖的那个逻辑嘛

执行流程

 

@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.查询 优惠券。 这个优惠券,差的不是普通的优惠券,而是秒杀券,所以要从秒杀券对应的库中找。
        // 空指针判断,否则别人绕过前端,通过postman来访问你这个接口
        SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
        //2.判断抢券活动是否开始
        if (seckillVoucher == null) {
            return Result.fail("优惠券不存在");//防止别人直接绕过前端,通过postman来访问你这个借口
        }

        //3.判断抢券活动是否结束
        LocalDateTime beginTime = seckillVoucher.getBeginTime();
        if (LocalDateTime.now().isBefore(beginTime)) { //当前时间在开始时间之前。
            return Result.fail("秒杀尚未开始");
        }
        LocalDateTime endTime = seckillVoucher.getEndTime();
        if (LocalDateTime.now().isAfter(endTime)) { //当前时间是在结束时间之后。
            return Result.fail("秒杀已经结束了");
        }

        //4.判断库存是否充足
        if (seckillVoucher.getStock() < 1) {
            return Result.fail("库存不足了");
        }
        Long userId = UserHolder.getUser().getId();
        synchronized (userId.toString().intern()) { // 这是最后倒数的几步:这样加锁,就能确保,方法执行完毕(提交事物之后),就释放锁。
            //必须去启动类,暴露代理对象,这样才能直接获取。
            // 为什么使用:IVoucherOrderService接收,,因为AOP是对IVoucherOrderService 这个实现类的方法进行功能完善的。使用到AOP的。
            // 所以它的代理对象一定是:IVoucherOrderService接口来的。
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();//这个时候就拿到代理对象了。
            return proxy.getResult(voucherId);//这样事物就能生效了
        }
        //还没有完,因为 getResult(voucherId) 这个方法,添加了事物。
        //而我们调用这个方法是用过this来完成了。this调用这个方法,就是普通方法,跟没加@Transactionanl一摸一样的效果。这是根据 AOP来实现的。
        // 如果使用this就没有AOP的功能了。所以必须使用代理对象来调用getResult()这个方法,才能有AOP的功能。
        //事物失效的解决方案:
        // 拿到事物的代理对象,然后通过代理对象,来执行这个getResult
        //最后必须得导入依赖。
        //启动类,去暴露道理对象


    }

    @Transactional//最后是添加事物,设计到两张表的操作。这样一旦出现问题就可以回滚。
    public Result getResult(Long voucherId) {
        Long userId = UserHolder.getUser().getId();

//        synchronized (userId.toString().intern()) { //但是userId.toString()底层是通过newString来执行了,所以每一个线程进来都会newString,仍然锁不住。
                                            //所以要调用intern()这个方法,只要你传过来的值一样,只要我电脑上还有这个值,那么就一定能取出来,并且不会new。
                                            //而不同的用户不会被锁定,那么性能就能提高。
            //方法内部加锁,仍然可能会出现一个问题。
                // 流程一般是这样了,执行了相关业务之后,释放锁,然后提交事务。
                // 就是这个事物有spring操控的,不一定说释放锁之后就马上提交事物。
                // 所以当我们释放锁的时候,此时有大量的请求进来,查询订单的话,我们之前事物还没有提交对吧,那说明还没有完成插入操作。 所以说再次查询的时候依然不存在,所以仍然可能会出现并发安全问题。
                // 所以我们要再事物提交之后,才能释放锁。
                // 所以不能锁在方法上,同时又不能锁在方法内。同时又要通过id来锁。 --->那么就只能在调用这个方法之前,获取id,然后通过锁id,锁的代码就是调用这个方法。
            //TODO 一人一单
            //查询订单
            int count = query()
                    .eq("user_id", userId)
                    .eq("voucher_id", voucherId)
                    .count();
            if (count > 0) {
                return Result.fail("用户已经购买过了");
            }
            //5.扣减库存
            boolean success = seckillVoucherService.update()
                    .setSql("stock = stock -1")// stock = stock -1;
                    .eq("voucher_id", voucherId) //where id = voucherId
                    .gt("stock", 0) // and stock = getStock ×:相等会导致成功率过低。 提高成功率的方案:只要stock > 0即可。
                    .update();
            if (!success) {
                //更新失败,一般来说就是库存不组
                return Result.fail("库存不足");
            }
            //6.创建订单
            VoucherOrder voucherOrder = new VoucherOrder();
            // 订单id ,使用id生成器自动生成的
            long orderId = redisIdWorker.nextId("order");
            voucherOrder.setId(orderId);
            // 用户id,从登录拦截器中获取用户id
            UserDTO user = UserHolder.getUser();
            voucherOrder.setUserId(user.getId());
            // 代金券id
            voucherOrder.setVoucherId(voucherId);
            //将订单写入数据库
            //7.返回订单id
            return Result.ok(orderId);
//        }
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 2019年黑马项目-畅购商城springcloud微服务实战是一门以实战为主的课程,旨在通过项目实践的方式,帮助学员深入理解和掌握SpringCloud微服务架构以及相关技术的应用。 课程的主要内容包括搭建基础的微服务架构、使用SpringCloud构建服务注册与发现、实现服务间的负载均衡、实现分布式配置中心、服务间的调用与容错处理、使用网关统一接入服务等。通过这些实战练习,学员不仅能够熟悉SpringCloud架构与组件,还能够了解微服务架构下的常见问题与解决方案。 畅购商城项目是一个典型的电商应用,通过实现该项目,学员可以接触到真实的业务场景与需求,并能够将所学知识应用到实际项目中。课程中通过模块化的方式逐步完善商城的功能,包括用户注册登录、商品浏览、购物车管理、订生成与支付等。通过这些实践,学员除了掌握SpringCloud微服务的开发技术,还能够了解和掌握电商项目开发流程和注意事项。 该课程的目标是让学员通过实战项目,全面了解和掌握SpringCloud微服务架构的设计与开发,在此基础上能够独立完成具有较高要求的微服务项目。通过参与实战项目的过程,学员还能够提升团队协作能力、解决问题的能力以及项目管理能力。 通过这门课程的学习,学员将会对SpringCloud微服务架构有更深入的理解,并能够将这些知识应用到实际项目中,提高自己在微服务开发领域的竞争力。 ### 回答2: 2019年黑马项目-畅购商城springcloud微服务实战是一个基于springcloud微服务架构的商城项目。该项目的目标是通过运用微服务的理念和技术,构建一个高可用、可扩展的商城系统。 在该项目中,使用了springcloud的多个组件,如Eureka注册中心、Feign负载均衡、Ribbon客户端负载均衡、Hystrix服务降级和容错、Zuul网关等。这些组件共同协作,实现了系统的弹性伸缩和高可用性。 畅购商城的功能包括商品展示、购物车、订管理、支付、用户管理等。通过将这些功能拆分成独立的微服务,使得系统更加灵活和可维护。同时,使用分布式事务和消息队列来保障数据的一致性和可靠性。 在项目开发过程中,采用了敏捷开发的方法,以迭代的方式进行开发和测试。通过使用Jenkins进行持续集成和部署,保证了代码的质量和系统的稳定性。 在项目的实战过程中,面临了许多挑战和困难,如微服务之间的通信、服务的负载均衡、服务的容错等。但通过团队的共同努力和不断的学习,最终成功地完成了该项目开发和部署。 在该项目的实施过程中,不仅学到了springcloud微服务架构的相关知识和技术,还体会到了团队合作和解决问题的能力。该项目的成功实施,不仅为公司带来了商业价值,也提升了团队的技术水平和项目管理能力。 ### 回答3: 2019年黑马项目-畅购商城springcloud微服务实战是一个以Spring Cloud为基础的微服务项目。微服务架构是一种将应用拆分成多个小型服务的架构模式,这些服务可以独立开发、部署、扩展和管理。 畅购商城项目使用了Spring Cloud的一系列子项目,如Eureka、Ribbon、Feign、Hystrix、Zuul等,来实现各个微服务之间的通信、负载均衡、服务降级与熔断等功能。 在项目中,我们会通过Eureka来实现服务的注册与发现,每个微服务都会向Eureka注册自己的地址,其他微服务可以通过Eureka来发现并调用这些服务。而Ribbon则负责实现客户端的负载均衡,可以轮询、随机、加权等方式分发请求。 Feign是一种声明式的HTTP客户端,它简化了服务间的调用方式。我们只需编写接口,并通过注解来描述需要调用的服务和方法,Feign会自动实现远程调用。 Hystrix是一个容错机制的实现,可以通过断路器来实现服务的降级与熔断,当某个服务出现故障或超时时,Hystrix会快速响应并返回一个可控制的结果,从而保证系统的稳定性。 另外,Zuul作为微服务网关,可以实现请求的统一入口和路由转发,提高系统的安全性和性能。 通过这些Spring Cloud的组件,畅购商城项目可以实现高可用、容错、自动扩展等优质的微服务架构。 总之,2019年黑马项目-畅购商城springcloud微服务实战是一个基于Spring Cloud的微服务项目,通过使用Spring Cloud的各个子项目,可以实现微服务之间的通信、负载均衡、服务降级与熔断等功能,为项目开发、部署和管理提供了便利。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值