多线程 -- 超卖问题(redis分布式锁)

x33g5p2x  于2022-02-12 转载在 Redis  
字(7.8k)|赞(0)|评价(0)|浏览(288)

📒博客首页:崇尚学技术的科班人
🍣今天给大家带来的文章是《多线程 -- 超卖问题(redis分布式锁)》🍣
🍣希望各位小伙伴们能够耐心的读完这篇文章🍣
🙏博主也在学习阶段,如若发现问题,请告知,非常感谢🙏
💗同时也非常感谢各位小伙伴们的支持💗

1、搭建测试环境

下面抽取的是一个SpringBoot项目中的秒杀案例的一部分,synchronized可以解决该超卖问题,但是由于此项目是分布式情景下的,所以最终我们用redis分布式锁解决了超卖问题。

  • 引入的依赖
<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <!--   springBoot整合redis     -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
  • yaml
server:
  servlet-path: /sell # 访问路径前缀
  • KeyUtil
import java.util.Random;

public class KeyUtil {

    public static String getUniqueKey(){
        Random random = new Random();
        Integer a = random.nextInt(900000) + 100000;
        return String.valueOf(a) + System.currentTimeMillis();
    }
}
  • SecKillService
/**
 * @author :小肖
 * @date :Created in 2022/2/10 10:42
 */
public interface SecKillService {

    /**
     * 查询秒杀活动特价商品的信息
     * @param productId
     * @return
     */
    String querySecKillProductInfo(String productId);

    /**
     * 模拟不同用户秒杀同一商品的请求
     * @param productId
     * @return
     */
    void orderProductMockDiffUser(String productId);
}
  • SecKillServiceImpl
import com.xiao.exception.SellException;
import com.xiao.service.SecKillService;
import com.xiao.utils.KeyUtil;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;

/**
 * @author :小肖
 * @date :Created in 2022/2/10 10:43
 */
@Service
public class SecKillServiceImpl implements SecKillService {
    /**
     * 国庆活动,皮蛋粥特价,限量100000份
     */
    static Map<String,Integer> products;
    static Map<String,Integer> stock;
    static Map<String,String> orders;
    static
    {
        /**
         * 模拟多个表,商品信息表,库存表,秒杀成功订单表
         */
        products = new HashMap<>();
        stock = new HashMap<>();
        orders = new HashMap<>();
        products.put("123456", 100000);
        stock.put("123456", 100000);
    }

    private String queryMap(String productId)
    {
        return "国庆活动,皮蛋粥特价,限量份"
                + products.get(productId)
                +" 还剩:" + stock.get(productId)+" 份"
                +" 该商品成功下单用户数目:"
                +  orders.size() +" 人" ;
    }

    @Override
    public String querySecKillProductInfo(String productId)
    {
        return this.queryMap(productId);
    }

    @Override
    public synchronized void orderProductMockDiffUser(String productId)
    {
        //1.查询该商品库存,为0则活动结束。
        int stockNum = stock.get(productId);
        if(stockNum == 0) {
            throw new SellException(100,"活动结束");
        }else {
            //2.下单(模拟不同用户openid不同)
            orders.put(KeyUtil.getUniqueKey(),productId);
            //3.减库存
            stockNum =stockNum-1;
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            stock.put(productId,stockNum);
        }
    }
}
  • SecKillController
import com.xiao.service.SecKillService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author :小肖
 * @date :Created in 2022/2/10 10:46
 */

@RestController
@RequestMapping("/skill")
@Slf4j
public class SecKillController {

    @Autowired
    private SecKillService secKillService;

    /**
     * 查询秒杀活动特价商品的信息
     * @param productId
     * @return
     */
    @GetMapping("/query/{productId}")
    public String query(@PathVariable String productId)throws Exception
    {
        return secKillService.querySecKillProductInfo(productId);
    }

    /**
     * 秒杀,没有抢到获得"哎呦喂,xxxxx",抢到了会返回剩余的库存量
     * @param productId
     * @return
     * @throws Exception
     */
    @GetMapping("/order/{productId}")
    public String skill(@PathVariable String productId)throws Exception
    {
        log.info("@skill request, productId:" + productId);
        secKillService.orderProductMockDiffUser(productId);
        return secKillService.querySecKillProductInfo(productId);
    }
}

以上是我们测试的时候所需要使用到的代码,请学习的小伙伴保持一致。

2、测试结果

(1)、并发测试

  • 我们使用postman来模拟高并发进行测试,以上表示的是我们的请求总数是2000,并发量是20

(2)、测试结果

  • 由上图可见我们超卖了12
  • postman并发测试并不明显,但超卖现象已经反映出来了。

(3)、简要分析

  • 由于我们大量的请求来自浏览器,那么有的时候请求同时会从数据库中获取到相同的数据,所以此时得到的是没有减少库存后的结果数据。所以会导致超卖问题。

3、在方法上加 synchronized 测试

@Override
    public synchronized void orderProductMockDiffUser(String productId)
    {
        //1.查询该商品库存,为0则活动结束。
        int stockNum = stock.get(productId);
        if(stockNum == 0) {
            throw new SellException(100,"活动结束");
        }else {
            //2.下单(模拟不同用户openid不同)
            orders.put(KeyUtil.getUniqueKey(),productId);
            //3.减库存
            stockNum =stockNum-1;
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            stock.put(productId,stockNum);
        }
    }

