使用Redis分佈式鎖處理併發,解決超賣問題

1、使用Apache ab模擬併發壓測

一、壓測工具介紹

$ ab -n 100 -c 100 http://www.baidu.com/html

-n表示發出100個請求,-c模擬100個併發,至關是100我的同時訪問。java

還能夠這樣寫:redis

$ ab -t 60 -c 100 http://www.baidu.com/spring

-t表示60秒,-c是100個併發,會在連續60秒內不停的發出請求。數據庫

使用ab工具模擬多線程併發請求,對發出負載的機器要求比較低,既不會佔用不少cpu,也不會佔用不少的內存,所以也是不少DDoS攻擊的必備良藥,不過要慎用,別耗光本身機器的資源。一般來講1000個請求,100個併發算是比較正常的模擬。apache

至於工具的使用,具體見:Apache ab 測試工具使用(一)網絡

下載後,進入support文件夾,執行命令。多線程

二、併發測試

我建立了兩張表,一個商品表,一個訂單記錄表;
而後寫了兩個接口,一個是查詢商品信息,一個是下單秒殺。併發

查詢訂單:app

image_1caapork5q212c91aschplhfop.png-48.5kB

秒殺下單:

image_1caapplln4pqcnk1q4fmv44vj16.png-45.3kB

當我併發測試時:

$ ab -n 500 -c 100 http://localhost:8080/seckill/1/

image_1cabf0e35c361t5qltaqj03au13.png-65.1kB

這TM確定不行啊,這就超賣了,明明沒這麼多商品,結果還賣出去了。。。

2、synchronized處理併發

首先,synchronized的確是一個解決辦法,並且也很簡單,在方法前面加一個synchronized關鍵字。

可是經過壓測,發現請求變的很慢,由於:
synchronized就用一個鎖把這個方法鎖住了,每次訪問這個方法,只會有一個線程,因此這就是它致使慢的緣由。經過這種方式,保證這個方法中的代碼都是單線程來處理,不會出什麼問題。

同時,使用synchronized仍是存在一些問題的,首先,它沒法作到細粒度的控制,好比同一時間有秒殺A商品和B商品的請求,都進入到了這個方法,雖然秒殺A商品的人不少,可是秒殺B商品的人不多,可是即便是買B商品,進入到了這個方法,也會同樣的慢。

最重要的是,它只適合單點的狀況。若是之後程序水平擴展了,弄了個集羣,很顯然,負載均衡以後,不一樣的用戶看到的結果必定是五花八門的。

因此,仍是使用更好的辦法,使用redis分佈式鎖。

3、redis分佈式鎖

一、兩個redis的命令

setnx key value 簡單來講,setnx就是,若是沒有這個key,那麼就set一個key-value, 可是若是這個key已經存在,那麼將不會再次設置,get出來的value仍是最開始set進去的那個value.
網站中還專門講到可使用!SETNX加鎖,若是得到鎖,返回1,若是返回0,那麼該鍵已經被其餘的客戶端鎖定。
而且也提到了如何處理死鎖。

getset key value 這個就更簡單了,先經過key獲取value,而後再將新的value set進去。

二、redis分佈式鎖的實現

咱們但願的,無非就是這一段代碼,可以單線程的去訪問,所以在這段代碼以前給他加鎖,相應的,這段代碼後面要給它解鎖:

image_1cabec77q16dibn41a207mkpb19.png-80.3kB

2.1 引入redis依賴

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

2.2 配置redis

spring:  redis:  host: localhost  port: 6379

2.3 編寫加鎖和解鎖的方法

package com.vito.service; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; /** * Created by VitoYi on 2018/4/5. */ @Component public class RedisLock { Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired private StringRedisTemplate redisTemplate; /** * 加鎖 * @param key 商品id * @param value 當前時間+超時時間 * @return */ public boolean lock(String key, String value) { if (redisTemplate.opsForValue().setIfAbsent(key, value)) { //這個其實就是setnx命令,只不過在java這邊稍有變化,返回的是boolea return true; } //避免死鎖,且只讓一個線程拿到鎖 String currentValue = redisTemplate.opsForValue().get(key); //若是鎖過時了 if (!StringUtils.isEmpty(currentValue) && Long.parseLong(currentValue) < System.currentTimeMillis()) { //獲取上一個鎖的時間 String oldValues = redisTemplate.opsForValue().getAndSet(key, value); /* 只會讓一個線程拿到鎖 若是舊的value和currentValue相等,只會有一個線程達成條件,由於第二個線程拿到的oldValue已經和currentValue不同了 */ if (!StringUtils.isEmpty(oldValues) && oldValues.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) { logger.error("『redis分佈式鎖』解鎖異常,{}", e); } } }

爲何要有避免死鎖的一步呢?
假設沒有『避免死鎖』這一步,結果在執行到下單代碼的時候出了問題,畢竟操做數據庫、網絡、io的時候拋了個異常,這個異常是偶然拋出來的,就那麼偶爾一次,那麼會致使解鎖步驟不去執行,這時候就沒有解鎖,後面的請求進來天然也或得不到鎖,這就被稱之爲死鎖。
而這裏的『避免死鎖』,就是給鎖加了一個過時時間,若是鎖超時了,就返回true,解開以前的那個死鎖。

2.4 下單代碼中引入加鎖和解鎖,確保只有一個線程操做

@Autowired private RedisLock redisLock; @Override @Transactional public String seckill(Integer id)throws RuntimeException { //加鎖 long time = System.currentTimeMillis() + 1000*10; //超時時間:10秒,最好設爲常量 boolean isLock = redisLock.lock(String.valueOf(id), String.valueOf(time)); if(!isLock){ throw new RuntimeException("人太多了,換個姿式再試試~"); } //查庫存 Product product = productMapper.findById(id); if(product.getStock()==0) throw new RuntimeException("已經賣光"); //寫入訂單表 Order order=new Order(); order.setProductId(product.getId()); order.setProductName(product.getName()); orderMapper.add(order); //減庫存 product.setPrice(null); product.setName(null); product.setStock(product.getStock()-1); productMapper.update(product); //解鎖 redisLock.unlock(String.valueOf(id),String.valueOf(time)); return findProductInfo(id); } 

這樣再來跑幾回壓測,就不會超賣了:

image_1cabeppmqfn11gau8gu4gn6a5m.png-56.2kB

相關文章
相關標籤/搜索