测压工具的使用和redis分布式锁

技术文档网 2021-04-25

分布式锁都用在高并发、大流量的场景,之前的Postman模拟用户下单的操作,无法模拟这种情景,所以这里用压测工具来模拟并发。

  • 测压工具:Apache ab
    • 命令:ab -n 100 -c 100 http://www.XXXX.com

      这里-n表示发出100个请求,-c模拟100个线程并发,相当于100个人同时访问,最后是我们要测试的url; 也可以这样写: ab -t 60 -c 100 http://www.XXX.com/

-t表示60秒,-c表示100个并发,它会在连续60秒内不停地发请求。
更详细的命令说明,见官方文档

Apache ab安装方法: 这里有一篇详细的安装方法:https://blog.csdn.net/ahaaaaa/article/details/51514175

注意:

  • 最好解压到磁盘根目录下,我开始解压到Download目录下,运行:httpd.exe命令就出错了。
  • 解压后的根目录下有个说明文档readme_first.html,里面有介绍方式。

下面开始做实验:

  1. 模拟秒杀的业务方法如下:

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

    Controller方法如下:

    @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 用压测工具模拟秒杀

    ab -n 1000 -c 500 http://127.0.0.1:8080/sell/skill/order/123456
    

    查看结果:http://127.0.0.1:8080/sell/skill/query/123456

    国庆活动,皮蛋粥特价,限量份100000 还剩:99992 份 该商品成功下单用户数目:1000 人

出现超卖现象。

  1. 解决方法
    方法1: 在秒杀的业务方法上加synchronized关键字,结果对上了,但是请求慢的像蜗牛。

    • 缺点:无法做到细粒度控制。当有许多商品时,每个商品id不同,但这里对每个商品的抢购都会加锁,假如秒杀A商品的人很多,秒杀B商品的人很少,一旦进入这个方法都会造成一样的慢,这就是说无法做到细粒度的控制。
    • 只适合单点的情况。只能跑在单机上。

    方法2:解决思路,保证下面一段代码单线程访问。

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

    我们要在这段代码之前和之后分别加锁和解锁。

    • 加锁逻辑(很难理解,多看视频,有非常细致的讲解)
    /**
     * 加锁
     * @param key
     * @param value 当前时间+超时时间
     * @return
     */
    public boolean lock(String key, String value) {
        if(redisTemplate.opsForValue().setIfAbsent(key, value)) {
            return true;
        }
        //currentValue=A   这两个线程的value都是B  其中一个线程拿到锁
        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(currentValue)) {
                return true;
            }
        }

        return false;
    }
  • 解锁逻辑

    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);
       }
    }
    

    最终秒杀的业务方法如下: ```java @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.genUniqueKey(),productId);
       //3.减库存
       stockNum =stockNum-1;
       try {
           Thread.sleep(100);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
       stock.put(productId,stockNum);
    

    }

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

}
优点:
1. 支持分布式。
2. 可以更细粒度的控制(秒杀同一种商品才会加锁,而不同的商品可以同时访问这个方法,不会同步)。


[redis官网](https://redis.io/),查看命令含义[try redis](http://try.redis.io/)。
上面主要要到:`setnx``getset`命令。

----------------------------

秒杀实验中,redis分布式锁解决方案的完整代码。

```java
package com.imooc.controller;

import com.imooc.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;

@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);
    }
}
package com.imooc.service.impl;

import com.imooc.exception.SellException;
import com.imooc.service.RedisLock;
import com.imooc.service.SecKillService;
import com.imooc.utils.KeyUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;

@Service
public class SecKillServiceImpl implements SecKillService {

    private static final int TIMEOUT = 10 * 1000; //超时时间 10s

    @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.genUniqueKey(),productId);
            //3.减库存
            stockNum =stockNum-1;
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            stock.put(productId,stockNum);
        }

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

    }
}
package com.imooc.service;

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;


@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;
        }
        //currentValue=A   这两个线程的value都是B  其中一个线程拿到锁
        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(currentValue)) {
                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);
        }
    }

}

相关文章

  1. 硅谷互联网公司的开发流程

    开发流程包括这么几个阶段: OKR 的设立; 主项目及其子项目的确立; 每个子项目的生命周期; 主项目的生命周期; 收尾、维护、复盘。 第一点,OKR 的设立 所有项目的起始,都应该从 Ro

  2. RESTful-表述性状态转移风格

    REST英文全拼:Representational State Transfer 面向资源编程 资源指的就是一类数据 产品表-&gt;就是产品资源 最重要的是如何表示一个资源 地址即

  3. 稳定性思考

    产品功能线 0-1: 当系统从无到有的时候,首要考虑的是研发效率,功能快速迭代,满足快速增长的业务需求 1-10 系统已经搭建起来,此时考虑的是系统的稳定性。 可用性:1.隔离:区分出核心和非核心功能

  4. Supervisor守护队列发邮件

    安装 CentOS: yum -y install supervisor Debien/Ubuntu适用:apt-get install supervisor 配置 修改主配置文件:vim /et

  5. 安装libsodium,让服务器支持chacha20等加密方式

    用chacha20加密方式需要安装libsodium 注意:libsodium从1.0.15开始就废弃了aes-128-ctr yum install wget m2crypto git libsod

随机推荐

  1. 硅谷互联网公司的开发流程

    开发流程包括这么几个阶段: OKR 的设立; 主项目及其子项目的确立; 每个子项目的生命周期; 主项目的生命周期; 收尾、维护、复盘。 第一点,OKR 的设立 所有项目的起始,都应该从 Ro

  2. RESTful-表述性状态转移风格

    REST英文全拼:Representational State Transfer 面向资源编程 资源指的就是一类数据 产品表-&gt;就是产品资源 最重要的是如何表示一个资源 地址即

  3. 稳定性思考

    产品功能线 0-1: 当系统从无到有的时候,首要考虑的是研发效率,功能快速迭代,满足快速增长的业务需求 1-10 系统已经搭建起来,此时考虑的是系统的稳定性。 可用性:1.隔离:区分出核心和非核心功能

  4. Supervisor守护队列发邮件

    安装 CentOS: yum -y install supervisor Debien/Ubuntu适用:apt-get install supervisor 配置 修改主配置文件:vim /et

  5. 安装libsodium,让服务器支持chacha20等加密方式

    用chacha20加密方式需要安装libsodium 注意:libsodium从1.0.15开始就废弃了aes-128-ctr yum install wget m2crypto git libsod