Java Web(1)高并發業務

 2023-10-21 阅读 24 评论 0

摘要:  互聯網無時無刻不面對著高并發問題,例如商品秒殺、微信群搶紅包、大麥網搶演唱會門票等。   當一個Web系統,在一秒內收到數以萬計甚至更多的請求時,系統的優化和穩定是至關重要的。   互聯網的開發包括Java后臺、NoSQL、數據庫、限流、CDN、負載

  互聯網無時無刻不面對著高并發問題,例如商品秒殺、微信群搶紅包、大麥網搶演唱會門票等。

  當一個Web系統,在一秒內收到數以萬計甚至更多的請求時,系統的優化和穩定是至關重要的。

  互聯網的開發包括Java后臺、NoSQL、數據庫、限流、CDN、負載均衡等。

  一、互聯系統應用架構基礎分析

  

  防火墻的功能是防止互聯網上的病毒和其他攻擊,正常的請求通過防火墻后,最先到達的就是負載均衡器。

  負載均衡器的主要功能:

  • 對業務請求做初步的分析,決定分不分發請求到Web服務器,常見的分發軟件比如Nginx和Apache等反向代理服務器,它們在關卡處可以通過配置禁止一些無效的請求,比如封禁經常作弊的IP地址,也可以使用Lua、C語言聯合 NoSQL 緩存技術進行業務分析,這樣就可以初步分析業務,決定是否需要分發到服務器
  • 提供路由算法,它可以提供一些負載均衡算法,根據各個服務器的負載能力進行合理分發,每一個Web服務器得到比較均衡的請求,從而降低單個服務器的壓力,提高系統的響應能力。
  • 限流,對于一些高并發時刻,如雙十一,需要通過限流來處理,因為可能某個時刻通過上述的算法讓有效請求過多到達服務器,使得一些Web服務器或者數據庫服務器產生宕機。當某臺機器宕機后,會使得其他服務器承受更大的請求量,這樣就容易產生多臺服務器連續宕機的可能性,持續下去就會引發服務器雪崩。因此,在這種情況下,負載均衡器有限流的算法,對于請求過多的時刻,可以告知用戶系統繁忙,稍后再試,從而保證系統持續可用。

  為了應對復雜的業務,可以把業務存儲在 NoSQL 上,通過C語言或者Lua語言進行邏輯判斷,它們的性能比Web服務器判斷的性能要快速得多,從而降低Web服務器的壓力,提高互聯網系統的響應速度。

  

  二、應對無效請求

  

  在負載均衡器轉發給Web服務器之前,使用C語言和Redis進行判斷是否是無效請求。對于黃牛組織,可以考慮僵尸賬號排除法進行應對。

  

  三、系統設計

  高并發系統往往需要分布式的系統分攤請求的壓力,要盡量根據 Web 服務器的性能進行均衡分配請求。  

  劃分系統可以按照業務劃分,即水平劃分。也可以不按照業務分,即垂直劃分。

  按照業務劃分可以提高開發效率以及更方便地設計數據庫。但是,還要通過RPC(Remote Procedure Call Protocol)遠程過程調用協議處理這些信息,例如Dubbo、Thrift和Hessian等。

  

  四、數據庫設計

  為了得到高性能,可以使用分表或分庫技術,從而提高系統的響應能力。

  分表是指在一個數據庫內本來一張表可以保存的數據,設計成多張表去保存。例如,將每一年的交易記錄分別分成交易表,而不是只有一張交易表來記錄所有的交易記錄。

  分庫是把表數據分配在不同的數據庫中,分庫首先需要一個路由算法確定數據在哪個數據庫上,然后才能進行查詢,這里可以把用戶和對應業務的數據庫的信息緩存到Redis中,這樣路由算法就可以通過Redis從讀取的數據來決定使用哪個數據庫進行查詢了。

  另外,還可以考慮SQL優化,建立索引等優化,提高數據庫的性能。

