乐观锁:
乐观锁的关键是判断之前查询得到的数据是否有被过修改过,常见的方式有两种
CAS法:compare and set 先比较然后再修改
版本号法
- 修改之前判断当前版本是否是之前查询的版本
- 每次修改都要修改一次版本号
超卖这样的线程安全问题,解决方案有哪些?
1.悲观锁:添加同步锁,让线程串行话执行
- 优点:简单粗暴
- 缺点:性能一般
2.乐观锁:不加锁,在更新时判断是否有其他线程在修改
- 优点:性能好
- 缺点:存在成功率低的问题
例如:在优惠卷秒杀时,多线程并发查询储存量,结果都均为100,有一个线程率先执行了修改操作,储存量->99, 那么如果在修改时判断是否与第一次查询储存量相同,则会有很多线程操作失败,但这时储存量是大于0,在业务上是可以执行的,但为了线程安全问题,使得无法修改,成功率降低
解决办法:where stock=stock 修改为 where stock>0
其它无法判断量的问题时,可以将资源分块存储,存储在多张表中
当创建一个Long类型的变量时,实际上是在创建一个指向Long对象在堆内存中位置的引用(地址),存储在堆上,通过引用(地址)来访问
相同值的Long对象不一定指向相同的地址
e519258c-fbc5-43a4-b9be-3b2bd739db92
在Java中,intern()
方法是 String
类的一个方法,它的作用是确保所有具有相同字符序列的字符串字面量都共享同一个内存空间。当调用一个字符串的 intern()
方法时,JVM 会检查字符串常量池中是否已经存在该字符串的副本:
- 如果存在,则返回常量池中该字符串的引用(即,不会创建新的字符串对象,而是返回已经存在的那个字符串的引用)。
- 如果不存在,JVM 会将该字符串添加到常量池中,并返回对该字符串的引用。
注:
- this自调用时会不被spring管理 需要通过代理对象(事务)来调用
- 要在创建订单执行完之后-> 提交事务 ->再释放锁,不然在高并发下,锁释放了,事务没提交前,可能有其它线程来创建订单
@Resource
private RedisWorker redisWorker;
@Resource
private ISeckillVoucherService iSeckillVoucherService;
/**
* 用户抢购秒杀优惠卷
* @param voucherId
* @return
*/
@Override
public Result seckillVoucher(Long voucherId) {
// 1.查询优惠卷信息
SeckillVoucher seckillVoucher = iSeckillVoucherService.getById(voucherId);
// 2.查询优惠卷秒杀是否开始,结束
if (Objects.isNull(seckillVoucher)){
return Result.fail("优惠卷不存在");
}
if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now()) && seckillVoucher.getEndTime().isBefore(LocalDateTime.now()) ){
return Result.fail("活动时间不正确");
}
// 3.查询优惠卷是否还有库存
if (seckillVoucher.getStock()<1){
return Result.fail("库存不足了");
}
Long userId = UserHolder.getUser().getId();
// 一个用户只能买一单,需要上锁,创建订单的,需要用悲观锁,对id上锁
synchronized(userId.toString().intern()){
// this自调用时会不被spring管理 需要通过代理对象(事务)来调用
IVoucherOrderService iVoucherOrderService = (IVoucherOrderService)AopContext.currentProxy();
return iVoucherOrderService.createOrder(voucherId);
}
}
@Override
@Transactional
public Result createOrder(Long voucherId){
Long userId = UserHolder.getUser().getId();
// 4.判断用户是否已经买过
Integer count= query().eq("voucher_id",voucherId)
.eq("user_id",userId)
.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();
long orderId = redisWorker.nextId("order");
voucherOrder.setId(orderId);
voucherOrder.setVoucherId(voucherId);
voucherOrder.setUserId(userId);
save(voucherOrder);
return Result.ok("success");
}
代理对象的依赖
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
@MapperScan("com.hmdp.mapper")
@EnableAspectJAutoProxy(exposeProxy = true)
@SpringBootApplication
public class HmDianPingApplication {
public static void main(String[] args) {
SpringApplication.run(HmDianPingApplication.class, args);
}
}
在Spring中,如果你在一个bean内部自调用了一个被事务管理的方法(即该方法上使用了如@Transactional
注解来声明事务),那么这个自调用通常不会走代理,因此事务管理逻辑也不会被应用。
这是因为Spring AOP(包括事务管理)是通过代理机制来实现的。当你从Spring容器外部(例如,通过另一个bean的依赖注入)调用一个被事务管理的方法时,实际上调用的是该bean的代理对象,代理对象会拦截这个调用,并在调用目标方法之前和之后执行事务管理的逻辑(如开启事务、提交事务或回滚事务)。
然而,当你从bean内部(即使用this
关键字)调用同一个类中的另一个方法时,你直接调用的是bean的实例本身,而不是它的代理对象。因此,代理对象上的任何拦截逻辑(包括事务管理)都不会被触发。
为了解决这个问题,你有几个选项:
- 重构代码:
将需要事务管理的方法移动到一个新的bean中,并从原始bean中调用这个新bean的方法。这样,调用就会通过Spring的代理机制进行,从而应用事务管理。 - 使用
AopContext.currentProxy()
(不推荐):
虽然你可以通过AopContext.currentProxy()
在bean内部获取当前代理对象的引用,并通过该代理对象调用其他方法,但这种方法通常不被推荐。它会使代码更加复杂且难以维护,而且如果expose-proxy
没有正确配置,AopContext.currentProxy()
还会返回null
。 - 程序化事务管理(如果适用):
如果你正在编写一些非常特定的逻辑,并且需要更细粒度地控制事务,你可以考虑使用程序化事务管理(即通过编程方式开启、提交和回滚事务)。但是,请注意,这通常比声明式事务管理更复杂,并且更难于维护。 - 考虑使用AspectJ:
如果你发现Spring AOP的代理机制不能满足你的需求,你可以考虑使用AspectJ进行更强大的切面编程。AspectJ支持编译时织入和加载时织入,这意味着它可以在没有代理对象的情况下应用切面逻辑。但是,请注意,AspectJ的使用比Spring AOP更复杂,并且需要额外的配置和依赖项。
在大多数情况下,重构代码以将事务性方法移动到新的bean中是最简单且最有效的方法。这样做不仅可以确保事务管理逻辑被正确应用,还可以提高代码的可读性和可维护性。
集群部署时,一个新的部署,就意味着一个新的tomcat,一个全新的JVM,有各自的堆,栈,方法区,监视器对象就会有自己的锁监视器,锁监视器在JVM内部可以实现多个线程的锁互斥,在多个JVM时每个JV内都会有一个线程成功。