乐观锁是什么?
乐观锁时相对于悲观锁来说,在每次提交时才会判断数据是否发生冲突。乐观锁适用于读多写少的场景,这样可以提高程序的吞吐量。
乐观锁一般采用版本号来判断数据是否发生冲突。当数据在提交时,会判断之前的版本号和提交数据时的版本号。版本号一致则提交。
库存超卖问题
seckillVoucherService为优惠券秒杀接口
//将stock设定为乐观锁的版本号,判断时如果stock一致,说明此前没有其他线程修改,当前线程进行提交。
SeckillVoucher seckillVoucher = seckillVoucherService.query().one();
boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).eq("stock", seckillVoucher.getStock()).update();
但失败率太高,当其他线程同时执行完查询库存后,在进行提交时大量线程在判断版本号时不一致,放弃提交。
该进:
SeckillVoucher seckillVoucher = seckillVoucherService.query().one();
boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).gt("stock", 0).update();
解释:当库存大于0时随便提交,直至库存为0;
一人一单
思路:用户下单,根据userId和voucherId判断记录数,记录数大于 0,说明此时该usr已下一单,禁止下单,否则就行库存扣减,创建订单
Long userId = UserHolder.getUser().getId();
Integer count = query().eq("voucher_id", voucherId).eq("user_id", userId).count();
if (count >= 1){
return Result.fail("请勿重复下单!");
}
boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).update();
if (!success){
Result.fail("库存不足!");
}
//5、创建订单
.....
存在问题:多个线程同时来抢,同时判断订单不存在,则同时下单,导致多次下单。
解决:从查询订单到执行完毕加悲观锁,尽量不要加在对象上(影响性能),将锁加在用户上,多个用户加不同的锁,互不影响。
createVoucherOrder();
Long userId = UserHolder.getUser().getId();
sychronized(userId.toString().intern()){
Integer count = query().eq("voucher_id", voucherId).eq("user_id", userId).count();
if (count >= 1){
return Result.fail("请勿重复下单!");
}
boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).update();
if (!success){
Result.fail("库存不足!");
}
//5、创建订单
........
}
存在问题:spring事务是在方法结束后提交,而sychronized执行完后事务并没有提交,此时已经释放了锁,当有其他线程进入时也会获得锁。此时的事务还没有提交。
该进:这样方法执行完事务提交后才会释放锁,然而,createVoucherOrder()是被this当前对象调用,并不是Spring管理,事务之所以生效,是被Spring对当前类做了动态代理,而this不是被代理对现象。拿到事务代理对象
sychronized(usdId){
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
Result voucherOrder = proxy.createVoucherOrder(voucherId);
}
集群模式下,会维护不同的jvm,每个jvm会维护自己唯一一个锁监视器,单体下只有一个锁监视器,相同的userId会被锁住,集群下不同的jvm对应不同的锁监视器,同样的userId被在不同的服务器上。
解决:分布式锁
代码:定义一个锁接口和实现类
public interface ILock {
public boolean tryLock(long timeout);
public void unLock();
}
public class SimpleRedisLock implements ILock {
private String name;
public static final String KEY_PRE_FIX = "lock:";
public static final String ID_PRE_FIX = UUID.fastUUID().toString(true) + "-";
private StringRedisTemplate redisTemplate;
public SimpleRedisLock(String name, StringRedisTemplate redisTemplate) {
this.name = name;
this.redisTemplate = redisTemplate;
}
@Override
public boolean tryLock(long timeout) {
long threadId = Thread.currentThread().getId();
String value = ID_PRE_FIX + threadId;
Boolean isSuccess = redisTemplate.opsForValue().setIfAbsent(KEY_PRE_FIX + name, value , timeout, TimeUnit.SECONDS);
return BooleanUtil.isTrue(isSuccess);
}
@Override
public void unLock() {
String threadId = ID_PRE_FIX + Thread.currentThread().getId();
String value = redisTemplate.opsForValue().get(KEY_PRE_FIX + name);
//Todo 当前线程执行完,准备释放锁时发生了阻塞,超时释放了锁,线程2获得锁,同时当前线程阻塞结束继续执行,
// 但却释放了线程2的锁,线程2继续执行,此时线程3发现没锁又获取了锁。
//判断锁标识和释放锁是两个操作,不是原子操作
if (value.equals(threadId)){
redisTemplate.delete(KEY_PRE_FIX + name);
}
}
}
业务类
long userId = UserHolder.getUser().getId();
SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, redisTemplate);
boolean isLock = lock.tryLock(1200);
if (!isLock){
return Result.fail("不允许重复下单!");
}
try{
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
Result voucherOrder = proxy.createVoucherOrder(voucherId);
return voucherOrder;
}finally {
lock.unLock();
}
问题:存在锁误删,当第一个业务执行时间长或者发生阻塞时,锁超时释放,同时另一个线程执行,发现没有锁,获取锁成功,执行自己业务,线程1恢复执行,开始释放锁,然而却释放的线程2的锁,这时线程3进入,获取锁成功,此时2个线程同时操作,出现并发问题。
解决思路:释放锁时判断要释放的锁是否时自己的锁,利用UUID+线程id来确保锁标识,解决锁错误释放
public static final String ID_PRE_FIX = UUID.fastUUID().toString(true) + "-";
@Override
public boolean tryLock(long timeout) {
long threadId = Thread.currentThread().getId();
String value = ID_PRE_FIX + threadId;
Boolean isSuccess = redisTemplate.opsForValue().setIfAbsent(KEY_PRE_FIX + name, value , timeout, TimeUnit.SECONDS);
return BooleanUtil.isTrue(isSuccess);
}
@Override
public void unLock() {
String threadId = ID_PRE_FIX + Thread.currentThread().getId();
String value = redisTemplate.opsForValue().get(KEY_PRE_FIX + name);
if (value.equals(threadId)){
//Todo 当前线程执行完,准备释放锁时发生了阻塞,超时释放了锁,线程2获得锁,同时当前线程阻塞结束继续执行释放锁(释放锁之前已经判断了锁标识),
// 但却释放了线程2的锁,线程2继续执行,此时线程3发现没锁又获取了锁。
//判断锁标识和释放锁是两个操作,不是原子操作
redisTemplate.delete(KEY_PRE_FIX + name);
}
}