文章目录

  • 什么是库存超卖??
  • 下订单基本流程
  • 方式一:限制库存量大于0
  • 方式二:悲观锁
  • 方式三:乐观锁(可用)
  • 方式四:将请求放队列
  • 方式五:通过事务控制超卖
  • 方式六使用redis分布式来解决(悲观锁)
  • 基于redis实现分布式锁
  • 基于redisson开源框架实现
  • redis分布式锁的缺点
  • redis分布式锁优化
  • redis分布式锁优化缺点
  • 方式七使用redis的原子操作解决超卖
  • 共享锁和排他锁
  • 共享锁和排他锁的加锁原则
  • Spring事务-数据库事务-锁的关系
  • 事务的隔离级别是什么??
  • Spring事务原理
  • spiring事务与对象锁
  • spring事务传播属性
  • spring隔离级别


什么是库存超卖??

问题原始描述:两用户查询某商品库存都是1,导致卖出2个商品,产生了超卖问题。
超卖导致的原因有两种:

  1. 不同用户检查库存够用,然后并发下订单,减库存,导致库存减为负数 扣减库存问题
  2. 用户重复下单导致
    第二种解决:在数据库中将用户id和商品id加上唯一索引解决
    第一种解决:


    使用java提供的synchronized或者ReentrantLock将{检查库存,创建订单,扣减库存,更新缓存数量}这几个步骤锁住,就可以达到要求。

发生的问题:增加多台机器后,订单模块集群部署,则java提供的原生锁机制失效了

解决:在整个系统提供一个全局的,唯一的获取锁的东西,每个系统需要获取锁时,都从这里获取

Java商品自动下架解决方案 java商品超卖_redis


一般电商网站都会增加团购,秒杀,特价之类的活动。而这样的活动最大的特点就是访问量激增,上千甚至数万人抢购一个商品。

但是事务是控制超卖的必要条件,而不是充分条件。

下订单基本流程

doOrder(Model model,user user,goodsId)

  1. 判断用户是否登陆,是否有收货地址
  2. 判断库存是否够用
  3. 判断是否已经秒杀到了,防止重复下单
  4. 减库存,创建订单
    多线程并发问题:多线程同时读到有库存,从而进行下单带来的线程安全问题。首先想到的解决办法就是通过事务来解决,先更新后查询,然后判断是否超库存,若发生超卖则抛出异常,通过spring事务回滚处理。但最后分析了这种方案的缺陷:spirng事务不能严格保证数据一致性和业务逻辑正确性,而且减库存的压力全部落在数据库身上,不能保证高并发。
    spring事务为什么不能保证数据一致性和业务逻辑正确性??
    1.如果事务方法抛异常,此时会回滚数据库操作,但已经执行的其他方法不会回滚,因此无法保证业务逻辑正确性;
  5. 即使事务方法不抛异常,也不能保证数据一致性。因为事务里的数据库操作是整个方法执行结束后才提交到数据库,最后提交到数据库的前后很有可能带来数据一致性问题
    然后想了乐观锁,悲观锁,最后分析都会将压力下层到数据库身上。
    最终我们决定把数据都存到redis,然后尝试了redis分布式锁,发现其并发量并不高,因为redis分布式锁实质是一种分布式悲观锁,它将处理串行化。最终放弃使用,选用redis的原子操作来进行预减库存来解决超卖,实质是使用了一种分布式非阻塞乐观锁,底层是CAS算法

优化思路:减少数据库访问,通过redis预减库存避免线程安全问题,通过rabbitmq实现异步下单,回退库存(解决订单超时问题),通过定时任务检查redis和mysql的数据一致性。
a) 系统初始化,把商品库存数量加载到redsi,并通过定时任务检查redis和mysql的数据一致性
b). 判断用户是否登陆,用户是否有收货地址,用户是否已经秒杀到了(防止重复下单)
c). 通过redis预减库存(原子操作),判断库存是否不足,若不足则直接返回库存不足(实质是将并发控制上移到redis),否则将请求入消息队列并返回秒杀成功
d). 请求出队,创建订单,扣减库存
e) .客户端轮询,判断是否秒杀成功

方式一:限制库存量大于0

分析:可以简单解决,但是不能完全避免。
原因:因为数据库底层的写操作,读操作可以同时进行,innodb引擎默认是【非锁定一致读】,所以当用户a去修改库存的时候,用户b任然可以读取到库存数>0,导致出现超卖情况

方式二:悲观锁

在读库存上加锁,这样用户a读数据库时,用户b就需要等待
当并发量很高时,处理效率大大降低

方式三:乐观锁(可用)

使用数据版本version实现。为数据库表增加一个version字段实现。
读取数据时将version字段一同读出,数据每更新一次对此version值加1。
当我们更新时,判断数据库中的version与自己读到的version是否一致,若相等则进行更新,否则认为是过期数据。
但是数据库的压力太大,应该将秒杀数据存入redis中