(1)、并发测试

  • 我们使用postman来模拟高并发进行测试,以上表示的是我们的请求总数是500,并发量是20

(2)、测试结果

  • 由上图可见并没有发生超卖。

(3)、简要分析

  1. 虽然加了 synchronized 解决了超卖问题,但是 synchronized 是重量级锁,每次访问秒杀方法的时候都是单线程的形式,系统的性能变得更低了。但是数据更加安全了。
  2. 无法做到细粒度的控制。也就是我们当前商品是高并发的,但是当商品并不是处于高并发状态的(也就是每秒请求数量很少的时候),那么它会对其他的商品一视同仁,无法做到细粒度的控制。
  3. 只适合单点的情况。由于我们这个项目是分布式情景下的,所以此种方法肯定行不通。

4、利用 redis 加分布式锁

(1)、修改后的代码

  • yaml
spring:
  redis:
    host: 192.168.123.135
    port: 6379  # 由于没有设置密码,所以不需要进行设置
    
server:
  servlet-path: /sell # 访问路径前缀
  • RedisLock
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

/**
 * redis分布式锁
 * @author :小肖
 * @date :Created in 2022/2/10 13:28
 */
@Component
@Slf4j
public class RedisLock {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 加锁
     * @param key
     * @param value
     * @return
     */
    public boolean lock(String key,String value){
        if(redisTemplate.opsForValue().setIfAbsent(key,value)){
            // 加锁成功
            return true;
        }

        // 如果锁过期
        String currentValue = redisTemplate.opsForValue().get(key);
        if(!StringUtils.isEmpty(currentValue) &&
                Long.parseLong(currentValue) < System.currentTimeMillis()){

            String oldValue = redisTemplate.opsForValue().getAndSet(key,value);
            if(!StringUtils.isEmpty(oldValue) &&
                    oldValue.equals(oldValue)){
                return true;
            }
        }

        // 锁已经被其它线程获取
        return false;
    }

    /**
     * 解锁
     * @param key
     * @param value
     */
    public void unlock(String key,String value){
        try{
            String currentValue = redisTemplate.opsForValue().get(key);
            if(!StringUtils.isEmpty(currentValue) &&
                    currentValue.equals(value)){
                redisTemplate.opsForValue().getOperations().delete(key);
            }
        }catch (Exception e){
            log.error("【redis 分布式锁】 解锁异常,{}",e);
        }
    }
}
  • SecKillServiceImpl
import com.xiao.exception.SellException;
import com.xiao.service.RedisLock;
import com.xiao.service.SecKillService;
import com.xiao.utils.KeyUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;

/**
 * @author :小肖
 * @date :Created in 2022/2/10 10:43
 */
@Service
public class SecKillServiceImpl implements SecKillService {

    private static final int TIMEOUT = 10 * 1000;

    @Autowired
    private RedisLock redisLock;

    /**
     * 国庆活动,皮蛋粥特价,限量100000份
     */
    static Map<String,Integer> products;
    static Map<String,Integer> stock;
    static Map<String,String> orders;
    static
    {
        /**
         * 模拟多个表,商品信息表,库存表,秒杀成功订单表
         */
        products = new HashMap<>();
        stock = new HashMap<>();
        orders = new HashMap<>();
        products.put("123456", 100000);
        stock.put("123456", 100000);
    }

    private String queryMap(String productId)
    {
        return "国庆活动,皮蛋粥特价,限量份"
                + products.get(productId)
                +" 还剩:" + stock.get(productId)+" 份"
                +" 该商品成功下单用户数目:"
                +  orders.size() +" 人" ;
    }

    @Override
    public String querySecKillProductInfo(String productId)
    {
        return this.queryMap(productId);
    }

    @Override
    public void orderProductMockDiffUser(String productId)
    {
        // 加锁
        long time = System.currentTimeMillis() + TIMEOUT;
        if(!redisLock.lock(productId,String.valueOf(time))){
            throw new SellException(101,"哎呦喂,人也太多了吧,请耐心排队等待~~");
        }

        //1.查询该商品库存,为0则活动结束。
        int stockNum = stock.get(productId);
        if(stockNum == 0) {
            throw new SellException(100,"活动结束");
        }else {
            //2.下单(模拟不同用户openid不同)
            orders.put(KeyUtil.getUniqueKey(),productId);
            //3.减库存
            stockNum =stockNum-1;
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            stock.put(productId,stockNum);
        }

        // 解锁
        redisLock.unlock(productId,String.valueOf(time));
    }
}

(2)、测试结果

  • 由上图可见并没有发生超卖。

(3)、简要分析

  • 其实分布式锁最为关键的就是RedisLock这个类了。
  • 首先就是上锁方法,它利用了redis中的setnx方法,从而我们以商品Id作为key,以时间作为value,如果没有设置该key的话,那么我们就可以调用此方法从而实现加锁。如果该锁已被其它线程占用的话,那么我们就会抛出异常。同时还有一个过期时间的判断,如果没有这个判断的话当我们处理其中的业务逻辑抛出异常之后会导致系统死锁。所以这个过期时间的判断是十分必要的。
  • 其次就是解锁方法,解锁的话其实就是将对应redis中的key删除即可。

相关文章

微信公众号

最新文章

更多