一、超卖问题
乐观锁解决超卖问题
1、 版本号法
2、CAS法
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private ISeckillVoucherService seckillVoucherService;
@Override
public Result seckillVoucher(Long voucherId) {
//1.查询优惠券信息
SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
//2.判断秒杀是否开始
if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
return Result.fail("秒杀尚未开始");
}
//3.判断秒杀是否结束
if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
return Result.fail("秒杀已经结束");
}
//4.判断库存是否充足
if (seckillVoucher.getStock() < 1) {
return Result.fail("库存不足");
}
//5.扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")//set stock = stock - 1
.eq("voucher_id", voucherId).eq("stock",seckillVoucher.getStock())//where id = ? and stock = ?
.update();
if (!success) {
return Result.fail("库存不足");
}
//6.创建订单
VoucherOrder voucherOrder = new VoucherOrder();
//6.1订单id
voucherOrder.setId(Long.valueOf(RandomUtil.randomNumbers(10)));
//6.2用户id
Long id = UserHolder.getUser().getId();
voucherOrder.setUserId(id);
//6.3代金券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
//7.返回订单id
return Result.ok();
}
}
没有超卖,为什么失败太多?
原因:库存充足时,只要改了stock就是取消。所以只要stock > 0就行
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private ISeckillVoucherService seckillVoucherService;
@Override
public Result seckillVoucher(Long voucherId) {
//1.查询优惠券信息
SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
//2.判断秒杀是否开始
if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
return Result.fail("秒杀尚未开始");
}
//3.判断秒杀是否结束
if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
return Result.fail("秒杀已经结束");
}
//4.判断库存是否充足
if (seckillVoucher.getStock() < 1) {
return Result.fail("库存不足");
}
//5.扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")//set stock = stock - 1
.eq("voucher_id", voucherId).gt("stock",0)//where id = ? and stock > 0
.update();
if (!success) {
return Result.fail("库存不足");
}
//6.创建订单
VoucherOrder voucherOrder = new VoucherOrder();
//6.1订单id
voucherOrder.setId(Long.valueOf(RandomUtil.randomNumbers(10)));
//6.2用户id
Long id = UserHolder.getUser().getId();
voucherOrder.setUserId(id);
//6.3代金券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
//7.返回订单id
return Result.ok();
}
}
二、一人一单
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private ISeckillVoucherService seckillVoucherService;
@Override
public Result seckillVoucher(Long voucherId) {
//1.查询优惠券信息
SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
//2.判断秒杀是否开始
if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
return Result.fail("秒杀尚未开始");
}
//3.判断秒杀是否结束
if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
return Result.fail("秒杀已经结束");
}
//4.判断库存是否充足
if (seckillVoucher.getStock() < 1) {
return Result.fail("库存不足");
}
//5.一人一单
Long userId = UserHolder.getUser().getId();
//5.1判断是否存在订单
Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
//5.2判断是否存在
if (count > 0) {
//用户已经购买过
return Result.fail("用户已经购买一次");
}
//6.扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")//set stock = stock - 1
.eq("voucher_id", voucherId).gt("stock",0)//where id = ? and stock > 0
.update();
if (!success) {
return Result.fail("库存不足");
}
//7.创建订单
VoucherOrder voucherOrder = new VoucherOrder();
//7.1订单id
voucherOrder.setId(Long.valueOf(RandomUtil.randomNumbers(10)));
//7.2用户id
Long id = UserHolder.getUser().getId();
voucherOrder.setUserId(id);
//7.3代金券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
//8.返回订单id
return Result.ok();
}
}
200个订单,为什么异常95%(为什么有10个订单)?
原因:多个线程穿插,并发情况(插入操作,不能用乐观锁),只能用悲观锁
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private ISeckillVoucherService seckillVoucherService;
@Override
public Result seckillVoucher(Long voucherId) {
//1.查询优惠券信息
SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
//2.判断秒杀是否开始
if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
return Result.fail("秒杀尚未开始");
}
//3.判断秒杀是否结束
if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
return Result.fail("秒杀已经结束");
}
//4.判断库存是否充足
if (seckillVoucher.getStock() < 1) {
return Result.fail("库存不足");
}
Long userId = UserHolder.getUser().getId();
synchronized (userId.toString().intern()) {//锁代码块需要共用同一把锁,所以做字符串转成同一对象
//获取代理对象
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
}
}
//在这个方法上加synchronized,它的范围是整个方法,锁的对象是this(当前类),意味着所有多线程对象都会加锁
// ,且锁的对象都为同一个,会串行执行,前面的锁不释放,就会一直等待,影响性能
@Transactional
public Result createVoucherOrder(Long voucherId) {
//5.一人一单
Long userId = UserHolder.getUser().getId();
//5.1判断是否存在订单
Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
//5.2判断是否存在
if (count > 0) {
//用户已经购买过
return Result.fail("用户已经购买一次");
}
//6.扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")//set stock = stock - 1
.eq("voucher_id", voucherId).gt("stock",0)//where id = ? and stock > 0
.update();
if (!success) {
return Result.fail("库存不足");
}
//7.创建订单
VoucherOrder voucherOrder = new VoucherOrder();
//7.1订单id
voucherOrder.setId(Long.valueOf(RandomUtil.randomNumbers(10)));
//7.2用户id
Long id = UserHolder.getUser().getId();
voucherOrder.setUserId(id);
//7.3代金券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
//8.返回订单id
return Result.ok();
}
}
总结:
1、synchronized锁不要加在createVoucherOrder方法上,虽然满足要求,但会影响性能。因为它的范围是整个方法,锁的对象是this(当前类),意味着所有多线程对象都会加锁,且锁的对象都为同一个,会串行执行,前面的锁不释放,就会一直等待,影响性能。
2、synchronized代码块不要写在createVoucherOrder方法内部,虽然满足要求,但也会造成并发问题。因为方法上加了@Transactional,在内部加锁会造成事务没有提交就有订单,也会造成并发问题。
3、写synchronized代码块时,括号里面参数要唯一。因为锁代码块需要共用同一把锁,所以做字符串转成同一对象。
4、要调用有@Transactional的方法时,必须要用代理对象调用。因为@Transactional底层是代理对象,而一般调用方法是this调用。
集群模式下的并发安全问题
原因:多个jvm没有使用同一把锁
解决方法:分布式锁,Redis锁,Redssion
总结:
1、Redis分布式锁可以用命令SETNX lock thread1和命令expire lock 10,但要保证是一个事务,所以用命令SET lock thread1 NX EX 10。
2、Redis分布式锁要设置过期时间,防止服务器宕机后死锁,且锁是非阻塞式,保证死锁后线程不会一直等待释放锁。
1、tryLock和unLock工具类方法
public class SimpleRedisLock {
private StringRedisTemplate stringRedisTemplate;
private String name;
private static final String key_prefix = "lock:";
public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
this.stringRedisTemplate = stringRedisTemplate;
this.name = name;
}
public boolean tryLock(long timeoutSec){
long value = Thread.currentThread().getId();
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(key_prefix + name, value + "", timeoutSec, TimeUnit.SECONDS);
//自动拆箱,可能有安全问题,万一是null
return Boolean.TRUE.equals(success);
}
public void unlock(){
stringRedisTemplate.delete(key_prefix + name);
}
}
2、Redis分布式锁实现(部分逻辑)
Long userId = UserHolder.getUser().getId();
//创建锁对象
SimpleRedisLock lock = new SimpleRedisLock(stringRedisTemplate, "order" + userId);
//获取锁
boolean isLock = lock.tryLock(1200);
//判断是否获取锁成功
if (!isLock) {
return Result.fail("不允许重复下单");
}
//获取代理对象
try {
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
} finally {
//释放锁
lock.unlock();
}
1、Redis分布式锁误删问题
产生原因:业务执行时间比锁时间长
线程一尝试获取锁,因为第一个来,所以获取锁成功 。因为某种原因业务产生阻塞,超过超时时间,线程一锁被释放。
线程二尝试获取锁成功,执行业务。线程一业务完成,释放锁(del),线程二锁被释放。
线程三尝试获取锁成功,执行业务。
结果:出现并发问题。
解决方法 :判断是否是自己的锁
public class SimpleRedisLock {
@Autowired
private StringRedisTemplate stringRedisTemplate;
private String name;
private static final String key_prefix = "lock:";
private static final String id_prefix = UUID.randomUUID().toString(true) + "-";
public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
this.stringRedisTemplate = stringRedisTemplate;
this.name = name;
}
public boolean tryLock(long timeoutSec){
//获取线程标识
String value = id_prefix + Thread.currentThread().getId();
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(key_prefix + name, value, timeoutSec, TimeUnit.SECONDS);
//自动拆箱,可能有安全问题,万一是null
return Boolean.TRUE.equals(success);
}
public void unlock(){
//获取线程标识
String value = id_prefix + Thread.currentThread().getId();
//获取锁中标识
String id = stringRedisTemplate.opsForValue().get(key_prefix + name);
//判断是否一致
if (id.equals(value)) {
//释放锁
stringRedisTemplate.delete(key_prefix + name);
}
}
}
总结:在获取锁可以用UUID + 线程号表示,防止集群中出现相同标识。
2、Redis锁的原子性问题
产生原因:当将要释放锁时产生阻塞。例如:jvm垃圾回收调用gc时会产生阻塞。
解决方法: Lua脚本
1、在Resource里编写Lua脚本
--比较线程标识和锁中的标识是否一致
if (redis.call('get', KEYS[1]) == ARGV[1]) then
--释放锁
return redis.call('del',KEYS[1])
end
return 0
2、调用Lua脚本
public class SimpleRedisLock {
@Autowired
private StringRedisTemplate stringRedisTemplate;
private String name;
private static final String key_prefix = "lock:";
private static final String id_prefix = UUID.randomUUID().toString(true) + "-";
//读取Lua脚本
private static final DefaultRedisScript<Long> unlock_script;
static {
unlock_script = new DefaultRedisScript<>();
unlock_script.setLocation(new ClassPathResource("unlock.lua"));
unlock_script.setResultType(Long.class);
}
public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
this.stringRedisTemplate = stringRedisTemplate;
this.name = name;
}
public boolean tryLock(long timeoutSec){
//获取线程标识
String value = id_prefix + Thread.currentThread().getId();
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(key_prefix + name, value, timeoutSec, TimeUnit.SECONDS);
//自动拆箱,可能有安全问题,万一是null
return Boolean.TRUE.equals(success);
}
public void unlock(){
//调用Lua脚本
stringRedisTemplate.execute(unlock_script,
Collections.singletonList(key_prefix + name),
id_prefix + Thread.currentThread().getId());
}
}
Redission锁(不用自己写setnx,底层已经封装)
1、引入redission依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>2.9.0</version>
</dependency>
2、配置redission
@Configuration
public class RedissionConfig {
public RedissonClient redissonClient(){
//配置
Config config = new Config();
//useSingleServer()单节点模式
config.useSingleServer().setAddress("redis://127.0.0.1:6379");//.setPassword("")
//创建RedissonClient对象
return Redisson.create(config);
}
}
3、代码
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private RedissonClient redissonClient;
@Override
public Result seckillVoucher(Long voucherId) {
//1.查询优惠券信息
SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
//2.判断秒杀是否开始
if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
return Result.fail("秒杀尚未开始");
}
//3.判断秒杀是否结束
if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
return Result.fail("秒杀已经结束");
}
//4.判断库存是否充足
if (seckillVoucher.getStock() < 1) {
return Result.fail("库存不足");
}
Long userId = UserHolder.getUser().getId();
//创建锁对象
// SimpleRedisLock lock = new SimpleRedisLock(stringRedisTemplate, "order" + userId);
RLock lock = redissonClient.getLock("order" + userId);
//获取锁
// boolean isLock = lock.tryLock(1200);
boolean isLock = lock.tryLock();
//判断是否获取锁成功
if (!isLock) {
return Result.fail("不允许重复下单");
}
//获取代理对象
try {
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
} finally {
//释放锁
lock.unlock();
}
}
//在这个方法上加synchronized,它的范围是整个方法,锁的对象是this(当前类),意味着所有多线程对象都会加锁
// ,且锁的对象都为同一个,会串行执行,前面的锁不释放,就会一直等待,影响性能
@Transactional
public Result createVoucherOrder(Long voucherId) {
//5.一人一单
Long userId = UserHolder.getUser().getId();
//5.1判断是否存在订单
Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
//5.2判断是否存在
if (count > 0) {
//用户已经购买过
return Result.fail("用户已经购买一次");
}
//6.扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")//set stock = stock - 1
.eq("voucher_id", voucherId).gt("stock",0)//where id = ? and stock > 0
.update();
if (!success) {
return Result.fail("库存不足");
}
//7.创建订单
VoucherOrder voucherOrder = new VoucherOrder();
//7.1订单id
voucherOrder.setId(Long.valueOf(RandomUtil.randomNumbers(10)));
//7.2用户id
Long id = UserHolder.getUser().getId();
voucherOrder.setUserId(id);
//7.3代金券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
//8.返回订单id
return Result.ok();
}
}