目录
前言:
实现全局ID生成器,秒杀优惠券(基于乐观锁解决超卖问题),秒杀的一人一单(单机与集群线程安全问题)
1.全局ID生成器
1.1.思考:由于之前一直都在数据库中设置id为自增长字段(自增1),以订单id举例,那么会出现什么问题呢?
- 订单id每次新增一个订单就自增1,那么这样id规律性太明显了,用户可以直接根据它每次下的订单id来判断商家的营收情况(获取到了商家的数据)
- 如果订单量多,数据库一张表已经无法满足保存这么多数据了,我们需要分表来保存数据,但是由于我们设置的id自增(每张表都是从1开始自增),因此我们的订单id将会重复,在以后售后处理时,我们需要根据订单id来查询订单信息,而订单id有重复的,那么就不便于我们进行售后处理
1.2.订单id的特性:
- 订单量大
- id要唯一
1.3.全局ID生成器的要求:
- 唯一性:保证id唯一
- 高可用性:保证无论什么时候使用都可以生成正确的id
- 高性能性:保证生产的id的速度足够快
- 递增性:保证id的生产一定是整体逐渐递增的,有利于数据库创建索引增加插入速度
- 安全性:规律性不能太明显
1.4.实现方案:
- UUID:生成16进制最终转换成字符串(无序并且不自增)
- Redis自增::第1位是符号位,始终为0;接下来的31位是时间戳,记录了ID生成的时间;最后的32位是序列号,生成64位的二进制最终形成long类型数据
- snowflake(雪花算法):第1位是符号位,始终为0;接下来的41位是时间戳,记录了ID生成的时间;然后的10位是工作进程ID,用于区分不同的服务器或进程;最后的12位是序列号,用于在同一毫秒内生成不同的ID,生成64位的二进制最终形成long类型数据
- 数据库自增:单独使用一张表来存生成的id值,其他要使用id的表就来查询即可
1.5.具体实现(Redis自增方案):
为什么可以实现:
- 唯一:由于Redis是独立于数据库之外的(不管有几张表或者是有几个数据库),我们的Redis始终是只有一个(唯一),因此它的自增的id就永远唯一
- 高可用:利用集群,哨兵,主从方案
- 高性能:Redis基于内存,数据库基于硬盘,因此性能更好
- 递增:Redis自带命令可以实现自增
- 安全性:不会直接使用Redis的自增数值(依旧是规律性太明显),采用拼接信息实现
怎么实现:我们采用拼接信息实现,而为了增加性能,我们采用数值类型(long类型),它占用空间小,对建立索引方便
实现步骤:拼接信息,第1位是符号位,始终为0(0位正,1为负);接下来的31位是时间戳(秒数),记录了ID生成的时间;最后的32位是序列号(Redis自增数),生成64位的二进制最终形成long类型数据
解释:
时间戳(秒数):利用当前时间减去你自己设置的开始时间最后得到的时间秒数
------------
思考:那为什么不直接使用当前时间的秒数呢
解释:还是由于使用当前时间秒数容易被猜到规律,规律性明显
序列号:Redis自增数
------------
实现:Redis自增数使用String类型中的命令increment(每次自增1),并且由于该命令是如果Redis中没有key就会帮你自动创建key然后自增(此时值为1),存在key那么就直接将key中的value自增1,最终返回value值
------------
细节:由于使用的Redis的命令那么最终序列号作为value将存入Redis,那么存入Redis的自增数不就是我们的订单数吗?那以后我们需要统计订单数是不是直接查询Redis就行,而为了方便查询,我们的key是不是需要设置一个有意义的(通过key)
-------------
key的设置:自己设置前缀(以后生成id的不只是订单id,因此我们需要自己指定对应前缀来区分),然后用前缀拼接时间(具体到天),最终形成一个key
------------
思考:加前缀我能理解,为了区分存入Redis的key,那为什么还要拼接时间呢?
解释:如果你的序列号都使用同一个key,Redis存入是由上限的,而且为了你以后方便查询,key拼接时间(具体到天),那么我们可以统计每一天的下单量
实现细节:
思考:我们最终得到了时间戳(秒)long类型,序列号(订单数)long类型,我们需要拼接形成一个全新的long,符号位不需要管(正数0,负数1)
步骤:将时间戳向左移32位(留给序列号的),由于向左移位时以0来填充,那么再将移位后的时间戳异或上序列号即可(只有有一个为真那就是真,有1就是1),第一位符号位不需要管,时间戳是正数(id一般也会设置为正数),最终形成一个新的long类型的id
解释:我们这里是进行的二进制计算,而二进制只有0/1,那么有值就为1,没有值就为0了(异或)
-
import org.springframework.beans.factory.annotation.Autowired;
-
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 {
-
-
@Autowired
-
private
final StringRedisTemplate stringRedisTemplate;
-
-
public
RedisIdWorker
(StringRedisTemplate stringRedisTemplate) {
-
this.stringRedisTemplate = stringRedisTemplate;
-
}
-
-
//定义开始时间戳
-
private
static
final
Long
BEGIN_TIME_SECOND
=
1740960000L;
-
//移动位数
-
private
static
final
Long
COUNT_BIT
=
32L;
-
-
public Long
setId
(String keyPrefix){
-
//1.设置时间戳
-
//当前时间戳
-
LocalDateTime
now
= LocalDateTime.now();
-
long
second
= now.toEpochSecond(ZoneOffset.UTC);
-
//最终时间戳
-
Long
time
= second - BEGIN_TIME_SECOND;
-
-
//2.获取序列号
-
String
data
= now.format(DateTimeFormatter.ofPattern(
"yyyy:MM:dd"));
-
//Redis返回的序列号
-
long
increment
= stringRedisTemplate.opsForValue().increment(
"icr" + keyPrefix + data);
-
-
//拼接
-
return time << COUNT_BIT | increment;
-
}
-
}
AI写代码
Redis图效果:
2.秒杀优惠券
2.1.秒杀优惠券的基本实现
思考:在下单优惠券之前,我们需要判断两点
- 秒杀是否开始或结束
- 库存是否充足
步骤:
前端提交优惠券id
==》后端接收id
==》根据优惠券id查询数据库,得到优惠券信息
==》判断秒杀是否开始或结束
==》秒杀没有开始或已经结束
==》返回错误信息
-------
==》秒杀正在进行
==》判断库存是否充足
==》不足
==》返回错误信息
-------
==》充足
==》扣减库存
==》创建订单
==》返回订单id
-
@Service
-
public
class
VoucherOrderServiceImpl
extends
ServiceImpl<VoucherOrderMapper, VoucherOrder>
implements
IVoucherOrderService {
-
-
@Autowired
-
private ISeckillVoucherService iSeckillVoucherService;
-
@Autowired
-
private RedisIdWorker redisIdWorker;
-
@Override
-
@Transactional
-
public Result
seckillVoucher
(Long voucherId) {
-
//1.根据id查询数据库优惠券信息
-
SeckillVoucher
voucher
= iSeckillVoucherService.getById(voucherId);
-
-
//2.获取时间
-
LocalDateTime
beginTime
= voucher.getBeginTime();
-
LocalDateTime
endTime
= voucher.getEndTime();
-
-
//3.判断时间
-
if (beginTime.isAfter(LocalDateTime.now())) {
-
return Result.fail(
"秒杀还未开始");
-
}
-
if (endTime.isBefore(LocalDateTime.now())) {
-
return Result.fail(
"秒杀已经结束");
-
}
-
-
//4.获取库存
-
Integer
stock
= voucher.getStock();
-
//库存不足
-
if(stock <
1){
-
return Result.fail(
"库存不足");
-
}
-
-
//库存足
-
//5.库存减1
-
boolean
success
= iSeckillVoucherService.update()
-
.setSql(
"stock = stock -1")
-
.eq(
"voucher_id", voucherId)
-
.update();
-
if (!success){
-
return Result.fail(
"库存不足");
-
}
-
//6.创建订单
-
VoucherOrder
voucherOrder
=
new
VoucherOrder();
-
//优惠券id
-
voucherOrder.setVoucherId(voucherId);
-
//订单id
-
Long
orderId
= redisIdWorker.setId(
"order");
-
voucherOrder.setId(orderId);
-
//用户id
-
Long
userId
= UserHolder.getUser().getId();
-
voucherOrder.setUserId(userId);
-
//存入数据库
-
save(voucherOrder);
-
return Result.ok(orderId);
-
}
-
-
}
AI写代码
-
-
@Component
-
public
class
RedisIdWorker {
-
-
@Autowired
-
private
final StringRedisTemplate stringRedisTemplate;
-
-
public
RedisIdWorker
(StringRedisTemplate stringRedisTemplate) {
-
this.stringRedisTemplate = stringRedisTemplate;
-
}
-
-
//定义开始时间戳
-
private
static
final
Long
BEGIN_TIME_SECOND
=
1740960000L;
-
//移动位数
-
private
static
final
Long
COUNT_BIT
=
32L;
-
-
public Long
setId
(String keyPrefix){
-
//1.设置时间戳
-
//当前时间戳
-
LocalDateTime
now
= LocalDateTime.now();
-
long
second
= now.toEpochSecond(ZoneOffset.UTC);
-
//最终时间戳
-
Long
time
= second - BEGIN_TIME_SECOND;
-
-
//2.获取序列号
-
String
data
= now.format(DateTimeFormatter.ofPattern(
"yyyy:MM:dd"));
-
//Redis返回的序列号
-
long
increment
= stringRedisTemplate.opsForValue().increment(
"icr" + keyPrefix + data);
-
-
//拼接
-
return time << COUNT_BIT | increment;
-
}
-
}
AI写代码
解释:我们唯一要注意的点就是秒杀的时间和库存的数量(判断)
2.2.超卖问题
解释:
- 前提:库存此时为1
例子:线程1先执行查询库存,线程2再执行查询,线程1扣减库存,线程2扣减库存
==》线程1先执行
==》线程1查询库存(1)
==》线程2抢到执行权
==》线程2查询库存(1)
==》线程1再次抢到执行权
==》由于库存大于0
==》线程1执行库存扣减操作
==》此时库存(0)
==》线程2执行
==》由于之前查询库存结果为1
==》线程2也执行库存扣减操作
==》此时库存(-1)
----------
那么此时优惠券库存为-1,已经形成了超卖问题
2.3.解决超卖问题的方案
解决方案:
方案一:悲观锁
悲观锁:认为线程安全问题一定会发生,因此在每次操作数据之前先获取锁,以此确保线程安全,保证线程串行执行
- Synchronized,Lock都属于悲观锁
- 优点:简单粗暴
- 缺点:性能一般
方案二:乐观锁
乐观锁:认为线程安全问题不一定发生,因此不加锁,只是在更新数据时去判断有没有其他线程来对数据进行了修改
- 如果没有修改则认为是安全的,自己才更新数据
- 如果已经被其他线程修改说明发生了安全问题,此时可以重试或返回异常
- 优点:性能好
- 缺点:存在安全率低的问题
解释:悲观锁就是直接加锁,由于是加锁其他线程都需要等待因此性能低,乐观锁是不加锁,由于不加锁那么就会出现安全问题(概率低)
思考:
- 由于我们是优惠券库存问题(有数据给我们判断,这个数据到底有没有修改过),我们可以直接根据库存来判断是否出现数据不一致问题,那么就可以采用乐观锁
- 如果不是库存呢,那么只能通过数据的整体变化来判断,此时采用乐观锁是复杂的,你需要判断的数据太多了,那么就采用悲观锁
- 但是悲观锁的性能一般,怎么提高性能呢:采用分批加锁(分段锁),将数据分成几份(假设分成10张表),那么用户是不是同时去这10张表抢,同时10个人抢(效率提高),最终思想:每次锁定的资源少
总结:如果要更新数据那么可以使用乐观锁,添加数据使用悲观锁
2.4.基于乐观锁来解决超卖问题
版本号法:设置版本号,每次查询库存时也查询版本号,最后扣减库存时增加判断条件(就是此时的版本号应该等于我先前查询到的版本号),如果不等于事务回滚
思想:更新数据前比较版本号是否发生改变
步骤:
前端提交优惠券id
==》后端接收id
==》根据优惠券id查询数据库,得到优惠券信息,获取版本号
==》判断秒杀是否开始或结束
==》秒杀没有开始或已经结束
==》返回错误信息
-------
==》秒杀正在进行
==》判断库存是否充足
==》不足
==》返回错误信息
-------
==》充足
==》判断版本号是否发生改变
==》改变返回错误信息
-------
==》版本号相同
==》扣减库存
==》创建订单
==》返回订单id
CAS法:直接比较库存,在更新数据时增加判断条件(库存是否发生改变),库存改变不执行更新操作事务回滚
思想:直接利用已有数据来进行判断,根据数据是否发生变化来确定是否更新数据
步骤:
前端提交优惠券id
==》后端接收id
==》根据优惠券id查询数据库,得到优惠券信息
==》判断秒杀是否开始或结束
==》秒杀没有开始或已经结束
==》返回错误信息
-------
==》秒杀正在进行
==》判断库存是否充足
==》不足
==》返回错误信息
-------
==》充足
==》判断库存是否发生变化
==》改变返回错误信息
-------
==》库存相同
==》扣减库存
==》创建订单
==》返回订单id
思考:由于我们是优惠券库存问题,那么我们可以直接使用库存来直接判断,只有库存发生变化,那我们就不进行更新操作
代码实现:
-
@Service
-
public
class
VoucherOrderServiceImpl
extends
ServiceImpl<VoucherOrderMapper, VoucherOrder>
implements
IVoucherOrderService {
-
-
@Autowired
-
private ISeckillVoucherService iSeckillVoucherService;
-
@Autowired
-
private RedisIdWorker redisIdWorker;
-
@Override
-
@Transactional
-
public Result
seckillVoucher
(Long voucherId) {
-
//1.根据id查询数据库优惠券信息
-
SeckillVoucher
voucher
= iSeckillVoucherService.getById(voucherId);
-
-
//2.获取时间
-
LocalDateTime
beginTime
= voucher.getBeginTime();
-
LocalDateTime
endTime
= voucher.getEndTime();
-
-
//3.判断时间
-
if (beginTime.isAfter(LocalDateTime.now())) {
-
return Result.fail(
"秒杀还未开始");
-
}
-
if (endTime.isBefore(LocalDateTime.now())) {
-
return Result.fail(
"秒杀已经结束");
-
}
-
-
//4.获取库存
-
Integer
stock
= voucher.getStock();
-
//库存不足
-
if(stock <
1){
-
return Result.fail(
"库存不足");
-
}
-
-
//库存足
-
//5.库存减1
-
boolean
success
= iSeckillVoucherService.update()
-
.setSql(
"stock = stock -1")
-
.eq(
"voucher_id", voucherId).eq(
"stock",stock)
//乐观锁
-
.update();
-
if (!success){
-
return Result.fail(
"库存不足");
-
}
-
//6.创建订单
-
VoucherOrder
voucherOrder
=
new
VoucherOrder();
-
//优惠券id
-
voucherOrder.setVoucherId(voucherId);
-
//订单id
-
Long
orderId
= redisIdWorker.setId(
"order");
-
voucherOrder.setId(orderId);
-
//用户id
-
Long
userId
= UserHolder.getUser().getId();
-
voucherOrder.setUserId(userId);
-
//存入数据库
-
save(voucherOrder);
-
return Result.ok(orderId);
-
}
-
-
}
AI写代码
-
-
@Component
-
public
class
RedisIdWorker {
-
-
@Autowired
-
private
final StringRedisTemplate stringRedisTemplate;
-
-
public
RedisIdWorker
(StringRedisTemplate stringRedisTemplate) {
-
this.stringRedisTemplate = stringRedisTemplate;
-
}
-
-
//定义开始时间戳
-
private
static
final
Long
BEGIN_TIME_SECOND
=
1740960000L;
-
//移动位数
-
private
static
final
Long
COUNT_BIT
=
32L;
-
-
public Long
setId
(String keyPrefix){
-
//1.设置时间戳
-
//当前时间戳
-
LocalDateTime
now
= LocalDateTime.now();
-
long
second
= now.toEpochSecond(ZoneOffset.UTC);
-
//最终时间戳
-
Long
time
= second - BEGIN_TIME_SECOND;
-
-
//2.获取序列号
-
String
data
= now.format(DateTimeFormatter.ofPattern(
"yyyy:MM:dd"));
-
//Redis返回的序列号
-
long
increment
= stringRedisTemplate.opsForValue().increment(
"icr" + keyPrefix + data);
-
-
//拼接
-
return time << COUNT_BIT | increment;
-
}
-
}
AI写代码
弊端:如果同时大量用户抢优惠券,而此时库存还有100张,用户们都先进行了查询库存的操作,但都没有进行库存扣减操作,等到第一个先抢到优惠券后,库存改变,那么其他用户全部会抢券失败
前提:优惠券库存100
例子:100个线程都先进行了查询库存操作,都还没有执行到判断库存是否发生改变
==》线程1-100查询库存(100)
==》线程1优先于其他线程先执行完判断库存操作(100)
==》线程1扣减库存(99)
==》不管之后是哪个线程来执行判断库存操作
==》库存已经发生变化,抢券失败
----------
那么此时100个用户抢券,只抢券成功一人,但是我的优惠券库存却还有99张,失败率极高
怎么提高用户抢券的成功率呢
思考:由于库存不能是负数,那么我们最后判断的条件不再是库存是否改变,而是库存大于0就行,只要有库存那么我就卖给用户,即使出现大量用户同时进行抢券的情况,我们也可以将券买给用户(而不是只能卖给第一个用户),并且当库存只有一张时,由于我们是更新操作,数据库只允许一个线程来执行更新操作,不允许多个线程同时执行更新库存操作(最后一张券被大量用户抢时,总会有一个用户抢到,其他用户则抢不到)
步骤:
前端提交优惠券id
==》后端接收id
==》根据优惠券id查询数据库,得到优惠券信息
==》判断秒杀是否开始或结束
==》秒杀没有开始或已经结束
==》返回错误信息
-------
==》秒杀正在进行
==》判断库存是否充足
==》不足
==》返回错误信息
-------
==》充足
==》再次判断库存是否大于0
==》库存不足返回错误信息
-------
==》库存足
==》扣减库存
==》创建订单
==》返回订单id
代码实现:
-
@Service
-
public
class
VoucherOrderServiceImpl
extends
ServiceImpl<VoucherOrderMapper, VoucherOrder>
implements
IVoucherOrderService {
-
-
@Autowired
-
private ISeckillVoucherService iSeckillVoucherService;
-
@Autowired
-
private RedisIdWorker redisIdWorker;
-
@Override
-
@Transactional
-
public Result
seckillVoucher
(Long voucherId) {
-
//1.根据id查询数据库优惠券信息
-
SeckillVoucher
voucher
= iSeckillVoucherService.getById(voucherId);
-
-
//2.获取时间
-
LocalDateTime
beginTime
= voucher.getBeginTime();
-
LocalDateTime
endTime
= voucher.getEndTime();
-
-
//3.判断时间
-
if (beginTime.isAfter(LocalDateTime.now())) {
-
return Result.fail(
"秒杀还未开始");
-
}
-
if (endTime.isBefore(LocalDateTime.now())) {
-
return Result.fail(
"秒杀已经结束");
-
}
-
-
//4.获取库存
-
Integer
stock
= voucher.getStock();
-
//库存不足
-
if(stock <
1){
-
return Result.fail(
"库存不足");
-
}
-
-
//库存足
-
//5.库存减1
-
boolean
success
= iSeckillVoucherService.update()
-
.setSql(
"stock = stock -1")
-
.eq(
"voucher_id", voucherId)..gt(
"stock",
0)
//乐观锁
-
.update();
-
if (!success){
-
return Result.fail(
"库存不足");
-
}
-
//6.创建订单
-
VoucherOrder
voucherOrder
=
new
VoucherOrder();
-
//优惠券id
-
voucherOrder.setVoucherId(voucherId);
-
//订单id
-
Long
orderId
= redisIdWorker.setId(
"order");
-
voucherOrder.setId(orderId);
-
//用户id
-
Long
userId
= UserHolder.getUser().getId();
-
voucherOrder.setUserId(userId);
-
//存入数据库
-
save(voucherOrder);
-
return Result.ok(orderId);
-
}
-
-
}
AI写代码
-
-
@Component
-
public
class
RedisIdWorker {
-
-
@Autowired
-
private
final StringRedisTemplate stringRedisTemplate;
-
-
public
RedisIdWorker
(StringRedisTemplate stringRedisTemplate) {
-
this.stringRedisTemplate = stringRedisTemplate;
-
}
-
-
//定义开始时间戳
-
private
static
final
Long
BEGIN_TIME_SECOND
=
1740960000L;
-
//移动位数
-
private
static
final
Long
COUNT_BIT
=
32L;
-
-
public Long
setId
(String keyPrefix){
-
//1.设置时间戳
-
//当前时间戳
-
LocalDateTime
now
= LocalDateTime.now();
-
long
second
= now.toEpochSecond(ZoneOffset.UTC);
-
//最终时间戳
-
Long
time
= second - BEGIN_TIME_SECOND;
-
-
//2.获取序列号
-
String
data
= now.format(DateTimeFormatter.ofPattern(
"yyyy:MM:dd"));
-
//Redis返回的序列号
-
long
increment
= stringRedisTemplate.opsForValue().increment(
"icr" + keyPrefix + data);
-
-
//拼接
-
return time << COUNT_BIT | increment;
-
}
-
}
AI写代码
3.秒杀一人一单
3.1.秒杀一人一单的基本实现
思考:由于是秒杀问题,因此不能让用户一个人全部买走(这不就是黄牛吗),那么我们可以实现一个用户只能下一单
步骤:
前端提交优惠券id
==》后端接收id
==》根据优惠券id查询数据库,得到优惠券信息
==》判断秒杀是否开始或结束
==》秒杀没有开始或已经结束
==》返回错误信息
-------
==》秒杀正在进行
==》判断库存是否充足
==》不足
==》返回错误信息
-------
==》充足
==》根据优惠券id和用户id来查询数据库,返回查询数量
==》判断数量是否大于0
==》大于0,即用户已经下过一单(每张优惠券id不同)
==》返回错误信息
-------
==》数量小于0,即用户没有下单
==》再次判断库存是否大于0
==》库存不足返回错误信息
-------
==》库存足
==》扣减库存
==》创建订单
==》返回订单id
代码实现:
-
@Service
-
public
class
VoucherOrderServiceImpl
extends
ServiceImpl<VoucherOrderMapper, VoucherOrder>
implements
IVoucherOrderService {
-
-
@Autowired
-
private ISeckillVoucherService iSeckillVoucherService;
-
@Autowired
-
private RedisIdWorker redisIdWorker;
-
@Override
-
@Transactional
-
public Result
seckillVoucher
(Long voucherId) {
-
//1.根据id查询数据库优惠券信息
-
SeckillVoucher
voucher
= iSeckillVoucherService.getById(voucherId);
-
-
//2.获取时间
-
LocalDateTime
beginTime
= voucher.getBeginTime();
-
LocalDateTime
endTime
= voucher.getEndTime();
-
-
//3.判断时间
-
if (beginTime.isAfter(LocalDateTime.now())) {
-
return Result.fail(
"秒杀还未开始");
-
}
-
if (endTime.isBefore(LocalDateTime.now())) {
-
return Result.fail(
"秒杀已经结束");
-
}
-
-
//4.获取库存
-
Integer
stock
= voucher.getStock();
-
//库存不足
-
if(stock <
1){
-
return Result.fail(
"库存不足");
-
}
-
//根据用户id和优惠券id查询数据库
-
Long
userId
= UserHolder.getUser().getId();
-
int
count
= query().eq(
"user_id", userId).eq(
"voucher_id", voucherId).count();
-
if(count >
0){
-
//该用户已经下过单了
-
return Result.fail(
"一个用户只能下一单");
-
}
-
-
//库存足
-
//5.库存减1
-
boolean
success
= iSeckillVoucherService.update()
-
.setSql(
"stock = stock -1")
-
.eq(
"voucher_id", voucherId)..gt(
"stock",
0)
//乐观锁
-
.update();
-
if (!success){
-
return Result.fail(
"库存不足");
-
}
-
//6.创建订单
-
VoucherOrder
voucherOrder
=
new
VoucherOrder();
-
//优惠券id
-
voucherOrder.setVoucherId(voucherId);
-
//订单id
-
Long
orderId
= redisIdWorker.setId(
"order");
-
voucherOrder.setId(orderId);
-
//用户id
-
voucherOrder.setUserId(userId);
-
//存入数据库
-
save(voucherOrder);
-
return Result.ok(orderId);
-
}
-
-
}
AI写代码
-
-
@Component
-
public
class
RedisIdWorker {
-
-
@Autowired
-
private
final StringRedisTemplate stringRedisTemplate;
-
-
public
RedisIdWorker
(StringRedisTemplate stringRedisTemplate) {
-
this.stringRedisTemplate = stringRedisTemplate;
-
}
-
-
//定义开始时间戳
-
private
static
final
Long
BEGIN_TIME_SECOND
=
1740960000L;
-
//移动位数
-
private
static
final
Long
COUNT_BIT
=
32L;
-
-
public Long
setId
(String keyPrefix){
-
//1.设置时间戳
-
//当前时间戳
-
LocalDateTime
now
= LocalDateTime.now();
-
long
second
= now.toEpochSecond(ZoneOffset.UTC);
-
//最终时间戳
-
Long
time
= second - BEGIN_TIME_SECOND;
-
-
//2.获取序列号
-
String
data
= now.format(DateTimeFormatter.ofPattern(
"yyyy:MM:dd"));
-
//Redis返回的序列号
-
long
increment
= stringRedisTemplate.opsForValue().increment(
"icr" + keyPrefix + data);
-
-
//拼接
-
return time << COUNT_BIT | increment;
-
}
-
}
AI写代码
3.2.单机模式下的线程安全问题
解释:
前提:库存充足,并且是同一个用户下单,此时该用户还没有下单(订单数量0)
--------
例子:一个用户同时发出俩个请求(买相同的优惠券),线程1先查询,线程2后查询,线程1判断用户是否下过单,线程2判断用户是否下过单
==》线程1先执行
==》线程1查询订单数量(0)
==》线程1判断订单数量
==》订单数量为0,可以下单
==》线程2抢到执行权
==》线程2执行查询订单数量(0)
==》订单数量为0,也可以下单
==》线程1抢到执行权
==》由于订单数量为0,线程1执行下单操作
==》线程2执行
==》由于订单数量为0,线程2执行下单操作
---------
那么最终一个用户下了两单,出现了并发安全问题
思考:这是不是还是超卖问题,那么还是使用锁来解决,而我们现在是执行创建订单的操作,乐观锁是需要根据数据的变化来实现的,因此不能使用乐观锁(修改用乐观,添加用悲观)
思路:既然使用悲观锁,那么我们需要考虑在哪里加锁合适,是整个方法都加上锁吗?不是吧,我们最终问题出现在哪,是并发查询订单数量那里,而之前的查询库存操作(等等)是不需要加锁的(加锁是会导致我们的性能降低,因此我们需要考虑加锁的合适位置),既然是对于方法内部部分代码进行加锁,那么我们可以将要加锁的代码抽离出来,对于这个新方法进行加锁,而我们这里使用synchronized
-
@Transactional
-
public
synchronized Result
creatOrder
(Long voucherId) {
-
//根据用户id和优惠券id查询数据库
-
Long
userId
= UserHolder.getUser().getId();
-
int
count
= query().eq(
"user_id", userId).eq(
"voucher_id", voucherId).count();
-
if(count >
0){
-
//该用户已经下过单了
-
return Result.fail(
"一个用户只能下一单");
-
}
-
//库存足
-
//5.库存减1
-
boolean
success
= iSeckillVoucherService.update()
-
.setSql(
"stock = stock -1")
-
.eq(
"voucher_id", voucherId).gt(
"stock",
0)
//乐观锁
-
.update();
-
if (!success){
-
return Result.fail(
"库存不足");
-
}
-
//6.创建订单
-
VoucherOrder
voucherOrder
=
new
VoucherOrder();
-
//优惠券id
-
voucherOrder.setVoucherId(voucherId);
-
//订单id
-
Long
orderId
= redisIdWorker.setId(
"order");
-
voucherOrder.setId(orderId);
-
//用户id
-
// Long userId = UserHolder.getUser().getId();
-
voucherOrder.setUserId(userId);
-
//存入数据库
-
save(voucherOrder);
-
return Result.ok(orderId);
-
}
AI写代码
细节:我们是直接将锁synchronized加在整个新方法上吗?(返回值类型之前),不是吧,这样我们锁住的是整个方法(synchronized锁对象是该类的实例),那么不同用户都使用同一把锁(串行执行,效率极低),注意:需要加上事务注解
思考:为了将效率提高,那么我们需要将锁的范围缩小,一个用户一把锁(不同的用户不同的锁),不建议将synchronized直接加在方法上
实现:那么我们可以将方法内的代码抽离出来形成代码块,然后对代码块加锁synchronized,而为了保证一个用户一把锁,那么我们对于synchronized的定义该怎么办
一个用户一把锁的问题:我们之前不是取出来了用户的id吗,直接用id来定义synchronized,不对,如果直接用用户id这个变量来定义锁,那么相同用户发出多次请求,请求的锁不同(每次用户id的创建地址不同),那我们直接用用户id里面的id值就行(id.toString()),同样不对,toString()方法的底层依旧是new一个新的String类型,那么还是地址不同,锁不同
问题解决:使用id.toString().intern(),intern()方法的原理是虽然你toString()方法会new一个新的String对象,但是我会先去字符串池里找,找不到对应的值我才会new,找到了我直接复用该String地址,从而保证了用户id的值一样锁的定义也一样
-
@Transactional
-
public Result
creatOrder
(Long voucherId) {
-
//根据用户id和优惠券id查询数据库
-
Long
userId
= UserHolder.getUser().getId();
-
synchronized (userId.toString().intern()){
-
int
count
= query().eq(
"user_id", userId).eq(
"voucher_id", voucherId).count();
-
if(count >
0){
-
//该用户已经下过单了
-
return Result.fail(
"一个用户只能下一单");
-
}
-
//库存足
-
//5.库存减1
-
boolean
success
= iSeckillVoucherService.update()
-
.setSql(
"stock = stock -1")
-
.eq(
"voucher_id", voucherId).gt(
"stock",
0)
//乐观锁
-
.update();
-
if (!success){
-
return Result.fail(
"库存不足");
-
}
-
//6.创建订单
-
VoucherOrder
voucherOrder
=
new
VoucherOrder();
-
//优惠券id
-
voucherOrder.setVoucherId(voucherId);
-
//订单id
-
Long
orderId
= redisIdWorker.setId(
"order");
-
voucherOrder.setId(orderId);
-
//用户id
-
voucherOrder.setUserId(userId);
-
//存入数据库
-
save(voucherOrder);
-
return Result.ok(orderId);
-
}
-
}
AI写代码
代码块锁事务管理问题:由于此时锁是加在方法内部的,而我们的事务管理是由Spring来管理,要等到锁释放后,方法执行完,才能进行事务提交(更新库存,创建订单),而此时锁优先于事务提交之前就已经释放了,那么其他的线程就可以进行操作,依然会出现并发问题
解释:
前提:同一个用户发出两个请求,并且此时用户没有下单(订单数0)
==》线程1先执行
==》线程1查询订单数量(0)
==》线程1获取锁成功,执行锁内代码
==》线程1释放锁,但是事务还未提交
==》线程2查询订单数量(0)
==》线程2获取锁成功,执行锁内代码
==》线程2释放锁
==》线程1事务提交成功(订单加1)
==》线程2事务提交成功(订单加1)
------
此时同一个用户下了俩单
解决:既然是锁和事务执行顺序问题,那么我们先让事务先执行,锁后释放,而由于我们已经将要加锁的代码抽离出来形成一个新的方法,那么我们可以在调用该方法时给它加锁,从而锁住整个函数,保证数据已经更新
-
@Service
-
public
class
VoucherOrderServiceImpl
extends
ServiceImpl<VoucherOrderMapper, VoucherOrder>
implements
IVoucherOrderService {
-
-
@Autowired
-
private ISeckillVoucherService iSeckillVoucherService;
-
@Autowired
-
private RedisIdWorker redisIdWorker;
-
@Override
-
public Result
seckillVoucher
(Long voucherId) {
-
//1.根据id查询数据库优惠券信息
-
SeckillVoucher
voucher
= iSeckillVoucherService.getById(voucherId);
-
-
//2.获取时间
-
LocalDateTime
beginTime
= voucher.getBeginTime();
-
LocalDateTime
endTime
= voucher.getEndTime();
-
//3.判断时间
-
if (beginTime.isAfter(LocalDateTime.now())) {
-
return Result.fail(
"秒杀还未开始");
-
}
-
if (endTime.isBefore(LocalDateTime.now())) {
-
return Result.fail(
"秒杀已经结束");
-
}
-
//4.获取库存
-
Integer
stock
= voucher.getStock();
-
//库存不足
-
if(stock <
1){
-
return Result.fail(
"库存不足");
-
}
-
Long
userId
= UserHolder.getUser().getId();
-
synchronized (userId.toString().intern()){
-
return creatOrder(voucherId);
-
}
-
-
}
-
-
@Transactional
-
public Result
creatOrder
(Long voucherId) {
-
//根据用户id和优惠券id查询数据库
-
Long
userId
= UserHolder.getUser().getId();
-
int
count
= query().eq(
"user_id", userId).eq(
"voucher_id", voucherId).count();
-
if(count >
0){
-
//该用户已经下过单了
-
return Result.fail(
"一个用户只能下一单");
-
}
-
//库存足
-
//5.库存减1
-
boolean
success
= iSeckillVoucherService.update()
-
.setSql(
"stock = stock -1")
-
.eq(
"voucher_id", voucherId).gt(
"stock",
0)
//乐观锁
-
.update();
-
if (!success){
-
return Result.fail(
"库存不足");
-
}
-
//6.创建订单
-
VoucherOrder
voucherOrder
=
new
VoucherOrder();
-
//优惠券id
-
voucherOrder.setVoucherId(voucherId);
-
//订单id
-
Long
orderId
= redisIdWorker.setId(
"order");
-
voucherOrder.setId(orderId);
-
//用户id
-
voucherOrder.setUserId(userId);
-
//存入数据库
-
save(voucherOrder);
-
return Result.ok(orderId);
-
}
-
-
}
AI写代码
-
-
@Component
-
public
class
RedisIdWorker {
-
-
@Autowired
-
private
final StringRedisTemplate stringRedisTemplate;
-
-
public
RedisIdWorker
(StringRedisTemplate stringRedisTemplate) {
-
this.stringRedisTemplate = stringRedisTemplate;
-
}
-
-
//定义开始时间戳
-
private
static
final
Long
BEGIN_TIME_SECOND
=
1740960000L;
-
//移动位数
-
private
static
final
Long
COUNT_BIT
=
32L;
-
-
public Long
setId
(String keyPrefix){
-
//1.设置时间戳
-
//当前时间戳
-
LocalDateTime
now
= LocalDateTime.now();
-
long
second
= now.toEpochSecond(ZoneOffset.UTC);
-
//最终时间戳
-
Long
time
= second - BEGIN_TIME_SECOND;
-
-
//2.获取序列号
-
String
data
= now.format(DateTimeFormatter.ofPattern(
"yyyy:MM:dd"));
-
//Redis返回的序列号
-
long
increment
= stringRedisTemplate.opsForValue().increment(
"icr" + keyPrefix + data);
-
-
//拼接
-
return time << COUNT_BIT | increment;
-
}
-
}
AI写代码
思考:由于我们使用的是方法调用方法(锁的),而在相同类里方法调用方法使用的是this关键字,this代表当前类的对象(不是Spring的代理对象),而我们的事务生效是因为Spring对当前类实现了动态代理,是拿到了它的动态代理对象进行的事务管理,而现在的this调用是非代理对象不拥有事务功能(Spring事务失效的可能性之一),因此事务管理将会失效
解决:既然是没有代理对象来调用方法,那么我们就使用代理对象来调用方法
实现:
- 添加依赖
- 启动类添加注解(暴露代理对象)
- 使用AopContet.currentProxy();获取当前对象的代理对象
代码实现:
-
@Service
-
public
class
VoucherOrderServiceImpl
extends
ServiceImpl<VoucherOrderMapper, VoucherOrder>
implements
IVoucherOrderService {
-
-
@Autowired
-
private ISeckillVoucherService iSeckillVoucherService;
-
@Autowired
-
private RedisIdWorker redisIdWorker;
-
@Override
-
public Result
seckillVoucher
(Long voucherId) {
-
//1.根据id查询数据库优惠券信息
-
SeckillVoucher
voucher
= iSeckillVoucherService.getById(voucherId);
-
-
//2.获取时间
-
LocalDateTime
beginTime
= voucher.getBeginTime();
-
LocalDateTime
endTime
= voucher.getEndTime();
-
//3.判断时间
-
if (beginTime.isAfter(LocalDateTime.now())) {
-
return Result.fail(
"秒杀还未开始");
-
}
-
if (endTime.isBefore(LocalDateTime.now())) {
-
return Result.fail(
"秒杀已经结束");
-
}
-
//4.获取库存
-
Integer
stock
= voucher.getStock();
-
//库存不足
-
if(stock <
1){
-
return Result.fail(
"库存不足");
-
}
-
Long
userId
= UserHolder.getUser().getId();
-
synchronized (userId.toString().intern()){
-
//获取代理对象
-
IVoucherOrderService
proxy
= (IVoucherOrderService) AopContext.currentProxy();
-
return proxy.creatOrder(voucherId);
-
}
-
-
}
-
-
@Transactional
-
public Result
creatOrder
(Long voucherId) {
-
//根据用户id和优惠券id查询数据库
-
Long
userId
= UserHolder.getUser().getId();
-
int
count
= query().eq(
"user_id", userId).eq(
"voucher_id", voucherId).count();
-
if(count >
0){
-
//该用户已经下过单了
-
return Result.fail(
"一个用户只能下一单");
-
}
-
//库存足
-
//5.库存减1
-
boolean
success
= iSeckillVoucherService.update()
-
.setSql(
"stock = stock -1")
-
.eq(
"voucher_id", voucherId).gt(
"stock",
0)
//乐观锁
-
.update();
-
if (!success){
-
return Result.fail(
"库存不足");
-
}
-
//6.创建订单
-
VoucherOrder
voucherOrder
=
new
VoucherOrder();
-
//优惠券id
-
voucherOrder.setVoucherId(voucherId);
-
//订单id
-
Long
orderId
= redisIdWorker.setId(
"order");
-
voucherOrder.setId(orderId);
-
//用户id
-
voucherOrder.setUserId(userId);
-
//存入数据库
-
save(voucherOrder);
-
return Result.ok(orderId);
-
}
-
-
}
AI写代码
-
-
@Component
-
public
class
RedisIdWorker {
-
-
@Autowired
-
private
final StringRedisTemplate stringRedisTemplate;
-
-
public
RedisIdWorker
(StringRedisTemplate stringRedisTemplate) {
-
this.stringRedisTemplate = stringRedisTemplate;
-
}
-
-
//定义开始时间戳
-
private
static
final
Long
BEGIN_TIME_SECOND
=
1740960000L;
-
//移动位数
-
private
static
final
Long
COUNT_BIT
=
32L;
-
-
public Long
setId
(String keyPrefix){
-
//1.设置时间戳
-
//当前时间戳
-
LocalDateTime
now
= LocalDateTime.now();
-
long
second
= now.toEpochSecond(ZoneOffset.UTC);
-
//最终时间戳
-
Long
time
= second - BEGIN_TIME_SECOND;
-
-
//2.获取序列号
-
String
data
= now.format(DateTimeFormatter.ofPattern(
"yyyy:MM:dd"));
-
//Redis返回的序列号
-
long
increment
= stringRedisTemplate.opsForValue().increment(
"icr" + keyPrefix + data);
-
-
//拼接
-
return time << COUNT_BIT | increment;
-
}
-
}
AI写代码
3.3.集群模式下的线程安全问题
原因:在集群的情况下,同一个用户的多次请求如果请求到不同的Tomcat,那么锁也会不同,依然会出现超卖问题
思考:在集群情况下有多台Tomcat那么就会有多台jvm,而不同的jvm的锁(维护了一个锁的监视器对象)是不同的
解释:由于我们的锁是基于用户id来实现的,id记录在常量池中,id相同则代表是同一个锁(同一个监视器),就是监视器里有值了(值就是id),无论有多少个线程,只要第一个线程获取到锁(该用户id值被记录在监视器中),其他线程来获取锁,而锁发现监视器已经有值了,那么线程会获取锁失败,所以我们是基于看监视器对象是否记录值,而不同的Tomcat的监视器对象并不共享,因此同一个用户可以在多个Tomcat中形成多个锁
当我们集群时
==》有一个新的部署
==》就会有一个新的Tomcat
==》就会有一个新的jvm
==》就会有一个新的监视器对象(不同的jvm有不同的监视器)
==》因此当id相同时,Tomcat不同时,可以重复获取锁
==》假设有2个jvm
==》2个监视器
==》2个相同的id锁
-----
那么还是会出现线程安全问题,依旧是一个用户可以根据Tomcat的多少来下多少单
总结:在集群/分布式系统的情况下会有多个jvm存在,由于我们使用的是jvm自带的锁synchronized,而每个jvm都有自己的锁监视器对象,所以每个锁都可以有一个线程来获取,出现并行运行,出现安全问题
欢迎使用Markdown编辑器
你好! 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章,了解一下Markdown的基本语法知识。
新的改变
我们对Markdown编辑器进行了一些功能拓展与语法支持,除了标准的Markdown编辑器功能,我们增加了如下几点新功能,帮助你用它写博客:
- 全新的界面设计 ,将会带来全新的写作体验;
- 在创作中心设置你喜爱的代码高亮样式,Markdown 将代码片显示选择的高亮样式 进行展示;
- 增加了 图片拖拽 功能,你可以将本地的图片直接拖拽到编辑区域直接展示;
- 全新的 KaTeX数学公式 语法;
- 增加了支持甘特图的mermaid语法1 功能;
- 增加了 多屏幕编辑 Markdown文章功能;
- 增加了 焦点写作模式、预览模式、简洁写作模式、左右区域同步滚轮设置 等功能,功能按钮位于编辑区域与预览区域中间;
- 增加了 检查列表 功能。
功能快捷键
撤销:Ctrl/Command + Z
重做:Ctrl/Command + Y
加粗:Ctrl/Command + B
斜体:Ctrl/Command + I
标题:Ctrl/Command + Shift + H
无序列表:Ctrl/Command + Shift + U
有序列表:Ctrl/Command + Shift + O
检查列表:Ctrl/Command + Shift + C
插入代码:Ctrl/Command + Shift + K
插入链接:Ctrl/Command + Shift + L
插入图片:Ctrl/Command + Shift + G
查找:Ctrl/Command + F
替换:Ctrl/Command + G
合理的创建标题,有助于目录的生成
直接输入1次#,并按下space后,将生成1级标题。
输入2次#,并按下space后,将生成2级标题。
以此类推,我们支持6级标题。有助于使用TOC
语法后生成一个完美的目录。
如何改变文本的样式
强调文本 强调文本
加粗文本 加粗文本
标记文本
删除文本
引用文本
H2O is是液体。
210 运算结果是 1024.
插入链接与图片
链接: link.
图片:
带尺寸的图片:
居中的图片:
居中并且带尺寸的图片:
当然,我们为了让用户更加便捷,我们增加了图片拖拽功能。
如何插入一段漂亮的代码片
去博客设置页面,选择一款你喜欢的代码片高亮样式,下面展示同样高亮的 代码片
.
// An highlighted block
var foo = 'bar';
生成一个适合你的列表
- 项目
- 项目
- 项目
- 项目
- 项目1
- 项目2
- 项目3
- 计划任务
- 完成任务
创建一个表格
一个简单的表格是这么创建的:
项目 | Value |
---|---|
电脑 | $1600 |
手机 | $12 |
导管 | $1 |
设定内容居中、居左、居右
使用:---------:
居中
使用:----------
居左
使用----------:
居右
第一列 | 第二列 | 第三列 |
---|---|---|
第一列文本居中 | 第二列文本居右 | 第三列文本居左 |
SmartyPants
SmartyPants将ASCII标点字符转换为“智能”印刷标点HTML实体。例如:
TYPE | ASCII | HTML |
---|---|---|
Single backticks | 'Isn't this fun?' | ‘Isn’t this fun?’ |
Quotes | "Isn't this fun?" | “Isn’t this fun?” |
Dashes | -- is en-dash, --- is em-dash | – is en-dash, — is em-dash |
创建一个自定义列表
-
Markdown
- Text-to- HTML conversion tool Authors
- John
- Luke
如何创建一个注脚
一个具有注脚的文本。2
注释也是必不可少的
Markdown将文本转换为 HTML。
KaTeX数学公式
您可以使用渲染LaTeX数学表达式 KaTeX:
Gamma公式展示 Γ ( n ) = ( n − 1 ) ! ∀ n ∈ N \Gamma(n) = (n-1)!\quad\forall n\in\mathbb N Γ(n)=(n−1)!∀n∈N 是通过欧拉积分
Γ ( z ) = ∫ 0 ∞ t z − 1 e − t d t . \Gamma(z) = \int_0^\infty t^{z-1}e^{-t}dt\,. Γ(z)=∫0∞tz−1e−tdt.
你可以找到更多关于的信息 LaTeX 数学表达式here.
新的甘特图功能,丰富你的文章
- 关于 甘特图 语法,参考 这儿,
UML 图表
可以使用UML图表进行渲染。 Mermaid. 例如下面产生的一个序列图:
这将产生一个流程图。:
- 关于 Mermaid 语法,参考 这儿,
FLowchart流程图
我们依旧会支持flowchart的流程图:
- 关于 Flowchart流程图 语法,参考 这儿.
导出与导入
导出
如果你想尝试使用此编辑器, 你可以在此篇文章任意编辑。当你完成了一篇文章的写作, 在上方工具栏找到 文章导出 ,生成一个.md文件或者.html文件进行本地保存。
导入
如果你想加载一篇你写过的.md文件,在上方工具栏可以选择导入功能进行对应扩展名的文件导入,
继续你的创作。
注脚的解释 ↩︎