穀粒商城學習筆記,第七天:性能壓測+緩存+分佈式鎖

穀粒商城學習筆記,第七天:性能壓測+緩存+分佈式鎖

一、性能壓測

我們希望通過壓測發現其他測試更難發現的錯誤:內存泄漏、併發與同步

1、性能指標

吞吐量、響應時間QPS TPS、錯誤率

RT:Response Time 響應時間

HPS:hits per second  每秒點擊次數

TPS:Transaction per second 系統每秒處理交易數

QPS:query per second  每秒處理查詢次數

2、JMeter

下載地址

2.1、運行

bin目錄下的ApacheJMeter.jar 或者jmeter.bat

2.2、步驟

創建線程組

測試計劃 > 添加 > 線程 > 線程組

創建請求:

測試線程組 > 添加 > 取樣器 > HTTP請求、JDBC請求等

查看請求結果:

測試線程組 > 添加  >  監聽器  >  查看結果樹、彙總報告、聚合報告

2.3、異常

測試本地服務時有時會出現如下異常

java.net.BindException:Address already in use : connect

windows本身提供的端口訪問機制的問題。

Windows提供TCP/IP鏈接的端口爲1024-5000,並且要四分鐘來循環回收他們。就導致我們在短時間內跑大量的請求時將端口沾滿

解決:

1、cmd中,用regedit命令打開註冊表

2、在 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters

3、右鍵Parameters
4、添加新的DWORD,名字爲MaxUserPort和TcpTimedWaitDelay

5、分別輸入數值數據爲65534和30,基數選擇十進制;以增大可分配的tcp連接端口數、減小處於TIME_WAIT狀態的連接的生存時間

6、修改配置完畢之後記得重啓機器纔會生效

2.4、影響因素

##影響性能考慮點:
數據庫、應用程序、中間件(TOMCAT NGINX MQ)等、網絡帶寬和操作系統等

##考慮自己的應用數據CPU密集型 還是IO密集型

3、JConsole 和 JvisualVM

//TODO 之後在JVM虛擬機 總結

jconsole

jvisualvm

二、緩存

那些數據適合放入緩存:

1、即時性、數據一致性要求不高的
2、訪問量大且更新頻率不高的數據(讀多、寫少)

1、整合redis

1.1、導入依賴:

<!--reids-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

1.2、配置:

spring:  
  # redis 配置
  redis:
    # 地址
    host: localhost
    # 端口,默認爲6379
    port: 6379
    # 密碼
    password: admin123
    ##foobared
    # 連接超時時間
    timeout: 10s
    lettuce:
      pool:
        # 連接池中的最小空閒連接
        min-idle: 0
        # 連接池中的最大空閒連接
        max-idle: 8
        # 連接池的最大數據庫連接數
        max-active: 8
        # #連接池最大阻塞等待時間(使用負值表示沒有限制)
        max-wait: -1ms

1.3、使用:

private RedisTemplate redisTemplate;
或者
private StringRedisTemplate redisTemplate;

@Test
public void testRedisConnect(){
    ValueOperations<String, String> ops = redisTemplate.opsForValue();
    ops.set("hello","world_"+ UUID.randomUUID().toString());

    String hello = ops.get("hello");
    System.out.println(hello);
}

1.4、異常:OutofDirectNemoryError

堆外內存溢出:
springboot2.o以後默認使用lettuce作爲操作redis的客戶端。它使用netty進行網絡通信。

lettuce的bug導致netty堆外內存溢出
netty如果沒有指定堆外內存,默認使用應用配置的-Xmx300ml 
也可以通過-Dio.netty.maxDirectMemory進行設置

解決方案:
不能使用-Dio.netty.maxDirectNemory只去調大堆外內存。
1)、升級Lettuce客戶端。2)、切換使用jedis

【推薦使用jedis】

1.5、重新引入redis

<!--reids-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <!--排除lettuce-->
    <exclusions>
        <exclusion>
            <groupId>io.lettuce</groupId>
            <artifactId>lettuce-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>

<!--引入jedis-->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>

1.6、說明

//lettuce和jedis都是操作redis的底層客戶端
//springboot對lettuce和jedis進行了再次封裝,成了RedisTemplate
//springboot導入了LettuceConnectionConfiguration和JedisConnectionConfiguration


@EnableConfigurationProperties({RedisProperties.class})
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration {

}

2、緩存穿透

查詢一個不存在的數據

緩存穿透是指緩存和數據庫中都沒有的數據,而用戶惡意不斷髮起請求,如發起爲id爲“-1”的數據或id爲特別大不存在的數據。

這時的用戶很可能是攻擊者,攻擊會導致數據庫壓力過大。

解決方案:

1>、接口層增加校驗,如用戶鑑權校驗,id做基礎校驗,id小於等於0的直接攔截
                                      