分析:高并发场景下,假设库存只有 1件 ,两个请求同时进来,抢购该商品.
数据库层面会限制只有一个用户扣库存成功

乐观锁核心思想:当值和期待值相同时,就认为是正确数据,可以进行更新

方式四:将请求放队列

强行将多线程变成单线程

方式五:通过事务控制超卖

mysql innodb查询的结果是有版本控制的,若其他yoghurt没有commit之前,当前用户查询的结果依旧是旧版本。

beginTranse(开启事务)
 
try{
 
    query('select amount from s_store where postID = 12345');
 
    if(库存 > 0){
 
        //quantity为请求减掉的库存数量
 
        query('update s_store set amount = amount - quantity where postID = 12345');
 
    }
 
}catch( Exception e ){
 
    rollBack(回滚)
 
}
 
commit(提交事务)

分析:

  1. 当多个用户同时进入这个事务,会产生一个共享锁。查询的时候,这三个用户查询的都是一个值,当其他用户没有提交之前,当前用户查询的结果依旧是旧值
  2. 若三个用户同时到达update,会产生一个互斥锁,三个用户一个个的进行更新,导致产生超卖的情况
    改进: 先更新,再查询
beginTranse(开启事务)
 
try{
 
    //quantity为请求减掉的库存数量
    query('update s_store set amount = amount - quantity where postID = 12345');
 
    query('select amount from s_store where postID = 12345');
 
    if(剩余的库存 < 0){
 
       throw new Exception('库存不足');
 
    }
 
}catch(Exception e){
 
    rollBack(回滚)
 
}
 
commit(提交事务)

方式六使用redis分布式来解决(悲观锁)

基于redis实现分布式锁

重要指令:set lock_name unique_value NX PX
解释:
unique_value:表示客户端编码,一定要具有唯一性,在解锁的时候,需要验证value和加锁的一致才允许删除key
解决超时问题:第一个进程加锁释放了,但还未执行业务逻辑,第二个进程就获取了锁
NX:若key不存在就设置成功,若存在就返回true
PX:指定过期时间

基于redisson开源框架实现


RLock lock =redisson.getLock("huaweiLock");
//只有一个客户端可以成功加锁
lock.lock(); 

//执行业务逻辑
检查库存
创建订单
扣减库存
更新redis

//释放锁,其他客户端可以尝试加锁
lock.unlock();

redis分布式锁的缺点

redis分布式锁 锁住的资源是,同一个商品的库存
多用户同时下单的时候,会基于分布式锁串行化处理,导致没法同时处理同一个商品的大量下单的请求

redis分布式锁优化

  • 其实说出来也很简单,相信很多人看过java里的ConcurrentHashMap的源码和底层原理,应该知道里面的核心思路,就是分段加锁
  • Java 8中新增了一个LongAdder类,也是针对Java7以前的AtomicLong进行的优化,解决的是CAS类操作在高并发场景下,使用乐观锁思路,会导致大量线程长时间重复循环。
  • LongAdder中也是采用了类似的分段CAS操作,失败则自动迁移到下一个分段进行CAS的思路
  • 其实这就是分段加锁。你想,假如你现在iphone有1000个库存,那么你完全可以给拆成20个库存段,要是你愿意,可以在数据库的表里建20个库存字段,比如stock_01,stock_02,类似这样的,也可以在redis之类的地方放20个库存key。
  • 总之,就是把你的1000件库存给他拆开,每个库存段是50件库存,比如stock_01对应50件库存,stock_02对应50件库存。
    接着,每秒1000个请求过来了,好!此时其实可以是自己写一个简单的随机算法,每个请求都是随机在20个分段库存里,选择一个进行加锁。
  • bingo!这样就好了,同时可以有最多20个下单请求一起执行,每个下单请求锁了一个库存分段,然后在业务逻辑里面,就对数据库或者是Redis中的那个分段库存进行操作即可,包括查库存
  • 一旦对某个数据做了分段处理之后,有一个坑大家一定要注意:就是如果某个下单请求,咔嚓加锁,然后发现这个分段库存里的库存不足了,此时咋办?
    这时你得自动释放锁,然后立马换下一个分段库存,再次尝试加锁后尝试处理。这个过程一定要实现。

redis分布式锁优化缺点

首先,你得对一个数据分段存储,一个库存字段本来好好的,现在要分为20个分段库存字段;
其次,你在每次处理库存的时候,还得自己写随机算法,随机挑选一个分段来处理;
最后,如果某个分段中的数据不足了,你还得自动切换到下一个分段数据去处理

方式七使用redis的原子操作解决超卖

使用redis.increment的原子操作

