外贸云网站建设,体彩网站建设,wordpress 标签 图片 alt,杭州市招投标交易中心目录
前言#xff1a;
1.全局ID生成器
2.秒杀优惠券
2.1.秒杀优惠券的基本实现
2.2.超卖问题
2.3.解决超卖问题的方案
2.4.基于乐观锁来解决超卖问题
3.秒杀一人一单
3.1.秒杀一人一单的基本实现
3.2.单机模式下的线程安全问题
3.3.集群模式下的线程安全问题 前言
1.全局ID生成器
2.秒杀优惠券
2.1.秒杀优惠券的基本实现
2.2.超卖问题
2.3.解决超卖问题的方案
2.4.基于乐观锁来解决超卖问题
3.秒杀一人一单
3.1.秒杀一人一单的基本实现
3.2.单机模式下的线程安全问题
3.3.集群模式下的线程安全问题 前言
实现全局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位是符号位始终为00位正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那为什么还要拼接时间呢 解释如果你的序列号都使用同一个keyRedis存入是由上限的而且为了你以后方便查询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 {Autowiredprivate 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;}
}Redis图效果 2.秒杀优惠券
2.1.秒杀优惠券的基本实现
思考在下单优惠券之前我们需要判断两点
秒杀是否开始或结束库存是否充足
步骤 前端提交优惠券id 》后端接收id 》根据优惠券id查询数据库得到优惠券信息 》判断秒杀是否开始或结束 》秒杀没有开始或已经结束 》返回错误信息 ------- 》秒杀正在进行 》判断库存是否充足 》不足 》返回错误信息 ------- 》充足 》扣减库存 》创建订单 》返回订单id Service
public class VoucherOrderServiceImpl extends ServiceImplVoucherOrderMapper, VoucherOrder implements IVoucherOrderService {Autowiredprivate ISeckillVoucherService iSeckillVoucherService;Autowiredprivate RedisIdWorker redisIdWorker;OverrideTransactionalpublic 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.库存减1boolean success iSeckillVoucherService.update().setSql(stock stock -1).eq(voucher_id, voucherId).update();if (!success){return Result.fail(库存不足);}//6.创建订单VoucherOrder voucherOrder new VoucherOrder();//优惠券idvoucherOrder.setVoucherId(voucherId);//订单idLong orderId redisIdWorker.setId(order);voucherOrder.setId(orderId);//用户idLong userId UserHolder.getUser().getId();voucherOrder.setUserId(userId);//存入数据库save(voucherOrder);return Result.ok(orderId);}} Component
public class RedisIdWorker {Autowiredprivate 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;}
}解释我们唯一要注意的点就是秒杀的时间和库存的数量判断
2.2.超卖问题
解释
前提库存此时为1 例子线程1先执行查询库存线程2再执行查询线程1扣减库存线程2扣减库存 》线程1先执行 》线程1查询库存1 》线程2抢到执行权 》线程2查询库存1 》线程1再次抢到执行权 》由于库存大于0 》线程1执行库存扣减操作 》此时库存0 》线程2执行 》由于之前查询库存结果为1 》线程2也执行库存扣减操作 》此时库存-1 ---------- 那么此时优惠券库存为-1已经形成了超卖问题 2.3.解决超卖问题的方案
解决方案
方案一悲观锁
悲观锁认为线程安全问题一定会发生因此在每次操作数据之前先获取锁以此确保线程安全保证线程串行执行
SynchronizedLock都属于悲观锁优点简单粗暴缺点性能一般
方案二乐观锁
乐观锁认为线程安全问题不一定发生因此不加锁只是在更新数据时去判断有没有其他线程来对数据进行了修改
如果没有修改则认为是安全的自己才更新数据如果已经被其他线程修改说明发生了安全问题此时可以重试或返回异常优点性能好缺点存在安全率低的问题
解释悲观锁就是直接加锁,由于是加锁其他线程都需要等待因此性能低乐观锁是不加锁由于不加锁那么就会出现安全问题概率低
思考
由于我们是优惠券库存问题有数据给我们判断这个数据到底有没有修改过我们可以直接根据库存来判断是否出现数据不一致问题那么就可以采用乐观锁如果不是库存呢那么只能通过数据的整体变化来判断此时采用乐观锁是复杂的你需要判断的数据太多了那么就采用悲观锁但是悲观锁的性能一般怎么提高性能呢采用分批加锁分段锁将数据分成几份假设分成10张表那么用户是不是同时去这10张表抢同时10个人抢效率提高最终思想每次锁定的资源少
总结如果要更新数据那么可以使用乐观锁添加数据使用悲观锁
2.4.基于乐观锁来解决超卖问题
版本号法设置版本号每次查询库存时也查询版本号最后扣减库存时增加判断条件就是此时的版本号应该等于我先前查询到的版本号如果不等于事务回滚
思想更新数据前比较版本号是否发生改变
步骤 前端提交优惠券id 》后端接收id 》根据优惠券id查询数据库得到优惠券信息获取版本号 》判断秒杀是否开始或结束 》秒杀没有开始或已经结束 》返回错误信息 ------- 》秒杀正在进行 》判断库存是否充足 》不足 》返回错误信息 ------- 》充足 》判断版本号是否发生改变 》改变返回错误信息 ------- 》版本号相同 》扣减库存 》创建订单 》返回订单id CAS法直接比较库存在更新数据时增加判断条件库存是否发生改变库存改变不执行更新操作事务回滚
思想直接利用已有数据来进行判断根据数据是否发生变化来确定是否更新数据
步骤 前端提交优惠券id 》后端接收id 》根据优惠券id查询数据库得到优惠券信息 》判断秒杀是否开始或结束 》秒杀没有开始或已经结束 》返回错误信息 ------- 》秒杀正在进行 》判断库存是否充足 》不足 》返回错误信息 ------- 》充足 》判断库存是否发生变化 》改变返回错误信息 ------- 》库存相同 》扣减库存 》创建订单 》返回订单id 思考由于我们是优惠券库存问题那么我们可以直接使用库存来直接判断只有库存发生变化那我们就不进行更新操作
代码实现
Service
public class VoucherOrderServiceImpl extends ServiceImplVoucherOrderMapper, VoucherOrder implements IVoucherOrderService {Autowiredprivate ISeckillVoucherService iSeckillVoucherService;Autowiredprivate RedisIdWorker redisIdWorker;OverrideTransactionalpublic 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.库存减1boolean 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();//优惠券idvoucherOrder.setVoucherId(voucherId);//订单idLong orderId redisIdWorker.setId(order);voucherOrder.setId(orderId);//用户idLong userId UserHolder.getUser().getId();voucherOrder.setUserId(userId);//存入数据库save(voucherOrder);return Result.ok(orderId);}} Component
public class RedisIdWorker {Autowiredprivate 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;}
}弊端如果同时大量用户抢优惠券而此时库存还有100张用户们都先进行了查询库存的操作但都没有进行库存扣减操作等到第一个先抢到优惠券后库存改变那么其他用户全部会抢券失败 前提优惠券库存100 例子100个线程都先进行了查询库存操作都还没有执行到判断库存是否发生改变 》线程1-100查询库存100 》线程1优先于其他线程先执行完判断库存操作100 》线程1扣减库存99 》不管之后是哪个线程来执行判断库存操作 》库存已经发生变化抢券失败 ---------- 那么此时100个用户抢券只抢券成功一人但是我的优惠券库存却还有99张失败率极高 怎么提高用户抢券的成功率呢
思考由于库存不能是负数那么我们最后判断的条件不再是库存是否改变而是库存大于0就行只要有库存那么我就卖给用户即使出现大量用户同时进行抢券的情况我们也可以将券买给用户而不是只能卖给第一个用户并且当库存只有一张时由于我们是更新操作数据库只允许一个线程来执行更新操作不允许多个线程同时执行更新库存操作最后一张券被大量用户抢时总会有一个用户抢到其他用户则抢不到
步骤 前端提交优惠券id 》后端接收id 》根据优惠券id查询数据库得到优惠券信息 》判断秒杀是否开始或结束 》秒杀没有开始或已经结束 》返回错误信息 ------- 》秒杀正在进行 》判断库存是否充足 》不足 》返回错误信息 ------- 》充足 》再次判断库存是否大于0 》库存不足返回错误信息 ------- 》库存足 》扣减库存 》创建订单 》返回订单id 代码实现
Service
public class VoucherOrderServiceImpl extends ServiceImplVoucherOrderMapper, VoucherOrder implements IVoucherOrderService {Autowiredprivate ISeckillVoucherService iSeckillVoucherService;Autowiredprivate RedisIdWorker redisIdWorker;OverrideTransactionalpublic 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.库存减1boolean 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();//优惠券idvoucherOrder.setVoucherId(voucherId);//订单idLong orderId redisIdWorker.setId(order);voucherOrder.setId(orderId);//用户idLong userId UserHolder.getUser().getId();voucherOrder.setUserId(userId);//存入数据库save(voucherOrder);return Result.ok(orderId);}} Component
public class RedisIdWorker {Autowiredprivate 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;}
}3.秒杀一人一单
3.1.秒杀一人一单的基本实现
思考由于是秒杀问题因此不能让用户一个人全部买走这不就是黄牛吗那么我们可以实现一个用户只能下一单
步骤 前端提交优惠券id 》后端接收id 》根据优惠券id查询数据库得到优惠券信息 》判断秒杀是否开始或结束 》秒杀没有开始或已经结束 》返回错误信息 ------- 》秒杀正在进行 》判断库存是否充足 》不足 》返回错误信息 ------- 》充足 》根据优惠券id和用户id来查询数据库返回查询数量 》判断数量是否大于0 》大于0即用户已经下过一单每张优惠券id不同 》返回错误信息 ------- 》数量小于0即用户没有下单 》再次判断库存是否大于0 》库存不足返回错误信息 ------- 》库存足 》扣减库存 》创建订单 》返回订单id 代码实现
Service
public class VoucherOrderServiceImpl extends ServiceImplVoucherOrderMapper, VoucherOrder implements IVoucherOrderService {Autowiredprivate ISeckillVoucherService iSeckillVoucherService;Autowiredprivate RedisIdWorker redisIdWorker;OverrideTransactionalpublic 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.库存减1boolean 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();//优惠券idvoucherOrder.setVoucherId(voucherId);//订单idLong orderId redisIdWorker.setId(order);voucherOrder.setId(orderId);//用户idvoucherOrder.setUserId(userId);//存入数据库save(voucherOrder);return Result.ok(orderId);}} Component
public class RedisIdWorker {Autowiredprivate 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;}
}3.2.单机模式下的线程安全问题
解释 前提库存充足并且是同一个用户下单此时该用户还没有下单订单数量0 -------- 例子一个用户同时发出俩个请求买相同的优惠券线程1先查询线程2后查询线程1判断用户是否下过单线程2判断用户是否下过单 》线程1先执行 》线程1查询订单数量0 》线程1判断订单数量 》订单数量为0可以下单 》线程2抢到执行权 》线程2执行查询订单数量0 》订单数量为0也可以下单 》线程1抢到执行权 》由于订单数量为0线程1执行下单操作 》线程2执行 》由于订单数量为0线程2执行下单操作 --------- 那么最终一个用户下了两单出现了并发安全问题 思考这是不是还是超卖问题那么还是使用锁来解决而我们现在是执行创建订单的操作乐观锁是需要根据数据的变化来实现的因此不能使用乐观锁修改用乐观添加用悲观
思路既然使用悲观锁那么我们需要考虑在哪里加锁合适是整个方法都加上锁吗不是吧我们最终问题出现在哪是并发查询订单数量那里而之前的查询库存操作等等是不需要加锁的加锁是会导致我们的性能降低因此我们需要考虑加锁的合适位置既然是对于方法内部部分代码进行加锁那么我们可以将要加锁的代码抽离出来对于这个新方法进行加锁而我们这里使用synchronized
Transactionalpublic 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.库存减1boolean 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();//优惠券idvoucherOrder.setVoucherId(voucherId);//订单idLong orderId redisIdWorker.setId(order);voucherOrder.setId(orderId);//用户id
// Long userId UserHolder.getUser().getId();voucherOrder.setUserId(userId);//存入数据库save(voucherOrder);return Result.ok(orderId);}
细节我们是直接将锁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的值一样锁的定义也一样
Transactionalpublic 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.库存减1boolean 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();//优惠券idvoucherOrder.setVoucherId(voucherId);//订单idLong orderId redisIdWorker.setId(order);voucherOrder.setId(orderId);//用户idvoucherOrder.setUserId(userId);//存入数据库save(voucherOrder);return Result.ok(orderId);}}
代码块锁事务管理问题由于此时锁是加在方法内部的而我们的事务管理是由Spring来管理要等到锁释放后方法执行完才能进行事务提交更新库存创建订单而此时锁优先于事务提交之前就已经释放了那么其他的线程就可以进行操作依然会出现并发问题
解释 前提同一个用户发出两个请求并且此时用户没有下单订单数0 》线程1先执行 》线程1查询订单数量0 》线程1获取锁成功执行锁内代码 》线程1释放锁但是事务还未提交 》线程2查询订单数量0 》线程2获取锁成功执行锁内代码 》线程2释放锁 》线程1事务提交成功订单加1 》线程2事务提交成功订单加1 ------ 此时同一个用户下了俩单 解决既然是锁和事务执行顺序问题那么我们先让事务先执行锁后释放而由于我们已经将要加锁的代码抽离出来形成一个新的方法那么我们可以在调用该方法时给它加锁从而锁住整个函数保证数据已经更新
Service
public class VoucherOrderServiceImpl extends ServiceImplVoucherOrderMapper, VoucherOrder implements IVoucherOrderService {Autowiredprivate ISeckillVoucherService iSeckillVoucherService;Autowiredprivate RedisIdWorker redisIdWorker;Overridepublic 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);}}Transactionalpublic 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.库存减1boolean 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();//优惠券idvoucherOrder.setVoucherId(voucherId);//订单idLong orderId redisIdWorker.setId(order);voucherOrder.setId(orderId);//用户idvoucherOrder.setUserId(userId);//存入数据库save(voucherOrder);return Result.ok(orderId);}} Component
public class RedisIdWorker {Autowiredprivate 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;}
}思考由于我们使用的是方法调用方法锁的而在相同类里方法调用方法使用的是this关键字this代表当前类的对象不是Spring的代理对象而我们的事务生效是因为Spring对当前类实现了动态代理是拿到了它的动态代理对象进行的事务管理而现在的this调用是非代理对象不拥有事务功能Spring事务失效的可能性之一因此事务管理将会失效
解决既然是没有代理对象来调用方法那么我们就使用代理对象来调用方法
实现
添加依赖启动类添加注解暴露代理对象使用AopContet.currentProxy()获取当前对象的代理对象
代码实现
Service
public class VoucherOrderServiceImpl extends ServiceImplVoucherOrderMapper, VoucherOrder implements IVoucherOrderService {Autowiredprivate ISeckillVoucherService iSeckillVoucherService;Autowiredprivate RedisIdWorker redisIdWorker;Overridepublic 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);}}Transactionalpublic 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.库存减1boolean 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();//优惠券idvoucherOrder.setVoucherId(voucherId);//订单idLong orderId redisIdWorker.setId(order);voucherOrder.setId(orderId);//用户idvoucherOrder.setUserId(userId);//存入数据库save(voucherOrder);return Result.ok(orderId);}} Component
public class RedisIdWorker {Autowiredprivate 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;}
}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都有自己的锁监视器对象所以每个锁都可以有一个线程来获取出现并行运行出现安全问题