?

  五、動靜分離技術

  

  對于互聯網而言,大部分數據都是靜態數據,只有少數使用動態數據,動態數據的數據包很小,不會造成網絡瓶頸,而靜態的數據則不一樣,靜態數據包含圖片、CSS、JavaScript 和視頻等互聯網的應用,尤其是圖片和視頻占據的流量很大,如果都從動態服務器(比如Tomcat、WildFly和WebLogic等)獲取,那么動態服務器的帶寬壓力會很大,這個時候應該考慮動靜分離技術。

  可以使用靜態HTTP服務器,例如Apache,將靜態數據分離到靜態HTTP服務器上,這樣圖片、HTML、腳本等資源都可以從靜態服務器上獲取,盡量使用 Cookie 等技術,讓客戶端緩存能夠緩存數據,避免多次請求,降低服務器的壓力。

  企業可以還可以使用高級的動靜分離技術,例如CDN(Content Delivery Network,即內容分發網絡),它允許企業將自己的靜態數據緩存到網絡 CDN 的節點中,對于用戶的請求,直接通過CDN算法去指定的CDN節點去響應請求。

  

  六、鎖和高并發

  無論區分有效請求和無效請求、水平劃分還是垂直劃分、動靜分離技術,還是數據庫分表、分庫技術,都無法避免動態數據,而動態數據的請求最終也會落在一臺 Web 服務器上。

  例如,發放一個總額為 20 萬元的紅包,拆分成 2 萬個金額為 10 元 的小紅包,供給網站的 3 萬個會員在線搶奪,這就是一個典型的高并發的場景。

  由于會出現多個線程同時發起請求,由于線程每一步完成的順序不一樣,這樣會導致數據的一致性問題。

  為了保證數據一致性,可以使用加鎖的方式,但是加鎖會影響并發,從而影響系統的性能,而不加鎖就難以保證數據的一致性,這就是鎖和高并發的矛盾。

  

  為了解決鎖和高并發的矛盾,大部分企業提出了悲觀鎖和樂觀鎖的概念,

  • 對于數據庫而言,如果在短時間內需要執行大量SQL,對于服務器的壓力可想而知,需要優化數據庫的表設計、索引、SQL語句等。
  • 還可以使用 Redis 事務和 Lua 語言所提供的原子性來取代現有的數據庫技術,從而提高數據的存儲響應,以應對高并發場景,但是嚴格來說也屬于樂觀鎖。

  0.T_RED_PACKET為紅包表,T_USER_RED_PACKET為用戶搶紅包表

  (0)未加鎖情況下,并發導致了數據的不一致

    <!-- 查詢紅包具體信息 --><select id="getRedPacket" parameterType="long"resultType="com.ssm.chapter22.pojo.RedPacket">select id, user_id as userId, amount, send_date assendDate, total,unit_amount as unitAmount, stock, version, note fromT_RED_PACKETwhere id = #{id}</select>

  業務邏輯:

    @Override@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)public int grapRedPacket(Long redPacketId, Long userId) {// 獲取紅包信息// RedPacket redPacket = redPacketDao.getRedPacket(redPacketId);// 悲觀鎖RedPacket redPacket = redPacketDao.getRedPacketForUpdate(redPacketId);// 當前小紅包庫存大于0if (redPacket.getStock() > 0) {redPacketDao.decreaseRedPacket(redPacketId);// 生成搶紅包信息UserRedPacket userRedPacket = new UserRedPacket();userRedPacket.setRedPacketId(redPacketId);userRedPacket.setUserId(userId);userRedPacket.setAmount(redPacket.getUnitAmount());userRedPacket.setNote("搶紅包 " + redPacketId);// 插入搶紅包信息int result = userRedPacketDao.grapRedPacket(userRedPacket);return result;}// 失敗返回return FAILED;}

  

  1.使用數據庫的悲觀鎖和樂觀鎖進行設計

  (1)悲觀鎖

  悲觀鎖是一種利用數據庫內部機制提供的鎖的方法,也就是對更新的數據加鎖,這樣在并發期間一旦有一個事務持有了數據庫記錄的鎖,其他的線程將不能再對數據進行更新了。

  修改SQL語句,加入“for update”,意味著將持有對數據庫記錄的行更新鎖(因為這里使用主鍵查詢,所以只會對行加鎖。如果使用的是非主鍵查詢,要考慮是否對全表加鎖的問題,加鎖后可能引發其他查詢的阻塞),那就意味著在高并發的場景下,當一條事務持有了這個更新鎖后才能往下操作,其他的線程如果要更新這條記錄都需要等待,這樣就不會出現超發現象引發的數據不一致的問題了。

  但是,悲觀鎖會導致系統性能下降。對于悲觀鎖來說,當一條線程搶占了資源后,其他的線程將得不到資源,那么這個時候,CPU 就會將這些得不到資源的線程掛起,掛起的線程也會消耗 CPU 的資源。由于高并發環境下的頻繁掛起線程和恢復線程,導致CPU頻繁切換線程上下文,從而使CPU資源得到了極大的消耗,造成了性能不佳的問題。悲觀鎖也稱獨占鎖。

  業務邏輯和不加鎖時一致。

    <!-- 查詢紅包具體信息 --><select id="getRedPacketForUpdate" parameterType="long"resultType="com.ssm.chapter22.pojo.RedPacket">select id, user_id as userId, amount, send_date assendDate, total,unit_amount as unitAmount, stock, version, notefromT_RED_PACKET where id = #{id} for update</select>

  (2)樂觀鎖

  樂觀鎖是一種不會阻塞其他線程并發的機制,它不會使用數據庫的鎖進行實現,它的設計里面由于不阻塞其他線程,所以不會引發線程頻繁掛起和恢復,這樣可以提高并發能力。樂觀鎖也稱為非阻塞鎖。樂觀鎖使用的是CAS原理。

  

  CAS原理并不排斥并發,也不獨占資源,只是在線程開始階段就讀入線程共享數據,保存為舊值。當處理完邏輯,需要更新數據的時候,會進行一次比較,即比較各個線程當前共享的數據是否和舊值保持一致,如果一致,就開始更新數據,如果不一致,就認為該數據已經被其他線程修改了,那么就不再更新數據,可以考慮重試或者放棄。有時候可以重試,這樣就是一個可重入鎖。

  CAS原理存在ABA問題,ABA問題是因為業務邏輯存在回退的可能性。如果加入一個非業務邏輯的屬性,比如在一個數據中加入版本號,每次修改變量的數據時,強制版本號只能遞增,而不會回退,即使是其他業務數據回退,它也會遞增,那么就解決了ABA問題。

  對于查詢來說,沒有“for update”語句,避免了鎖的發生,就不會造成線程阻塞。然后,增加了對版本號的判斷,其次每次扣減都會對版本號加1,這樣就可以避免ABA問題了。

    <!-- 通過版本號扣減搶紅包 每更新一次,版本增1, 其次增加對版本號的判斷 --><update id="decreaseRedPacketForVersion">update T_RED_PACKETset stock = stock - 1,version = version + 1where id = #{id}and version = #{version}</update>

  在Service中使用樂觀鎖,無重入的代碼:其中,redPacket先獲取到了紅包的記錄,其redPacket.getVersion()表示的就是version版本值,在執行更新數據庫方法時,將redPacket.getVersion()傳入作為version變量,由于在判斷時增加了“and version = #{version}”語句,因此,如果不相等,就不執行update SQL語句,因此update返回值就為0,表明版本號發生了變化。

    // 樂觀鎖,無重入
    @Override@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)public int grapRedPacketForVersion(Long redPacketId, Long userId) {// 獲取紅包信息,注意version值RedPacket redPacket = redPacketDao.getRedPacket(redPacketId);// 當前小紅包庫存大于0if (redPacket.getStock() > 0) {// 再次傳入線程保存的version舊值給SQL判斷,是否有其他線程修改過數據int update = redPacketDao.decreaseRedPacketForVersion(redPacketId, redPacket.getVersion());// 如果沒有數據更新,則說明其他線程已經修改過數據,本次搶紅包失敗if (update == 0) {return FAILED;}// 生成搶紅包信息UserRedPacket userRedPacket = new UserRedPacket();userRedPacket.setRedPacketId(redPacketId);userRedPacket.setUserId(userId);userRedPacket.setAmount(redPacket.getUnitAmount());userRedPacket.setNote("搶紅包 " + redPacketId);// 插入搶紅包信息int result = userRedPacketDao.grapRedPacket(userRedPacket);return result;}// 失敗返回return FAILED;}

  在實際測試的情況下,經過3萬次的搶奪,原來的2萬個紅包中,由于版本號version不一致導致了還有8千多個沒有被搶到,也就是說,搶紅包失敗的記錄太大了。

  (3)樂觀鎖重入機制

  為了克服這個問題,提高成功率,還會考慮使用鎖重入機制。也就是一旦因為版本原因沒有搶到紅包,則重新嘗試搶紅包,但是過多的重入會造成大量的SQL執行,有兩種方式:

  • 按時間戳重入,也就是在一定的時間內(例如:當前時間+100毫秒),不成功的會循環到成功為止,直至超過時間戳,不成功才會退出,返回失敗。
  • 按次數重入,比如限定3次,如果超過3次嘗試后還失敗,那么就判定此次失敗

  按時間戳重入:有時候時間戳并不是那么穩定,也會隨著系統的空閑或者繁忙導致重試次數不一。

    // 樂觀鎖,按時間戳重入
    @Override@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)public int grapRedPacketForVersion(Long redPacketId, Long userId) {// 記錄開始時間long start = System.currentTimeMillis();// 無線循環,直到成功或者滿100毫秒退出while (true) {// 獲取循環當前時間long end = System.currentTimeMillis();// 如果超過了100毫秒就結束嘗試if (end - start > 100) {return FAILED;}
... 和樂觀鎖部分一樣
}

  按次數重入:限制重試次數,這樣就能避免過多的重試導致過多的SQL被執行,從而保證數據庫的性能。

    // 樂觀鎖,按重試次數重入
    @Override@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)public int grapRedPacketForVersion(Long redPacketId, Long userId) {for (int i = 0; i < 3; i++) {// 獲取紅包信息,主要是version值RedPacket redPacket = redPacketDao.getRedPacket(redPacketId);// 當前小紅包庫存大于0if (redPacket.getStock() > 0) {// 再次傳入線程保存的version舊值給SQL判斷,是否有其他線程修改過數據int update = redPacketDao.decreaseRedPacketForVersion(redPacketId, redPacket.getVersion());// 如果沒有數據更新,則說明其他線程已經修改過數據,則重新搶奪if (update == 0) {continue;}
...
}

  

  2.使用Redis進行設計

  數據庫最終會將數據保存到磁盤中,而Redis使用的是內存,內存的速度比磁盤速度快得多。

  Redis的Lua語言是原子性的,且功能更為強大,因此優先選擇Lua語言實現搶紅包業務。

  Redis并非是一個長久存儲數據的地方,它存儲的數據是非嚴格和安全的環境,更多的時候只是為了提供更為快速的緩存,因此當紅包金額為0或者紅包超時的時候,將紅包數據保存到數據庫中,這樣才能夠保證數據的安全性和嚴格性。  

  

  (0)表設計

  使用下面的Redis命令在Redis中初始化了一個編號為5的大紅包,其中庫存為2萬個,每個10元

127.0.0.1:6379> hset red_packet_5 stock 20000
(integer) 1
127.0.0.1:6379> hset red_packet_5 unit_amount 10
(integer) 1

  在數據庫中通過下面的SQL語句創建用戶搶紅包信息表:

create table T_USER_RED_PACKET 
(id                   int(12)                        not null auto_increment,red_packet_id        int(12)                        not null,user_id              int(12)                        not null,amount               decimal(16,2)                  not null,grab_time            timestamp                      not null,note                 varchar(256)                   null,primary key clustered (id)
);

  (1)Lua腳本設計

    // Lua腳本String script = "local listKey = 'red_packet_list_'..KEYS[1] \n"   // 被搶紅包列表 key+ "local redPacket = 'red_packet_'..KEYS[1] \n"    // 當前被搶紅包 key+ "local stock = tonumber(redis.call('hget', redPacket, 'stock')) \n" // 讀取當前紅包庫存+ "if stock <= 0 then return 0 end \n"  // 沒有庫存,返回0+ "stock = stock -1 \n"           // 庫存減1+ "redis.call('hset', redPacket, 'stock', tostring(stock)) \n"  // 保存當前庫存+ "redis.call('rpush', listKey, ARGV[1]) \n"     // 往Redis鏈表中加入當前紅包信息+ "if stock == 0 then return 2 end \n"     // 如果是最后一個紅包,則返回2,表示搶紅包已經結束,需要將Redis列表中的數據保存到數據庫中+ "return 1 \n";    // 如果并非最后一個紅包,則返回1,表示搶紅包成功。

  當返回為2的時候,說明紅包已經沒有庫存,會觸發數據庫對鏈表數據的保存,這是一個大數據量的保存,因為有20000條記錄。為了不影響最后一次搶紅包的響應,在實際的操作中往往會考慮使用 JMS 消息發送到別的服務器進行操作。這里只是創建了一條新的線程去運行保存 Redis 鏈表數據到數據庫的程序。

  保存 Redis 搶紅包信息到數據庫的服務類:

package com.ssm.chapter22.service.impl;
...
@Service
public class RedisRedPacketServiceImpl implements RedisRedPacketService {private static final String PREFIX = "red_packet_list_";// 每次取出1000條,避免一次取出消耗太多內存private static final int TIME_SIZE = 1000;@Autowiredprivate RedisTemplate redisTemplate = null; // RedisTemplate
@Autowiredprivate DataSource dataSource = null; // 數據源
@Override// 開啟新線程運行
    @Asyncpublic void saveUserRedPacketByRedis(Long redPacketId, Double unitAmount) {System.err.println("開始保存數據");Long start = System.currentTimeMillis();// 獲取列表操作對象BoundListOperations ops = redisTemplate.boundListOps(PREFIX + redPacketId);Long size = ops.size();Long times = size % TIME_SIZE == 0 ? size / TIME_SIZE : size / TIME_SIZE + 1;int count = 0;List<UserRedPacket> userRedPacketList = new ArrayList<UserRedPacket>(TIME_SIZE);for (int i = 0; i < times; i++) {// 獲取至多TIME_SIZE個搶紅包信息List userIdList = null;if (i == 0) {userIdList = ops.range(i * TIME_SIZE, (i + 1) * TIME_SIZE);} else {userIdList = ops.range(i * TIME_SIZE + 1, (i + 1) * TIME_SIZE);}userRedPacketList.clear();// 保存紅包信息for (int j = 0; j < userIdList.size(); j++) {String args = userIdList.get(j).toString();String[] arr = args.split("-");String userIdStr = arr[0];String timeStr = arr[1];Long userId = Long.parseLong(userIdStr);Long time = Long.parseLong(timeStr);// 生成搶紅包信息UserRedPacket userRedPacket = new UserRedPacket();userRedPacket.setRedPacketId(redPacketId);userRedPacket.setUserId(userId);userRedPacket.setAmount(unitAmount);userRedPacket.setGrabTime(new Timestamp(time));userRedPacket.setNote("搶紅包 " + redPacketId);userRedPacketList.add(userRedPacket);}// 插入搶紅包信息count += executeBatch(userRedPacketList);}// 刪除Redis列表redisTemplate.delete(PREFIX + redPacketId);Long end = System.currentTimeMillis();System.err.println("保存數據結束,耗時" + (end - start) + "毫秒,共" + count + "條記錄被保存。");}/*** 使用JDBC批量處理Redis緩存數據.* * @param userRedPacketList 搶紅包列表* @return 搶紅包插入數量.*/private int executeBatch(List<UserRedPacket> userRedPacketList) {Connection conn = null;Statement stmt = null;int[] count = null;try {conn = dataSource.getConnection();conn.setAutoCommit(false);stmt = conn.createStatement();for (UserRedPacket userRedPacket : userRedPacketList) {String sql1 = "update T_RED_PACKET set stock = stock-1 where id=" + userRedPacket.getRedPacketId();DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");String sql2 = "insert into T_USER_RED_PACKET(red_packet_id, user_id, " + "amount, grab_time, note)"+ " values (" + userRedPacket.getRedPacketId() + ", " + userRedPacket.getUserId() + ", "+ userRedPacket.getAmount() + "," + "'" + df.format(userRedPacket.getGrabTime()) + "'," + "'"+ userRedPacket.getNote() + "')";stmt.addBatch(sql1);stmt.addBatch(sql2);}// 執行批量count = stmt.executeBatch();// 提交事務
            conn.commit();} catch (SQLException e) {/********* 錯誤處理邏輯 ********/throw new RuntimeException("搶紅包批量執行程序錯誤");} finally {try {if (conn != null && !conn.isClosed()) {conn.close();}} catch (SQLException e) {e.printStackTrace();}}// 返回插入搶紅包數據記錄return count.length / 2;}
}

  這里的@Async注解表示讓Spring自動創建另外一條線程去運行它,這樣它便不在搶最后一個紅包的線程之內,因為這個方法是一個較長時間的方法,如果在同一個線程內,那么對于最后搶紅包的用戶來說就需要等待相當長的時間,影響用戶體驗。

  為了使用@Async注解,還需要在 Spring 中配置一個任務池:

package com.ssm.chapter22.config;
...
@EnableAsync
public class WebConfig extends AsyncConfigurerSupport { ...@Overridepublic Executor getAsyncExecutor() {ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();taskExecutor.setCorePoolSize(5);taskExecutor.setMaxPoolSize(10);taskExecutor.setQueueCapacity(200);taskExecutor.initialize();return taskExecutor;}
}

  用戶搶紅包邏輯:grapRedPacketByRedis接收的參數,第一個是大紅包名稱“red_packet_5”中的5,而userId是jsp文件中發起搶紅包請求的唯一標識[0,30000]中的某一個i值。

  當[0,30000]中的某一個值i發起請求后,假設 i 為 1000

  • jsp中直接異步post到url: "./userRedPacket/grapRedPacketByRedis.do?redPacketId=8&userId=" + i,然后調用grapRedPacketByRedis(Long redPacketId, Long userId)方法,其中redPacketId=5,而userId=1000。
  • 然后通過Object res = jedis.evalsha(sha1, 1, redPacketId + "", args);執行自定義的Lua腳本,其中,1表示key的個數,args表示grapRedPacketByRedis方法的兩個參數值。定義用戶搶紅包信息在Redis中的鍵listKey=red_packet_list_5,大紅包在Redis中的鍵red_packet_5,然后查詢red_packet_5中的stock,返回還剩紅包的數量stock,此時假設stock=7777,則由于還有紅包,于是將stock變為7776,并更新到red_packet_5的stock中,然后將鍵值對red_packet_list_5-ARGV[1](也就是userId),即red_packet_list_5 - 1000寫入Redis中。
  • 當返回結果為 2 時,說明最后一個紅包已經被搶了,這個時候,jedis.hget("red_packet_" + redPacketId, "unit_amount");得到red_packet_5 → unit_amount 即單個小紅包的金額10賦給變量unitAmount,然后saveUserRedPacketByRedis(redPacketId, unitAmount);方法將Redis中鍵red_packet_list的信息加上每個小紅包金額信息,以及其他各種信息對應成用戶搶紅包數據庫表定義,另開一個線程,將20000個數據庫記錄添加到數據庫中保存起來。
    @Autowiredprivate RedisTemplate redisTemplate = null;@Autowiredprivate RedisRedPacketService redisRedPacketService = null;// Lua腳本String script = "local listKey = 'red_packet_list_'..KEYS[1] \n" + "local redPacket = 'red_packet_'..KEYS[1] \n"+ "local stock = tonumber(redis.call('hget', redPacket, 'stock')) \n" + "if stock <= 0 then return 0 end \n"+ "stock = stock -1 \n" + "redis.call('hset', redPacket, 'stock', tostring(stock)) \n"+ "redis.call('rpush', listKey, ARGV[1]) \n" + "if stock == 0 then return 2 end \n" + "return 1 \n";// 在緩存LUA腳本后,使用該變量保存Redis返回的32位的SHA1編碼,使用它去執行緩存的LUA腳本[加入這句話]String sha1 = null;@Overridepublic Long grapRedPacketByRedis(Long redPacketId, Long userId) {// 當前搶紅包用戶和日期信息String args = userId + "-" + System.currentTimeMillis();Long result = null;// 獲取底層Redis操作對象Jedis jedis = (Jedis) redisTemplate.getConnectionFactory().getConnection().getNativeConnection();try {// 如果腳本沒有加載過,那么進行加載,這樣就會返回一個sha1編碼if (sha1 == null) {sha1 = jedis.scriptLoad(script);}// 執行腳本,返回結果Object res = jedis.evalsha(sha1, 1, redPacketId + "", args);result = (Long) res;// 返回2時為最后一個紅包,此時將搶紅包信息通過異步保存到數據庫中if (result == 2) {// 獲取單個小紅包金額String unitAmountStr = jedis.hget("red_packet_" + redPacketId, "unit_amount");// 觸發保存數據庫操作Double unitAmount = Double.parseDouble(unitAmountStr);System.err.println("thread_name = " + Thread.currentThread().getName());redisRedPacketService.saveUserRedPacketByRedis(redPacketId, unitAmount);}} finally {// 確保jedis順利關閉if (jedis != null && jedis.isConnected()) {jedis.close();}}return result;}

?  控制器方法:

    @RequestMapping(value = "/grapRedPacketByRedis")@ResponseBodypublic Map<String, Object> grapRedPacketByRedis(Long redPacketId, Long userId) {Map<String, Object> resultMap = new HashMap<String, Object>();Long result = userRedPacketService.grapRedPacketByRedis(redPacketId, userId);boolean flag = result > 0;resultMap.put("result", flag);resultMap.put("message", flag ? "搶紅包成功": "搶紅包失敗");return resultMap;}

  模擬高并發的jsp文件,其中,由于post是異步請求,所以可以模擬多個用戶同時請求的情況:

<%@ page language="java" contentType="text/html; charset=UTF-8"pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>參數</title>
<!-- 加載Query文件-->
<script type="text/javascript"src="https://code.jquery.com/jquery-3.2.0.js"></script>
<script type="text/javascript">$(document).ready(function () {//模擬30000個異步請求,進行并發var max = 30000;for (var i = 1; i <= max; i++) {//jQuery的post請求,請注意這是異步請求
                    $.post({//請求搶id為1的紅包//根據自己請求修改對應的url和大紅包編號url: "./userRedPacket/grapRedPacketByRedis.do?redPacketId=8&userId=" + i,//成功后的方法
                        success: function (result) {}});}});</script>
</head>
<body>
</body>
</html>

  最后結果,在3萬用戶同時爭搶2萬個紅包的場景下,Redis 只需要4秒,樂觀鎖需要33秒,而悲觀鎖需要54秒,可見Redis是多么的高效。

  使用Redis的風險在于Redis的不穩定性,因為其事務和存儲都存在不穩定的因素,所以更多的時候,應該使用獨立的Redis服務器做高并發業務,一方面可以提高Redis的性能,另一個方面即使在高并發的場合,Redis服務器宕機也不會影響現有的其他業務,同時也可以使用備機等設備提高系統的高可用,保證網絡的安全穩定。

?

  

轉載于:https://www.cnblogs.com/BigJunOba/p/9795169.html

版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。

原文链接:https://hbdhgg.com/2/154848.html

发表评论:

本站为非赢利网站,部分文章来源或改编自互联网及其他公众平台,主要目的在于分享信息,版权归原作者所有,内容仅供读者参考,如有侵权请联系我们删除!

Copyright © 2022 匯編語言學習筆記 Inc. 保留所有权利。

底部版权信息