【 Redis | 实战篇 秒杀实现 】
目录
前言:
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位是符号位,始终为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 {@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 ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Autowiredprivate ISeckillVoucherService iSeckillVoucherService;@Autowiredprivate RedisIdWorker redisIdWorker;@Override@Transactionalpublic 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.解决超卖问题的方案
解决方案:
方案一:悲观锁
悲观锁:认为线程安全问题一定会发生,因此在每次操作数据之前先获取锁,以此确保线程安全,保证线程串行执行
- 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 {@Autowiredprivate ISeckillVoucherService iSeckillVoucherService;@Autowiredprivate RedisIdWorker redisIdWorker;@Override@Transactionalpublic 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 ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Autowiredprivate ISeckillVoucherService iSeckillVoucherService;@Autowiredprivate RedisIdWorker redisIdWorker;@Override@Transactionalpublic 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 ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Autowiredprivate ISeckillVoucherService iSeckillVoucherService;@Autowiredprivate RedisIdWorker redisIdWorker;@Override@Transactionalpublic 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 ServiceImpl<VoucherOrderMapper, 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 ServiceImpl<VoucherOrderMapper, 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都有自己的锁监视器对象,所以每个锁都可以有一个线程来获取,出现并行运行,出现安全问题
相关文章:
【 Redis | 实战篇 秒杀实现 】
目录 前言: 1.全局ID生成器 2.秒杀优惠券 2.1.秒杀优惠券的基本实现 2.2.超卖问题 2.3.解决超卖问题的方案 2.4.基于乐观锁来解决超卖问题 3.秒杀一人一单 3.1.秒杀一人一单的基本实现 3.2.单机模式下的线程安全问题 3.3.集群模式下的线程安全问题 前言&…...
手搓传染病模型(SEIARW)
在传染病传播的研究中,水传播途径是一个重要的考量因素。SEAIRW 模型(易感者 S - 暴露者 E - 感染者 I - 无症状感染者 A - 康复者 R - 水中病原体 W)综合考虑了人与人接触传播以及水传播的双重机制,为分析此类传染病提供了全面的…...
【C++】深入理解 unordered 容器、布隆过滤器与分布式一致性哈希
【C】深入理解 unordered 容器、布隆过滤器与分布式一致性哈希 在日常开发中,无论是数据结构优化、缓存设计,还是分布式架构搭建,unordered_map、布隆过滤器和一致性哈希都是绕不开的关键工具。它们高效、轻量,在性能与扩展性方面…...
第五天——贪心算法——射气球
1.题目 有一些球形气球贴在一个表示 XY 平面的平坦墙壁上。气球用一个二维整数数组 points 表示,其中 points[i] [xstart, xend] 表示第 i 个气球的水平直径范围从 xstart 到 xend。你并不知道这些气球的具体 y 坐标。 可以从 x 轴上的不同位置垂直向上࿰…...
麦肯锡110页PPT企业组织效能提升调研与诊断分析指南
“战略清晰、团队拼命、资源充足,但业绩就是卡在瓶颈期上不去……”这是许多中国企业面临的真实困境。表面看似健康的企业,往往隐藏着“组织亚健康”问题——跨部门扯皮、人才流失、决策迟缓、市场反应滞后……麦肯锡最新研究揭示:组织健康度…...
BFS算法篇——从晨曦到星辰,BFS算法在多源最短路径问题中的诗意航行(上)
文章目录 引言一、多源BFS的概述二、应用场景三、算法步骤四、代码实现五、代码解释六、总结 引言 在浩渺的图论宇宙中,图的每一条边、每一个节点都是故事的组成部分。每当我们站在一个复杂的迷宫前,开始感受它的深邃时,我们往往不再局限于从…...
理解 C# 中的各类指针
前言 变量可以理解成是一块内存位置的别名,访问变量也就是访问对应内存中的数据。 指针是一种特殊的变量,它存储了一个内存地址,这个内存地址代表了另一块内存的位置。 指针指向的可以是一个变量、一个数组元素、一个对象实例、一块非托管内存…...
MySQL 事务(二)
文章目录 事务隔离性理论理解隔离性隔离级别 事务隔离级别的设置和查看事务隔离级别读未提交读提交(不可重复读) 事务隔离性理论 理解隔离性 MySQL服务可能会同时被多个客户端进程(线程)访问,访问的方式以事务方式进行一个事务可能由多条SQL…...
【HarmonyOS】ArkTS开发应用的横竖屏切换
文章目录 1、简介2、静态 — 横竖屏切换2.1、效果2.2、实现原理2.3、module.json5 源码 3、动态 — 横竖屏切换3.1、应用随系统旋转切换横竖屏3.2、setPreferredOrientation 原理配置3.3、锁定旋转的情况下,手动设置横屏状态 1、简介 在完成全屏网页嵌套应用开发后…...
Linux中find命令用法核心要点提炼
大家好,欢迎来到程序视点!我是你们的老朋友.小二! 以下是针对Linux中find命令用法的核心要点提炼: 基础语法结构 find [路径] [选项] [操作]路径:查找目录(.表当前目录,/表根目录)…...
专栏项目框架介绍
项目整体实现框图 如下图所示,是该项目的整体框图,项目的功能概括为:PC端下发数据文件,FPGA板卡接收数据文件,缓存至DDR中,待数据文件发送完毕,循环读取DDR有效写区域数据,将DDR数据…...
WSL 安装 Debian 12 后,Linux 如何安装 vim ?
在 WSL 的 Debian 12 中安装 Vim 非常简单,只需使用 apt 包管理器即可。以下是详细步骤: 1. 更新软件包列表 首先打开终端,确保系统包列表是最新的: sudo apt update2. 安装 Vim 直接通过 apt 安装 Vim: sudo apt …...
【SpringBoot】从零开始全面解析Spring MVC (一)
本篇博客给大家带来的是SpringBoot的知识点, 本篇是SpringBoot入门, 介绍Spring MVC相关知识. 🐎文章专栏: JavaEE初阶 🚀若有问题 评论区见 ❤ 欢迎大家点赞 评论 收藏 分享 如果你不知道分享给谁,那就分享给薯条. 你们的支持是我不断创作的动力 . 王子…...
C++—特殊类设计设计模式
目录 C—特殊类设计&设计模式1.设计模式2.特殊类设计2.1设计一个无法被拷贝的类2.2设计一个只能在堆上创建对象的类2.3设计一个只能在栈上创建对象的类2.4设计一个类,无法被继承2.5设计一个类。这个类只能创建一个对象【单例模式】2.5.1懒汉模式实现2.5.2饿汉模…...
初入OpenCV
OpenCV简介 OpenCV是一个开源的跨平台计算机视觉库,它实现了图像处理和计算机视觉方面的很多通用算法。 应用场景: 目标识别:人脸、车辆、车牌、动物; 自动驾驶;医学影像分析; 视频内容理解分析ÿ…...
霍夫圆变换全面解析(OpenCV)
文章目录 一、霍夫圆变换基础1.1 霍夫圆变换概述1.2 圆的数学表达与参数化 二、霍夫圆变换算法实现2.1 标准霍夫圆变换算法流程2.2 参数空间的表示与优化 三、关键参数解析3.1 OpenCV中的HoughCircles参数3.2 参数调优策略 四、Python与OpenCV实现参考4.1 基本实现代码4.2 改进…...
互联网大厂Java求职面试:优惠券服务架构设计与AI增强实践-4
互联网大厂Java求职面试:优惠券服务架构设计与AI增强实践-4 场景设定 面试官:某互联网大厂技术总监,拥有超过10年大型互联网企业一线技术管理经验,擅长分布式架构、微服务治理、云原生等领域。 候选人:郑薪苦&#…...
项目中会出现的css样式
1.重复渐变边框 思路: 主要是用重复的背景渐变实现的 如图: <div class"card"><div class"container">全面收集中医癌毒临床医案,建立医案共享机制,构建癌毒病机知识图谱,便于医疗人…...
LeetCode[101]对称二叉树
思路: 对称二叉树是左右子树对称,而不是左右子树相等,所以假设一个树只有3个节点,那么判断这个数是否是对称二叉树,肯定是先判断左右两个树,然后再看根节点,这样递归顺序我们就确认了࿰…...
黑马k8s(四)
1.资源管理介绍 本章节主要介绍yaml语法和kubernetes的资源管理方式 2.YAML语言介绍 3.资源管理方式 命令式对象管理 dev下删除了pod,之后发现还有pod,把原来的pod删除了,重新启动了一个 命令式对象配置 声明式对象配置 命令式对象配置&…...
华为ensp实现跨vlan通信
要在网络拓扑中实现主机192.168.1.1、192.168.1.2和192.168.2.1之间的互相通信,需要正确配置交换机(S5700)和路由器(AR3260),以确保不同网段之间的通信(即VLAN间路由)。 网络拓扑分析…...
TCPIP详解 卷1协议 十 用户数据报协议和IP分片
10.1——用户数据报协议和 IP 分片 UDP是一种保留消息边界的简单的面向数据报的传输层协议。它不提供差错纠正、队列管理、重复消除、流量控制和拥塞控制。它提供差错检测,包含我们在传输层中碰到的第一个真实的端到端(end-to-end)校验和。这…...
Java笔记4
第一章 static关键字 2.1 概述 以前我们定义过如下类: public class Student {// 成员变量public String name;public char sex; // 男 女public int age;// 无参数构造方法public Student() {}// 有参数构造方法public Student(String a) {} }我们已经知道面向…...
Matlab 垂向七自由度轨道车辆开关型半主动控制
1、内容简介 Matlab 229-垂向七自由度轨道车辆开关型半主动控制 可以交流、咨询、答疑 2、内容说明 略 3、仿真分析 略 4、参考论文 略...
Matlab 短时交通流预测AR模型
1、内容简介 Matlab 230-短时交通流预测AR模型 可以交流、咨询、答疑 2、内容说明 略 3、仿真分析 略 4、参考论文 略城市道路短时交通流预测.pdf...
MYSQL之表的约束
表中真正约束字段的是数据类型, 但是只有数据类型约束就很单一, 也需要有一些额外的约束, 从而更好的保证数据的合法性, 从业务逻辑角度保证数据的正确性. 比如有一个字段是email, 要求是唯一的. 为什么要有表的约束? 表的约束: 表中一定要有各种约束, 通过约束, 让我们未来…...
使用ACE-Step在本地生成AI音乐
使用ACE-Step v1-3.5B开源模型从文本提示、标签和歌词创建完整的AI生成歌曲 — 无需云服务,无需API,仅需您的GPU。 这是由ACE Studio和StepFun开发的开源音乐生成模型。 在对数据隐私和云服务依赖性日益增长的担忧时代,ACE-Step将强大的文本转音乐生成完全离线,使其成为A…...
web 自动化之 Unittest 四大组件
文章目录 一、如何开展自动化测试1、项目需求分析,了解业务需求 web 功能纳入自动化测试2、选择何种方式实现自动化测试 二、Unittest 框架三、TestCase 测试用例四、TestFixture 测试夹具 执行测试用例前的前置操作及后置操作五、TestSuite 测试套件 & TestLoa…...
2025年渗透测试面试题总结-渗透测试红队面试七(题目+回答)
网络安全领域各种资源,学习文档,以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各种好玩的项目及好用的工具,欢迎关注。 目录 渗透测试红队面试七 一百八十一、Shiro漏洞类型,721原理,721利用要注意什么&am…...
Mysql的索引,慢查询和数据库表的设计以及乐观锁和悲观锁
设计高性能数据表的原则 数据库设计经验和技巧 单张数据表的字段不宜过多(20个),如果确实存在大量field,考虑拆成多张表或json text存储 数据表字段都是not null的,即使没有数据,最好也使用无意义的值填充,…...
day012-软件包管理专题
文章目录 1. 生成随机密码2. 软件包管理2.1 类红帽系统2.1.1 安装软件包2.1.2 查找软件包2.1.3 查看软件包内容2.1.4 查看命令或文件属于哪个软件包2.1.5 重新安装软件包2.1.6 删除软件包2.1.7 升级2.1.8 rpm安装软件包2.1.9 rpm升级软件包2.1.10 rpm检查软件包文件是否改变 3.…...
学习黑客5 分钟深入浅出理解Windows Firewall
5 分钟深入浅出理解Windows Firewall 🔥 大家好!今天我们将探索Windows防火墙——这是Windows操作系统中的核心安全组件,负责控制进出计算机的网络流量。无论你是计算机初学者,还是在TryHackMe等平台上学习网络安全的爱好者&…...
node .js 启动基于express框架的后端服务报错解决
问题: node .js 用npm start 启动基于express框架的后端服务报错如下: /c/Program Files/nodejs/npm: line 65: 26880 Segmentation fault "$NODE_EXE" "$NPM_CLI_JS" "$" 原因分析: 遇到 /c/Program F…...
feign.RequestInterceptor 简介-笔记
1. feign.RequestInterceptor 简介 Feign 是一个声明式 Web 服务客户端,用于简化 HTTP 请求的编写与管理。feign.RequestInterceptor 是 Feign 提供的一个接口,用于在请求发出之前对其进行拦截和修改。这在微服务架构中非常有用,比如在请求中…...
软考错题(四)
在程序执行过程中,高速缓存cache与主存间的地址映射由硬件自动完成 以下关于两个浮点数相加运算的叙述中,正确的是首先进行对阶,阶码小的向阶码大的对齐 认证只能阻止主动攻击不能阻止被动攻击 BGP是外部网关协议 查看端口信息࿱…...
SSRF相关
SSRF(Server Side Request Forgery,服务器端请求伪造),攻击者以服务器的身份发送一条构造好的请求给服务器所在地内网进行探测或攻击。 产生原理: 服务器端提供了能从其他服务器应用获取数据的功能,如从指定url获取网页内容、加载指定地址的图…...
供应链学习
供应链安全 供应链:整个业务系统中的节点(一般是上游节点) 乙方一般提供资源:人 软件 硬件 服务 如何寻找供应链 1.招投标信息:寻标包 例如:烟草 智能办公 2.网站本身指纹 例如: powered by xxx…...
力扣HOT100之二叉树:226. 翻转二叉树
这道题很简单,用递归来做,对于一个根节点来说,有两种情况我们不需要翻转:一是根节点为空,二是根节点为叶子节点。这很容易理解,当传入的节点不满足上面的两种情况时,我们就需要做一个翻转&#…...
如何让rabbitmq保存服务断开重连?保证高可用?
在 Spring Boot 集成 RabbitMQ 时,可以通过以下几种方式让 RabbitMQ 保存服务断开重连,以保证高可用: 配置自动重连 application.properties 配置 :在 Spring Boot 的配置文件 application.properties 中,可以设置 Ra…...
TCPIP详解 卷1协议 九 广播和本地组播(IGMP 和 MLD)
9.1——广播和本地组播(IGMP 和 MLD) IPv4可以使用4种IP地址:单播(unicast)、任播(anycast)、组播(multicast)和广播(broadcast)。 IPv6可以使用…...
全球变暖-bfs
1.不沉的就是4个方向没有海,一个大岛屿有一个不沉就行了,其余染色就好了 2.第一个bfs来统计总岛屿个数 3.第二个来统计不沉岛屿个数 4.一减就ac啦 #include<bits/stdc.h> using namespace std; #define N 100011 typedef long long ll; typede…...
DDD领域驱动开发
1. 现象: 软件设计质量最高的时候是第一次设计的那个版本(通常是因为第一次设计时,业务技术沟通最充分,从业务技术整体视角出发设计系统)。当第一个版本设计上线以后就开始各种需求变更,这常常又会打乱原有的设计。 2…...
【HarmonyOS 5】鸿蒙App Linking详解
【HarmonyOS 5】鸿蒙App Linking详解 一、前言 HarmonyOS 的 App Linking 功能为开发者提供了一个强大的工具,通过创建跨平台的深度聚合链接,实现用户在不同场景下的无缝跳转,极大地提升了用户转化率和应用的可用性。 其安全性、智能路由和…...
Android Studio 中 build、assemble、assembleDebug 和 assembleRelease 构建 aar 的区别
上一篇:Tasks中没有build选项的解决办法 概述: 在构建 aar 包时通常会在下面的选项中进行构建,但是对于如何构建,选择哪种方式构建我还是处于懵逼状态,所以我整理了一下几种构建方式的区别以及如何选择。 1. build…...
【爬虫】12306查票
城市代码: 没有加密,关键部分: 完整代码: import json import requests with open(rE:\学习文件夹(关于爬虫)\项目实战\12306\城市代码.json,r,encodingutf-8) as f:city_codef.read() city json.loads(c…...
火山RTC 7 获得远端裸数据
一、获得远端裸数据 1、获得h264数据 1)、远端编码后视频数据监测器 /*** locale zh* type callback* region 视频管理* brief 远端编码后视频数据监测器<br>* 注意:回调函数是在 SDK 内部线程(非 UI 线程)同步抛出来的&a…...
请求参数:Header 参数,Body 参数,Path 参数,Query 参数分别是什么意思,什么样的,分别通过哪个注解获取其中的信息
在API开发中(如Spring Boot),请求参数可以通过不同方式传递,对应不同的注解获取。以下是 Header参数、Body参数、Path参数、Query参数 的区别及对应的注解: Header 参数 • 含义:通过HTTP请求头&#x…...
【Web/HarmonyOS】采用ArkTS+Web组件开发网页嵌套的全屏应用
文章目录 1、简介2、效果3、在ArkTs上全屏Web3.1、创建ArkTS应用3.2、修改模块化配置(module.json5)3.3、修改系统栏控制(ArkTS代码) 4、双网页嵌套Web实现5、ArkTSWeb技术架构的演进 1、简介 在鸿蒙应用开发领域,技术…...
Leetcode (力扣)做题记录 hot100(34,215,912,121)
力扣第34题:在排序数组中查找第一个数和最后一个数 34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode) class Solution {public int[] searchRange(int[] nums, int target) {int left 0;int right nums.length - 1;int[…...
Babylon.js学习之路《三、创建你的第一个 3D 场景:立方体、球体与平面》
文章目录 1. 引言:从零构建一个 3D 场景1.1 目标与成果预览1.2 前置条件 2. 初始化 Babylon.js 场景2.1 创建 HTML 骨架2.2 初始化引擎与场景 3. 创建基础几何体3.1 立方体(Box)3.2 球体(Sphere)3.3 平面(P…...