/**
     * 扣库存操作,秒杀的处理方案
     * @param orderCode
     * @param skuCode
     * @param num
     * @return
     */
    public boolean subtractStock(String orderCode,String skuCode, Integer num) {
        String key = "shop-product-stock" + skuCode;
        Object value = redis.get(key);
        if (value == null) {
            //前提 提前将商品库存放入缓存 ,如果缓存不存在,视为没有该商品
            return false;
        }
        //先检查 库存是否充足
        Integer stock = (Integer) value;
        if (stock < num) {
            LogUtil.info("库存不足");
            return false;
        } 
       //不可在这里直接操作数据库减库存,否则导致数据不安全,高并发访问情况下,此时可能有其他线程已经将redis的key修改了
       //redis 减少库存,然后才能操作数据库
        Long newStock = redis.increment(key, -num.longValue());
        //库存充足
        if (newStock >= 0) {
            LogUtil.info("成功抢购");
            //TODO 真正扣库存操作 可用MQ 进行 redis 和 mysql 的数据同步,减少响应时间
        } else {
            //库存不足,需要增加刚刚减去的库存
            redis.increment(key, num.longValue());
            LogUtil.info("库存不足,并发");
            return false;
        }
        return true;
    }

共享锁和排他锁

数据库的for update是给数据库上锁用的,可以为数据库中的行上一个排他锁,当一个事务未完成时,其他事务可以读取但是不能写入或更新

共享锁又称为读锁,简称S锁,顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。

排他锁又称为写锁,简称X锁,顾名思义,排他锁就是不能与其他所并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据就行读取和修改。

  1. 对于共享锁大家可能很好理解,就是多个事务只能读数据不能改数据,对于排他锁大家的理解可能就有些差别,我当初就犯了一个错误,以为排他锁锁住一行数据后,其他事务就不能读取和修改该行数据,其实不是这样的。排他锁指的是一个事务在一行数据加上排他锁后,其他事务不能再在其上加其他的锁
  2. mysql InnoDB引擎默认的修改数据语句,update,delete,insert都会自动给涉及到的数据加上排他锁,select语句默认不会加任何锁类型
  3. 如果加排他锁可以使用select …for update语句加共享锁可以使用select … lock in share mode语句。所以加过排他锁的数据行在其他事务种是不能修改数据的,也不能通过for update和lock in share mode锁的方式查询数据,但可以直接通过select …from…查询数据,因为普通查询没有任何锁机制
1.for update 仅适用于InnoDB,并且必须开启事务,在begin与commit之间才生效。
 
2.要测试for update的锁表情况,可以利用MySQL的Command Mode,开启二个视窗来做测试。
 
3.当开启一个事务进行for update的时候,另一个事务也有for update的时候会一直等着,直到第一个事务结束。除非第一个事务commit或者rollback或者断开连接,第二个事务会立马拿到锁进行后面操作。
 
4.如果没查到记录会锁表。表级锁时,不管是否查询到记录,都会锁定表。

共享锁和排他锁的加锁原则

拿MySql的InnoDB引擎来说,对于insert、update、delete等操作。会自动给涉及的数据加排他锁;

对于一般的select语句,InnoDB不会加任何锁,事务可以通过以下语句给显示加共享锁或排他锁。

共享锁:SELECT … LOCK IN SHARE MODE;

排他锁:SELECT … FOR UPDATE;

Spring事务-数据库事务-锁的关系

https://blog.csdn.net/weixin_38070406/article/details/78157603Spring事务本质上使用数据库事务,数据库事务本质上使用数据库锁。
开启spring事务意味着使用数据库锁

事务的隔离级别是什么??

是数据库开发商根据业务逻辑需要定义的一组锁的使用策略

Spring事务原理

  1. spring事务使用AOP拦截注解方法,使用动态代理处理事务方法,捕获处理过程中的异常
  2. spring只有捕获到异常才会终止或回滚(若自己处理里事务没有抛出则不会终止或回滚)
  3. spring事务回滚同一事物中 所有的数据库操作,本质上是数据库进行的操作

spiring事务与对象锁

对象锁可以保证数据一致性和业务逻辑正确性,但不保证并发
spirng事务不能严格保证数据一致性和业务逻辑正确性,但具有较好并发性
spring事务为什么不能保证数据一致性和业务逻辑正确性??
1.如果事务方法抛异常,此时会回滚数据库操作,但已经执行的其他方法不会回滚,因此无法保证业务逻辑正确性;
2. 即使事务方法不抛异常,也不能保证数据一致性。因为事务里的数据库操作是整个方法执行结束后才提交到数据库,最后提交到数据库的前后很有可能带来数据一致性问题

spring事务传播属性

定义存在多个事务,spring应该如何处理这些事务的行为。

spring隔离级别

参考博客