2>、NULL結果緩存,並加入短暫的過期時間【推薦】
從緩存取不到的數據,在數據庫中也沒有取到,這時也可以將key-value對寫爲key-null,
緩存有效時間可以設置短點,如30秒(設置太長會導致正常情況也沒法使用)。
這樣可以防止攻擊用戶反覆用同一個id暴力攻擊

3、緩存雪崩

緩存大面積失效

緩存雪崩是指我們設置緩存是KEY採用了相同的過期時間,導致緩存在某一時刻同時失效。
請求全部轉發到了DB,DB瞬時壓力過重雪崩。

解決方案:

1>、緩存數據的過期時間設置隨機,防止同一時間大量數據過期現象發生。【推薦】
在原有的失效時間基礎上增加一個隨機值,比如1-5分鐘隨機,這樣每一個緩存的過期時間的重複率
就會降低,就很難應發集體失效的事件

2>、如果緩存數據庫是分佈式部署,將熱點數據均勻分佈在不同的緩存數據庫中。

3>、設置熱點數據永遠不過期。

4、緩存擊穿

查詢一個存在的數據,但是緩存中沒有

緩存擊穿是指緩存中沒有但數據庫中有的數據(一般是緩存時間到期),這時由於併發用戶特別多,
同時讀緩存沒讀到數據,又同時去數據庫去取數據,引起數據庫壓力瞬間增大,造成過大壓力

解決方案:

1>、設置熱點數據永遠不過期。

2>、加互斥鎖【推薦】
大量併發只讓一個去查,其他人等待,查到以後釋放鎖,其他人獲取到鎖,先插緩存,就會有數據,不用去DB了

實例:

public static String getData(String key){
   String result;
    try{
        //從緩存讀取數據
        result = getDataFromRedis(key);
        //緩存中不存在數據
        if(result == null){
            //去獲取鎖,獲取成功,去數據庫取數據
            if(reenLock.tryLock()){
                //去獲取鎖,獲取成功,去DB取數據
                result = getDataFromDB(key);
                //更新緩存數據
                if(result != null){
                    setDataToRedis(key,result);
                }
                //釋放鎖
                reenLock.unlock();
            }else{
                //獲取鎖失敗
                //贊同100ms再去重新獲取數據
                Thread.sleep(100);
                result = getData(key);
            }
        }
    }catch(Exception e){
        //發生異常後釋放鎖
        lock.unlock;
    }
    return result;
}

三、分佈式鎖

1、分佈式鎖原理與使用

1.1、思考問題

1>、setnx佔好了位,業務代碼異常或者程序在業務過程中黨紀。沒有執行刪除鎖邏輯,這就造成了死鎖
	解決:設置鎖的自動過期,即使沒有手動刪除,也會自動刪除

2>、setnx設置好,正要去設置過期時間,宕機又死鎖了
	解決:設置過期時間和佔位必須是原子的,redis支持使用setnx ex命令

3>、如果業務時間過長,鎖自動過期了,我們直接刪除,有可能把別人正在持有的鎖刪除了,導致大量線程的進入
	解決:佔鎖的時候,值定位UUID,每一個線程只能匹配到自己的鎖才能刪除

4>、在刪除鎖的過程中,剛根據UUID匹配出自己持有的鎖,鎖自動過期,別人已經設置到了新的鎖值,那麼我們會刪除到別人的鎖
	解決:刪除鎖必須保證原子性,使用redis lua腳本完成

5>、保證加鎖【佔位+過期時間】和刪除鎖【判斷+刪除】的原子性。更難的事情是,鎖的自動續期

1.2、代碼實現

@Test
public void testRedisLock(){
    //創建鎖,去Redis佔位
    String locKValue = UUID.randomUUID().toString();
    //原子操作
    Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", locKValue, 300, TimeUnit.SECONDS);
    if (lock){
        //創建鎖成功:
        //處理業務邏輯
        try{
            System.out.println("----->處理業務邏輯");
        }finally {
            //刪除鎖:原子操作,查詢和刪除一起:LUA腳本
            String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
            //成功1失敗0
            Long lock1 = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), locKValue);
        }
    }else{
        //創建鎖失敗:重新調用自己,嘗試是否可以創建鎖
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        testRedisLock();
    }
}

2、分佈式鎖Redisson

官方地址

2.1、原生redisson使用

導入:

<!--以後使用redisson作爲分佈式鎖,分佈式對象等功能框架-->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.12.0</version>
</dependency>

配置:

@Component
public class RedissonConfig {

    @Bean(destroyMethod="shutdown")
    public RedissonClient redissonClient(){
        //創建配置
        Config config = new Config();
        SingleServerConfig singleServerConfig = config.useSingleServer();
        singleServerConfig.setAddress("redis://60.205.254.48:6380");
        singleServerConfig.setPassword("admin123");
        //創建實例
        RedissonClient redissonClient = Redisson.create(config);
        return redissonClient;
    }
}

使用:

@Autowired
private RedissonClient redissonClient;

@Test
public void testRedisson(){
    System.out.println(redissonClient);
}

