SpringMVC+Redis實現分散式鎖實現秒殺功能
1.實現分散式鎖的幾種方案
1.Redis實現 (推薦)
2.Zookeeper實現
3.資料庫實現
Redis實現分散式鎖
*
* 在叢集等多伺服器中經常使用到同步處理一下業務,這是普通的事務是滿足不了業務需求,需要分散式鎖
*
* 分散式鎖的常用3種實現:
* 0.資料庫樂觀鎖實現
* 1.Redis實現 — 使用redis的setnx()、get()、getset()方法,用於分散式鎖,解決死鎖問題
* 2.Zookeeper實現
* 參考:
* http://www.jb51.net/article/103617.htm
* http://www.hollischuang.com/archives/1716?utm_source=tuicool&utm_medium=referral
* 1、實現原理:
基於zookeeper瞬時有序節點實現的分散式鎖,其主要邏輯如下(該圖來自於IBM網站)。大致思想即為:每個客戶端對某個功能加鎖時,在zookeeper上的與該功能對應的指定節點的目錄下,生成一個唯一的瞬時有序節點。判斷是否獲取鎖的方式很簡單,只需要判斷有序節點中序號最小的一個。當釋放鎖的時候,只需將這個瞬時節點刪除即可。同時,其可以避免服務宕機導致的鎖無法釋放,而產生的死鎖問題。
2、優點
鎖安全性高,zk可持久化
3、缺點
效能開銷比較高。因為其需要動態產生、銷燬瞬時節點來實現鎖功能。
4、實現
可以直接採用zookeeper第三方庫curator即可方便地實現分散式鎖
*
* Redis實現分散式鎖的原理:
* 1.通過setnx(lock_timeout)實現,如果設定了鎖返回1, 已經有值沒有設定成功返回0
* 2.死鎖問題:通過實踐來判斷是否過期,如果已經過期,獲取到過期時間get(lockKey),然後getset(lock_timeout)判斷是否和get相同,
* 相同則證明已經加鎖成功,因為可能導致多執行緒同時執行getset(lock_timeout)方法,這可能導致多執行緒都只需getset後,對於判斷加鎖成功的執行緒,
* 再加expire(lockKey, LOCK_TIMEOUT, TimeUnit.MILLISECONDS)過期時間,防止多個執行緒同時疊加時間,導致鎖時效時間翻倍
* 3.針對叢集伺服器時間不一致問題,可以呼叫redis的time()獲取當前時間
2.Redis分分散式鎖的程式碼實現
1.定義鎖介面
/**
* @Description: Redis分散式鎖介面
* @Author: fxb
* @CreateDate: 2018/3/29 15:43
* @Version: 1.0
*/
public interface RedisLockService {
/**
* 加鎖成功,返回加鎖時間
* @param lockKey
* @param threadName
* @return
*/
public Long lock(String lockKey);
/**
* 解鎖, 需要更新加鎖時間,判斷是否有許可權
* @param lockKey
* @param lockValue
* @param threadName
*/
public void unlock(String lockKey, long lockValue);
/**
* 多伺服器叢集,使用下面的方法,代替System.currentTimeMillis(),獲取redis時間,避免多服務的時間不一致問題!!!
* @return
*/
public long currtTimeForRedis();
}
2、實現鎖實現
package com.jason.mrht.service.websrv.impl;
import com.jason.mrht.service.websrv.RedisLockService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
/**
* @author fxb
* @date 2018/3/29 15:43
*/
@Service
public class RedisLockServiceImpl extends BaseWebSrvService implements RedisLockService {
/**
* 加鎖超時時間,單位毫秒, 即:加鎖時間內執行完操作,如果未完成會有並發現象
*/
private static final long LOCK_TIMEOUT = 5 * 1000;
private static final Logger LOG = LoggerFactory.getLogger(RedisLockServiceImpl.class);
/**
* 取到鎖加鎖 取不到鎖一直等待直到獲得鎖
*/
@Override
public Long lock(final String lockKey) {
LOG.info("開始執行加鎖");
//迴圈獲取鎖
while (true) {
//鎖時間
final Long lock_timeout = System.currentTimeMillis() + LOCK_TIMEOUT + 1;
if (otherCache.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
JdkSerializationRedisSerializer jdkSerializer = new JdkSerializationRedisSerializer();
byte[] value = jdkSerializer.serialize(lock_timeout);
return connection.setNX(lockKey.getBytes(), value);
}
})) {
//如果加鎖成功
LOG.info("加鎖成功++++++++111111111");
//設定超時時間,釋放記憶體
otherCache.expire(lockKey, LOCK_TIMEOUT, TimeUnit.MILLISECONDS);
return lock_timeout;
} else {
// redis裡的時間
Long currt_lock_timeout_Str = (Long) otherCache.opsForValue().get(lockKey);
//鎖已經失效
if (currt_lock_timeout_Str != null && currt_lock_timeout_Str < System.currentTimeMillis()) {
// 判斷是否為空,不為空的情況下,說明已經失效,如果被其他執行緒設定了值,則第二個條件判斷是無法執行
Long old_lock_timeout_Str = (Long) otherCache.opsForValue().getAndSet(lockKey, lock_timeout);
// 獲取上一個鎖到期時間,並設定現在的鎖到期時間
if (old_lock_timeout_Str != null && old_lock_timeout_Str.equals(currt_lock_timeout_Str)) {
// 如過這個時候,多個執行緒恰好都到了這裡,但是隻有一個執行緒的設定值和當前值相同,他才有權利獲取鎖
LOG.info("加鎖成功+++++++2222222222");
//設定超時時間,釋放記憶體
otherCache.expire(lockKey, LOCK_TIMEOUT, TimeUnit.MILLISECONDS);
//返回加鎖時間
return lock_timeout;
}
}
}
try {
LOG.info("等待加鎖,睡眠100毫秒");
//睡眠100毫秒
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
public void unlock(String lockKey, long lockvalue) {
//正常直接刪除 如果異常關閉判斷加鎖會判斷過期時間
LOG.info("執行解鎖==========");
// redis裡的時間
Long currt_lock_timeout_Str = (Long) otherCache.opsForValue().get(lockKey);
//如果是加鎖者 則刪除鎖 如果不是則等待自動過期 重新競爭加鎖
if (currt_lock_timeout_Str != null && currt_lock_timeout_Str == lockvalue) {
//刪除鍵
otherCache.delete(lockKey);
LOG.info("解鎖成功-----------------");
}
}
/**
* 多伺服器叢集,使用下面的方法,代替System.currentTimeMillis(),獲取redis時間,避免多服務的時間不一致問題!!!
*
* @return
*/
@Override
public long currtTimeForRedis() {
return otherCache.execute(new RedisCallback<Long>() {
@Override
public Long doInRedis(RedisConnection redisConnection) throws DataAccessException {
return redisConnection.time();
}
});
}
}
3.分散式鎖驗證
package com.jason.mrht.core.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* 測試模組
* @author bao
*/
@Controller
@RequestMapping("/api/redis")
public class TestController extends BaseController {
/**
* 秒殺商品數量10個
*/
private int goodNum = 10;
private static final String LOCK_NO = "redis_distribution_lock_no_";
private static int i = 0;
private int taskNum = 1000;
/**
* redis分散式鎖測試
* 模擬1000個執行緒同時執行業務,修改資源
*/
@ResponseBody
@RequestMapping(value = "/redisLock", method = RequestMethod.GET)
public String testRedisDistributionLock1() {
/** task(); */
for (int i = 0; i < taskNum; i++) {
new Thread(new Runnable() {
@Override
public void run() {
task();
}
}).start();
}
return "OK";
}
/**
* 建立一個redis分散式鎖任務
*/
private void task() {
//加鎖時間
Long lockTime;
if ((lockTime = serviceTemplate.getRedisLockService().lock((LOCK_NO + 1) + "")) != null) {
//開始執行任務
logger.info("當前庫存的數量:"+goodNum);
if (goodNum == 0) {
logger.info("停止減庫存任務");
} else {
goodNum--;
logger.info("開始減庫存任務");
}
logger.info("任務執行中" + (i++));
// 任務執行完畢 關閉鎖
serviceTemplate.getRedisLockService().unlock((LOCK_NO + 1) + "", lockTime);
}
}
}
4.結果驗證:
在Controller中模擬了1000個執行緒,通過執行緒池方式提交,每次20個執行緒搶佔分散式鎖,搶到分散式鎖的執行程式碼,沒搶到的等待
模擬秒殺10個商品1000個人來爭奪
5、模擬使用者的併發請求
package com.jason.mrht.common.utils;
/**
* @Description: 類作用描述
* @Author: fxb
* @CreateDate: 2018/3/29 18:55
* @Version: 1.0
*/
import com.jason.mrht.common.exception.HttpException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.CountDownLatch;
/**
* 模擬使用者的併發請求,檢測使用者樂觀鎖的效能問題
*
* @author zzg
* @date 2017-02-10
*/
public class ConcurrentTest {
final static SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args){
//模擬10000人併發請求,使用者錢包
CountDownLatch latch=new CountDownLatch(1);
//模擬10000個使用者
for(int i=0;i<10000;i++){
AnalogUser analogUser = new AnalogUser("user"+i,"58899dcd-46b0-4b16-82df-bdfd0d953bfb"+i,"1","20.024",latch);
analogUser.start();
}
//計數器減一 所有執行緒釋放 併發訪問。
latch.countDown();
System.out.println("所有模擬請求結束 at "+sdf.format(new Date()));
}
static class AnalogUser extends Thread{
//模擬使用者姓名
String workerName;
String openId;
String openType;
String amount;
CountDownLatch latch;
public AnalogUser(String workerName, String openId, String openType, String amount,
CountDownLatch latch) {
super();
this.workerName = workerName;
this.openId = openId;
this.openType = openType;
this.amount = amount;
this.latch = latch;
}
@Override
public void run() {
// TODO Auto-generated method stub
try {
latch.await(); //一直阻塞當前執行緒,直到計時器的值為0
} catch (InterruptedException e) {
e.printStackTrace();
}
post();//傳送post 請求
}
public void post(){
String result = "";
System.out.println("模擬使用者: "+workerName+" 開始傳送模擬請求 at "+sdf.format(new Date()));
try {
result = HttpUtil.sendGet("http://localhost:8080/api/collect/distribution/redis/lock1",null);
}catch (HttpException e){
}
//sendPost("http://localhost:8080/Settlement/wallet/walleroptimisticlock.action", "openId="+openId+"&openType="+openType+"&amount="+amount);
System.out.println("操作結果:"+result);
System.out.println("模擬使用者: "+workerName+" 模擬請求結束 at "+sdf.format(new Date()));
}
}
}