黑马点评02优惠券秒杀(超卖问题)

1.数据库表格

2.如何生成全局唯一Id

2.1为什么id不自增

因为以上原因要用全局id生成器

2.2全局id生成器(利用redis的自增功能)

时间戳就是距离设定开始时间的秒数。

序列号就是一个redis string的key,主要由业务key + 当天时间 构成。存储的redis值是x年x月x日的订单数量,也就是序列号(自增)。

拼接形成订单号,也就是当前这单的订单号。

扫描二维码关注公众号,回复: 17299596 查看本文章

INCR 命令是一个针对字符串的操作。 因为 Redis 并没有专用的整数类型, 所以键 key 储存的值在执行 INCR 命令时会被解释为十进制 64 位有符号整数。 

package com.example.heima.utils;

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;

@Component
public class RedisIdWorker {
    //开始时间
    private static final long BEGIN_TIMESTAMP = 1640995200L;
    //序列号位数
    private static final int COUNT_BITS = 32;
    private StringRedisTemplate stringRedisTemplate;
    public RedisIdWorker(StringRedisTemplate stringRedisTemplate){
        this.stringRedisTemplate = stringRedisTemplate;
    }

    //id构成 : 1位符号位 + 31位时间戳 + 32位序列号
    public long nextId(String keyPrefix){
        //1.生成时间戳,当时时间
        LocalDateTime now = LocalDateTime.now();
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);//时间格式转换
        long timestamp = nowSecond - BEGIN_TIMESTAMP;//计算现在距离开始时间时间差

        //2.生成32位序列号
        //2.1生成订单时间当前日期,精确到天(因为32位最多2^32次方,序列号有上限,不管多久这个key都不变。
        // 所以为了让key随时间变化,加上天数拼接,但是一天很难到达2^32单)
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        //2.2自增长(传入的是string会被redis解释成数字类型)
        long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);

        //3,拼接并返回
        return timestamp << 32 | count ;

    }


}

2.2.1测试

CountDownLatch的使用和原理解析 - 知乎 (zhihu.com)

//线程池500个线程
    private ExecutorService es = Executors.newFixedThreadPool(500);
    @Test
    void testIdWorker(){
        //CountDownLatch可以使一个或多个线程等待其他线程各自执行完毕后再执行。
        CountDownLatch latch = new CountDownLatch(300);  //构造300的计数器
        //任务
        Runnable task = ()->{
            for(int i=0;i < 100;i++){
                long id =redisIdWorker.nextId("order");
                System.out.println("id = " + id);
            }
            latch.countDown(); //对计数器进行递减1操作,当计数器递减至0时,当前线程会去唤醒阻塞队列里的所有线程。
        };
        long begin = System.currentTimeMillis();
        //300个线程执行任务
        for(int i=0;i < 300;i ++){
            es.submit(task);
        }

        try {
            //等待latch计数器归零再开始执行该线程
            latch.await();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        long end = System.currentTimeMillis();

        System.out.println("time = " + (end-begin) );

    }

运行完redis中写入了一个名字为拼接的订单计数器 

生成成功

3.添加优惠券

基本就是前端传优惠券json数据,调用control和service进行数据库处理

3.1表结构

两个表通过券id联系

秒杀券

 所有券(包括普通券和秒杀券)

3.2添加优惠券

通过post传递json就可以得到信息

4.实现优惠券购买流程

对应优惠券购买service流程

5.解决多线程秒杀超卖问题

5.1模拟多用户请求软件

视频用了JMeter软件来测试负载,它可以设置线程数同时对某个url发起大量请求,用来模拟多用户场景。

JMeter性能测试,完整入门篇教程-CSDN博客

5.2超卖问题

在卖东西之前会做判断,判断成功之后才会调用扣减库存的函数,如果库存为1,两个线程同时成功判断有库存,然后分别扣减一个库存,那么库存就是-1出现超卖问题。

5.3解决方案(加锁,乐观锁悲观锁)

悲观锁是传统的锁,对效率影响大,所以研究如何实现乐观锁。

乐观锁因为要根据修改判断版本号,所以只能适用于更新情况。对于多线程查询情况只能用悲观锁。

5.4乐观锁

乐观锁因为要根据修改判断版本号,所以只能适用于更新情况。对于多线程查询情况只能用悲观锁。

5.4.1版本号法

5.4.2CAS法(用库存当作版本号Compare and switch)

5.5乐观锁实现---CAS法

在扣减库存之前,加上一个库存判断和之前是否一样

但是这样有新的问题:成功率太低,如果100个线程同时操作,只有1个线程能修改,其他都因为版本变更而退出,导致错误率提升。

所以在代码中不需要判断库存和不和之前一样,只要库存大于0就可以卖

5.5分段锁

因为乐观锁会提高失败率,所以为了提高成功率,可以采用分段锁的方式,把100个优惠券分散到10个表中,用户可以分别去10张表抢,那么这样购买一次加锁就只需要加一张表的锁,不影响其他表的购买,大大提高了成功率。

猜你喜欢

转载自blog.csdn.net/m0_50973548/article/details/134982825