穀粒商城學習筆記,第七天:性能壓測+緩存+分佈式鎖
一、性能壓測
我們希望通過壓測發現其他測試更難發現的錯誤:內存泄漏、併發與同步。
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