這篇博文所列舉的優化手段是針對比較傳統項目,可是想提升系統的吞吐量如今時髦的技術仍是那些先後端未分離, 使用nginx當成靜態資源服務器去代理咱們的靜態資源javascript
當咱們對一個傳統的項目進行壓力測試時,很容器就發現,系統的Throughput被數據庫(mysql)限制的死死的,儘管代碼看起來確實沒毛病,邏輯也沒有錯誤,可是過多的請求都被打向了數據庫,數據庫自個開啓大量的IO操做,這樣大的負載甚至會使Linux系統的總體負載驟然飆升,可是反觀咱們的系統的吞吐量,呵呵...css
既然mysql的抗壓能力限制了咱們的系統,那就將數據緩存起來,盡一切可能減小用戶和數據庫之間的直接接觸的次數,這樣咱們的系統的吞吐量,同一時間能處理器的請求數量天然會升上去html
市面上的緩存技術不少, 比較火爆的是兩款緩存數據庫 Memcache 和 Redis ,前端
Redis 和 Memcahe的區別vue
# 挨個測試redis中的命令 # 每一個數據包大小是3字節 # 100個併發, 發起10萬次請求 redis-benchmark -h 127.0.0.1 -p 6379 -c 100 -n 100000 [root@139 ~]# redis-benchmark -h 127.0.0.1 -p 9997 -c 100 -n 100000 ====== PING_INLINE ====== 100000 requests completed in 1.04 seconds 100 parallel clients 3 bytes payload keep alive: 1 98.68% <= 1 milliseconds // 百分之98.68的請求在1秒內完成了 99.98% <= 2 milliseconds 100.00% <= 2 milliseconds 96525.09 requests per second // 每秒完成的請求數在9萬六左右 -d 指定數據包的大小,看下面redis的性能仍是很強大的 -q 簡化輸出的參數 [root@139 ~]# redis-benchmark -h 127.0.0.1 -p 9997 -q -d 100 -c 100 -n 100000 PING_INLINE: 98619.32 requests per second PING_BULK: 95877.28 requests per second SET: 96153.85 requests per second GET: 95147.48 requests per second INCR: 95238.10 requests per second LPUSH: 95328.88 requests per second RPUSH: 95877.28 requests per second LPOP: 95328.88 requests per second RPOP: 97276.27 requests per second SADD: 96339.12 requests per second HSET: 98231.83 requests per second SPOP: 94607.38 requests per second LPUSH (needed to benchmark LRANGE): 92165.90 requests per second LRANGE_100 (first 100 elements): 97181.73 requests per second LRANGE_300 (first 300 elements): 96153.85 requests per second LRANGE_500 (first 450 elements): 94428.70 requests per second LRANGE_600 (first 600 elements): 95969.28 requests per second MSET (10 keys): 98231.83 requests per second 只測試 指定的命令 -t 跟多個命令參數 [root@139 ~]# redis-benchmark -p 9997 -t set,get -q -n 100000 -c 100 SET: 97276.27 requests per second GET: 98135.42 requests per second
從上面的壓力測試中,能夠看到,Redis的性能是絕對實力, 至關強悍,和mysql相比不是一個量級的, 因此結論很明顯,若是咱們在用戶和mysql中鍵加一層redis作緩存,系統的吞吐量天然會上去java
因而爲了提升系統的抗壓能力,咱們將壓力從mysql逐步轉移到redis中mysql
在說頁面緩存以前,咱們先說一下在一個傳統的項目中,一個請求的生命週期大概是這樣的: 從瀏覽器發出到服務端, 服務端查詢數據庫獲取結果, 再將結果數據傳遞給模板引擎將數據渲染進html頁面jquery
想提升這個過程的速度,咱們能夠這樣搞, 頁面緩存, 顧名思義就是將 html 頁面緩存到緩存數據庫中nginx
示例以下:web
一開始咱們會先嚐試從緩存中獲取出已經渲染好的html源碼響應給客戶端, 響應的格式經過@ResponseBody和produces
中的屬性進行控制,告訴瀏覽器本身會返回給它html文本
優勢: 將用戶的請求的壓力從mysql轉移到redis, 這點強度對redis單機來講根本不是事
缺點: 很明顯,將請求擴大到頁面級別,數據一致性不免會受到影響, 這也是使用頁面緩存不得不考慮的一點
特色1 : 嚴格控制緩存的時間, 必定別忘了添加過時時間...
特色2 : 原來都是讓thymeleaf自動完成數據的渲染,如今的話,很明顯是咱們手動在渲染數據
@RequestMapping(value = "/to_list",produces = "text/html;charset=UTF-8") @ResponseBody public String toLogin(Model model, User user, HttpServletResponse response, HttpServletRequest request) { // 先從redis緩存中獲取數據 String html = redisService.get(GoodsKey.goodsList, "", String.class); if (html != null) return html; // 查詢商品列表 List<GoodsVo> goodsList = goodsService.getGoodsList(); model.addAttribute("goodsList", goodsList); // 使用Thymeleaf模板引擎手動渲染數據 WebContext springWebContext = new WebContext(request,response,request.getServletContext(),request.getLocale(),model.asMap()); String goods_list = thymeleafViewResolver.getTemplateEngine().process("goods_list", springWebContext); // 存入redis if (goods_list!=null){ redisService.set(GoodsKey.goodsList,"",goods_list); } return goods_list; }
既然都說到這裏了, 就接着說還能怎麼玩吧...
你看, 上面經過手動控制模板引擎的api居然獲得的已經渲染好的html源代碼了, 什麼叫作已經渲染好的? 說白了就是原來我在前端寫:th ${user},這樣的佔位符,如今已經被thymeleaf替換成了 張三 ... (說的夠直接吧)
拿到了已經渲染好的源代碼,咱們就能經過IO操做,將這個文件寫到系統的某個目錄上去,不知道你們有沒有發現,去逛京東淘寶瀏覽某個商品頁面時,就會發現url是相似這樣的 www.jjdd.com/aguydg/ahdioa/1235345.html
這個後綴123145.html 大機率說明京東使用靜態頁的技術, 這太明智了,面對如此巨大數量的商品信息後綴用數字來表示也不錯,並且速度還快不是?
怎麼實現這種效果呢?
就是上面說的,經過IO將這些源碼的數據寫到Linux中的某一個目錄下面, 文件名就是上面URL中的最後的數字, 經過Nginx作靜態資源服務器將這些xxx.html代理起來, 用戶再訪問的話就走這個靜態頁, 一樣不會接觸數據庫, 並且nginx還支持零拷貝,併發數5萬不是事...
還有,後綴數組最好也別亂寫,直接使用商品id會更好,畢竟是先點擊商品獲取到id,再進入到靜態頁
緩存java中的對象, 好比將用戶的信息持久化進redis, 每次用戶查詢本身的信息先從redis中查詢,有的話直接返回,沒有的話再去查詢數據庫, 這樣一樣實現了在用戶和數據庫之間多添加出一層緩存,也能夠大大的提升系統的吞吐量
通常會怎麼玩呢?
用戶的請求在查詢數據庫以前先嚐試從redis中獲取對象信息, redis中不存在的話就去數據庫中查詢, 查詢完結果後將這個結果換存進redis
// todo 使用redis作緩存,減小和數據庫的接觸次數 public Label findById(Long labelId) { // 先嚐試從緩存中查詢當前對象 Label label = (Label) redisTemplate.opsForValue().get("label_id" + labelId); if (label==null){ Optional<Label> byId = labelRepository.findById(labelId); if (!byId.isPresent()) { // todo 異常 } label = byId.get(); // 將查出的結果存進緩存中 redisTemplate.opsForValue().set("label_id"+label.getId(),label); } return label; }
當用戶update數據 ,先更新數據庫,再刪除/更新redis中響應的緩存
public void update(Long labelId, Label label) { label.setId(labelId); Label save = labelRepository.save(label); // todo 數據庫修改爲功後, 將緩存刪除 redisTemplate.delete("label_id"+save.getId()); }
當用戶刪除數據,先刪除數據庫中的數據,再刪除redis中的緩存
public void delete(Long labelId) { labelRepository.deleteById(labelId); // todo 數據庫修改爲功後, 將緩存刪除 redisTemplate.delete("label_id"+labelId); }
你們都在說頁面靜態化, 它真的有那麼神奇嗎? 其實也沒有那麼神奇, 說白了吧,傳統的網頁上的數據是經過模板引擎渲染上去的,(好比JSP或者是說thymeleaf這類模板引擎), 作了靜態化的網頁中數據的渲染經過js完成, 並且這個網頁和項目中的 靜態資源好比js,css 這類的文件放在一個目錄下面, 地位和普通的靜態資源相同, 還有個好處就是, 瀏覽器給的福利, 由於瀏覽器對靜態資源是有緩存的, 若是你善於觀察,就會發現有時候重複請求某個網頁,網頁正常顯示,可是狀態碼是304... 請求依然會到達服務端,可是服務端會告訴瀏覽器它想訪問的頁面其實沒有變化, 因而瀏覽器就找到本地的緩存使用
時下國內 最火爆的玩靜態頁面時下最火爆的技術就是 Angular.js 以及 Vue.js , 也確實好用, 前幾個月我寫過有關vue的筆記, 感興趣的同窗能夠去看看 點擊查看個人vue筆記
在本篇博客中偏偏好沒用到VUE, 可是實現靜態頁的思路和vue是大差不差的, 一樣是經過js代碼實現頁面的靜態化
先後端分離嘛, 天然是json交互,後端經過@ResponseBody
控制返回給前端json對象, 並且, 推薦你們也整一個VO對象,用這個VO對象將各式各樣的數據封裝在一塊兒,一次性返回給前端, 這樣看上去,後端確實是簡單,也就是返回一個json對象
第一件事就是將html文件從template文件夾下move到static文件夾下面, 讓這個html文件和js/css文件稱兄道弟
而後是給這個xxx.html該名字, 爲啥要更名換目錄呢? 由於SpringBoot是約定大於編碼的, 在什麼目錄下面就是什麼文件, 此外Thymeleaf部分默認的配置信息以下
@ConfigurationProperties(prefix = "spring.thymeleaf") public class ThymeleafProperties { private static final Charset DEFAULT_ENCODING = StandardCharsets.UTF_8; public static final String DEFAULT_PREFIX = "classpath:/templates/"; public static final String DEFAULT_SUFFIX = ".html";
沒辦法,這些配置信息默認就認爲類路徑下的templates中都是xxx.html的文件
第二件事是將html標籤中引入的相似thymeleaf這中命名空間都去掉,靜態頁不須要
<!DOCTYPE HTML> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>商品列表</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <!-- jquery --> <!--<script type="text/javascript" th:src="@{/js/jquery.min.js}"></script>--> <script type="text/javascript" th:src="js/jquery.min.js"></script>
第三件事寫一個ajax,當頁面一加載就觸發向後臺發請求,獲取數據, 經過jQuery操做各個節點完成數據的渲染
三步打完收工, 看上也不是很複雜, 可是咱們的頁面就是已然成爲了靜態頁面, 今後她會被瀏覽器緩存起來,只要這個頁面不發生變更,瀏覽器就會一直使用本身的緩存,在網絡上的數據傳輸量有多帶勁本身腦補,系統RT絕對飆升幾個數量級
說一下市面上常見的靜態資源優化技術吧:
另外當多個用戶併發修改庫存時,居然將庫存修改爲了負數, 自己使用的數據庫引擎是 innodb是存在行級鎖的, 咱們只要修改一下咱們的sql就行 加條件 and stock_number > 0
爲了防止同一個用戶發送兩次請求,偶爾秒殺到多個商品的狀況,咱們去miaosha_user表中去創建一個惟一的索引,將userId創建惟一索引,不容許相同的userId出現兩次,從而避免上述的狀況
讓用戶去輸入驗證碼的好處有不少, 除了驗證用戶的身份信息以外, 最明顯的好處就是分散用戶對系統的壓力, 前端若是不添加圖片驗證碼的可能在1s內系統須要承載1萬併發, 可是添加了圖片驗證碼, 就能將這1萬併發分散到10秒之內,甚至更多
總體的思路:
圖片驗證碼不過是個image, 因此說前端想展現它,確定須要一個img標籤, 一個比較很差想的地方是啥呢? 就是這個圖片的路徑,src=啥的問題, 咱們能夠怎麼作呢? 能夠直接經過這個往src中寫入後端的生成imge的Controller的路徑, 每次刷新頁面,它就會往這個路徑中發起請求, Controller中去生成一個image, 經過HttpServletResponse的獲取到輸出流, 將生成的圖片用流的發送會瀏覽器,再加上他是img標籤,這不就ok了?
百度一下如何生成圖片驗證碼一類的技術,確實真的不少,我就不貼代碼了, 感興趣的同窗自行百度,代碼一片片的
由於這種圖片是靜態資源,若是你不由用緩存,這個圖片就會被緩存下來, 要想每次點擊圖片都實現更換驗證碼的話,參考下面的js實現, 添加時間戳
function refreshImageCode() { $("#verifyCodeImg").attr("src","/path/verifyCode?goodsId="+$("#goodsId").val()+"×tamp="+new Date()); }
舉個例子: 如說咱們想限制在一分鐘內單個用戶訪問 A Controller中的a方法的次數不能超過30次, 這其實就是一種接口限流的需求, 能夠有效的防止用戶的惡意訪問
其實這件事結合緩存來實現並不是是一件難事,好比我就用上面的例子: 不是想對a方法進行限流嗎? 咱們就在a方法中添加下面的邏輯
僞代碼以下:
public void a(User user){ // 校驗user合法性 // 限流 Integer count = redis.get(user.getId()); if(count!=null&&count>15) return ; // 到達了指定的闋值,直接返回不容許繼續訪問 if(count==null){ redis.set(user.getId(),30,1); // 表示當前用戶訪問了1次, 當前key的有效時間爲30s }else{ redis.incr(user.getId()); } }
咱們可使用攔截器技術, 若是咱們的重寫了攔截器的preHandler()
方法,它就會在執行Controller中的方法前進行回調, 再配合自定義註解技術, 後面簡直就是爲因此爲...
示例:
@Component public class AccessIntercepter extends HandlerInterceptorAdapter { // 在方法執行以前進行攔截 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (handler instanceof HandlerMethod){ HandlerMethod hd = (HandlerMethod) handler; LimitAccess methodAnnotation = hd.getMethodAnnotation(LimitAccess.class); if (methodAnnotation==null) return true; // 解析註解 int maxCount = methodAnnotation.maxCount(); boolean needLogin = methodAnnotation.needLogin(); int second = methodAnnotation.second(); // todo } return true; } }
結語: 最近又到考試周了,今個週六,下週三考試運籌學... 但願本身能平安度過...
我是bloger 賜我白日夢, 歡迎點贊支持