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发起大量请求,用来模拟多用户场景。
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张表抢,那么这样购买一次加锁就只需要加一张表的锁,不影响其他表的购买,大大提高了成功率。