2.2、redisson做分佈式鎖

@Autowired
private RedissonClient redissonClient;

@Test
public void testRedissonLock(){
    RLock lock = redissonClient.getLock("my-lock");
    //加鎖:阻塞式等待 且 Redisson會對鎖進行自動續期 
    lock.lock();
    try {
        System.out.println("------處理業務邏輯------");
    }finally {
        //解鎖
        lock.unlock();
    }
}

注意:

lock.lock();是阻塞式等待,默認加鎖時間是30s

Redisson會給鎖自動續期,不用擔心業務時間過長,鎖自動過期的情況

加鎖的業務只要運行完成,Redisson就不會給鎖續期了,即使不手動解鎖,鎖默認在30s後自動過期

但如果使用lock.lock(10,TimeUnit.SECONDS)給鎖設置了過期時間,鎖就不會自動續期了

看門狗原理

2.3、讀寫鎖

說明:寫鎖是一個排它鎖(互斥鎖),讀鎖是一個共享鎖

@RestController
@RequestMapping("/redission")
public class RedissonController {

    @Autowired
    private RedissonClient redissonClient;

    @Autowired
    private StringRedisTemplate redisTemplate;

    //寫鎖
    @GetMapping("/writeLock")
    public String writeLock(){

        RReadWriteLock rReadWriteLock = redissonClient.getReadWriteLock("rw-lock");
        //獲取寫鎖
        RLock rLock = rReadWriteLock.writeLock();
        String s = "";
        try {
            rLock.lock();
            s = UUID.randomUUID().toString();
            Thread.sleep(30000);
            redisTemplate.opsForValue().set("aa",s);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            rLock.unlock();
        }
        return s;
    }

    //讀鎖
    @GetMapping("/readLock")
    public String readLock(){
        RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("rw-lock");
        RLock rLock = readWriteLock.readLock();
        String s = "";
        rLock.lock();
        try {
            s = redisTemplate.opsForValue().get("aa");
        } finally {
            rLock.unlock();
        }
        return s;
    }
}

測試:

##訪問:http://localhost:10000/redission/readLock

##和: http://localhost:10000/redission/writeLock

2.4、閉鎖

@RestController
@RequestMapping("/redission")
public class RedissonController {

    @Autowired
    private RedissonClient redissonClient;

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 閉鎖
     * 學校裏有5個班,所有班裏的人都走了,才能鎖校門
     */
    @GetMapping("/closeDoor")
    public String closeDoor() throws InterruptedException {
        RCountDownLatch countDownLatch = redissonClient.getCountDownLatch("close-door");
        countDownLatch.trySetCount(5);//設置總共有5個班,5個班都走了 才能繼續往下執行
        countDownLatch.await();//等待閉鎖都完成

        return "...所有人都走了,鎖校門";
    }

    @GetMapping("/gogogo/{id}")
    public String gogogo(@PathVariable("id") Integer id){
        RCountDownLatch countDownLatch = redissonClient.getCountDownLatch("close-door");
        countDownLatch.countDown();//計數減一

        return id+"班的人走了";
    }
}

測試:

##訪問
http://localhost:10000/redission/gogogo/1  2   3  4  5

http://localhost:10000/redission/closeDoor

2.5、信號量

@RestController
@RequestMapping("/redission")
public class RedissonController {

    @Autowired
    private RedissonClient redissonClient;

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 信號量
     * 總共有3個車位
     * 三個車位滿了以後,開走一輛車才能再停一輛車
     */
    @GetMapping("/park")
    public String park() throws InterruptedException {
        RSemaphore park = redissonClient.getSemaphore("park");
        park.acquire();//獲取一個信號量,佔取一個車位

        return "...佔取車位SUCCESS";
    }

    @GetMapping("/go")
    public String go(){
        RSemaphore park = redissonClient.getSemaphore("park");
        park.release();//釋放一個信號量,一輛車開走了
        return "一輛車開走了";
    }
}

測試:

##訪問
http://localhost:10000/redission/park

http://localhost:10000/redission/go

四、緩存數據一致性的問題

##一般解決緩存數據一致性的問題,有兩種方案:

#####雙寫模式
		如果修改了數據,數據庫和緩存在同一個方法下同時修改
		問題:產生髒數據,如下圖,請求1比請求2執行的慢,最後會出現數據庫是2緩存是1的狀況

#####失效模式【推薦】
        如果修改了數據,數據庫中的數據修改且同時直接刪除緩存,等讀取數據的時候就會重新添加進緩存了
        問題:產生髒數據,如下圖,請求3在請求2還沒修改完數據庫後就讀取了數據庫,讀到的數據是1,
             等請求2寫完數據庫又刪除了緩存後,請求3將緩存更新爲了數據庫1的內容,最後導致數據庫是2的內容,緩存是1的內容

【解決方案】
	1、修改的時候使用讀寫鎖redissonClient.getReadWriteLock,但是會影響效率
    2、使用阿里的中間件Canel

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章