分佈式場景中確保線程安全的解決方案,redis實現分佈式鎖

實際工做中,常常會遇到多線程併發時的相似搶購的功能,本篇描述一個簡單的redis分佈式鎖實現的多線程搶票功能。java


直接上代碼。首先按照慣例,給出一個錯誤的示範:面試


咱們能夠看看,當20個線程一塊兒來搶10張票的時候,會發生什麼事。redis


package com.tiger.utils;spring

 

public class TestMutilThread {數組

 

// 總票量安全

public static int count = 10;session

 

public static void main(String[] args) {多線程

statrtMulti();併發

}dom

 

public static void statrtMulti() {

for (int i = 1; i <= 20; i++) {

TicketRunnable tickrunner = new TicketRunnable();

Thread thread = new Thread(tickrunner, "Thread No: " + i);

thread.start();

}

 

}

 

public static class TicketRunnable implements Runnable {

 

@Override

public void run() {

System.out.println(Thread.currentThread().getName() + "  start "

+ count);

// TODO Auto-generated method stub

// logger.info(Thread.currentThread().getName()

// + "  really  start" + count);

if (count <= 0) {

System.out.println(Thread.currentThread().getName()

+ "  ticket sold out ! No tickets remained!" + count);

return;

} else {

count = count - 1;

System.out.println(Thread.currentThread().getName()

+ " bought a ticket,now remaining :" + (count));

}

}

}

}

測試結果,從結果能夠看到,票數在不一樣的線程中已經出現混亂。


Thread No: 2  start 10

Thread No: 6  start 10

Thread No: 4  start 10

Thread No: 5  start 10

Thread No: 3  start 10

Thread No: 9  start 6

Thread No: 1  start 10

Thread No: 1 bought a ticket,now remaining :3

Thread No: 9 bought a ticket,now remaining :4

Thread No: 3 bought a ticket,now remaining :5

Thread No: 12  start 3

Thread No: 5 bought a ticket,now remaining :6

Thread No: 4 bought a ticket,now remaining :7

Thread No: 8  start 7

Thread No: 7  start 8

Thread No: 12 bought a ticket,now remaining :1

Thread No: 14  start 0

Thread No: 6 bought a ticket,now remaining :8

Thread No: 16  start 0

Thread No: 2 bought a ticket,now remaining :9

Thread No: 16  ticket sold out ! No tickets remained!0

Thread No: 14  ticket sold out ! No tickets remained!0

Thread No: 18  start 0

Thread No: 18  ticket sold out ! No tickets remained!0

Thread No: 7 bought a ticket,now remaining :0

Thread No: 15  start 0

Thread No: 8 bought a ticket,now remaining :1

Thread No: 13  start 2

Thread No: 19  start 0

Thread No: 11  start 3

Thread No: 11  ticket sold out ! No tickets remained!0

Thread No: 10  start 3

Thread No: 10  ticket sold out ! No tickets remained!0

Thread No: 19  ticket sold out ! No tickets remained!0

Thread No: 13  ticket sold out ! No tickets remained!0

Thread No: 20  start 0

Thread No: 20  ticket sold out ! No tickets remained!0

Thread No: 15  ticket sold out ! No tickets remained!0

Thread No: 17  start 0

Thread No: 17  ticket sold out ! No tickets remained!0

爲了解決多線程時出現的混亂問題,這裏給出真正的測試類!!!


真正的測試類,這裏啓動20個線程,來搶10張票。


RedisTemplate 是用來實現redis操做的,由spring進行集成。這裏是使用到了RedisTemplate,因此我以構造器的形式在外部將RedisTemplate傳入到測試類中。


MultiTestLock 是用來實現加鎖的工具類。


總票數使用volatile關鍵字,實現多線程時變量在系統內存中的可見性,這點能夠去了解下volatile關鍵字的做用。


TicketRunnable用於模擬搶票功能。


其中因爲lock與unlock之間存在if判斷,爲保證線程安全,這裏使用synchronized來保證。


測試類:


package com.tiger.utils;

 

import java.io.Serializable;

 

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.data.redis.core.RedisTemplate;

 

 

public class MultiConsumer {

Logger logger=LoggerFactory.getLogger(MultiTestLock.class);

private RedisTemplate<Serializable, Serializable> redisTemplate;

public MultiTestLock lock;

//總票量

public volatile static int count = 10;

 

public void statrtMulti() {

lock = new MultiTestLock(redisTemplate);

for (int i = 1; i <= 20; i++) {

TicketRunnable tickrunner = new TicketRunnable();

Thread thread = new Thread(tickrunner, "Thread No: " + i);

thread.start();

}

 

}

 

public class TicketRunnable implements Runnable {

 

@Override

public void run() {

logger.info(Thread.currentThread().getName() + "  start "

+ count);

// TODO Auto-generated method stub

if (count > 0) {

// logger.info(Thread.currentThread().getName()

// + "  really  start" + count);

lock.lock();

synchronized (this) {

if(count<=0){

logger.info(Thread.currentThread().getName()

+ "  ticket sold out ! No tickets remained!" + count);

lock.unlock();

return;

}else{

count=count-1;

logger.info(Thread.currentThread().getName()

+ " bought a ticket,now remaining :" + (count));

}

}

lock.unlock();

}else{

logger.info(Thread.currentThread().getName()

+ "  ticket sold out !" + count);

}

}

}

 

public RedisTemplate<Serializable, Serializable> getRedisTemplate() {

return redisTemplate;

}

 

public void setRedisTemplate(

RedisTemplate<Serializable, Serializable> redisTemplate) {

this.redisTemplate = redisTemplate;

}

 

public MultiConsumer(RedisTemplate<Serializable, Serializable> redisTemplate) {

super();

this.redisTemplate = redisTemplate;

}

}

Lock工具類:


咱們知道爲保證線程安全,程序中執行的操做必須時原子的。redis後續的版本中可使用set key同時設置expire超時時間。


想起上次去 電信翼支付 面試時,面試官問過一個問題:分佈式鎖如何防止死鎖,問題關鍵在於咱們在分佈式中進行加鎖操做時成功了,可是後續業務操做完畢執行解鎖時出現失敗。致使分佈式鎖沒法釋放。出現死鎖,後續的加鎖沒法正常進行。因此這裏設置expire超時時間的目的就是防止出現解鎖失敗的狀況,這樣,即便解鎖失敗了,分佈式鎖依然會在超時時間過了以後自動釋放。


具體在代碼中也有註釋,也能夠做爲參考。


package com.tiger.utils;

 

import java.io.Serializable;

import java.util.Arrays;

import java.util.Collections;

import java.util.HashMap;

import java.util.Iterator;

import java.util.List;

import java.util.Random;

import java.util.concurrent.TimeUnit;

import java.util.concurrent.locks.Condition;

import java.util.concurrent.locks.Lock;

 

import javax.sound.midi.MidiDevice.Info;

 

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.dao.DataAccessException;

import org.springframework.data.redis.core.RedisOperations;

import org.springframework.data.redis.core.RedisTemplate;

import org.springframework.data.redis.core.SessionCallback;

import org.springframework.data.redis.core.script.RedisScript;

 

 

public class MultiTestLock implements Lock {

Logger logger=LoggerFactory.getLogger(MultiTestLock.class);

private RedisTemplate<Serializable, Serializable> redisTemplate;

public MultiTestLock(RedisTemplate<Serializable, Serializable> redisTemplate) {

super();

this.redisTemplate = redisTemplate;

}

 

@Override

public void lock() {

//這裏使用while循環強制線程進來以後先進行搶鎖操做。只有搶到鎖才能進行後續操做

while(true){

if(tryLock()){

try {

//這裏讓線程睡500毫秒的目的是爲了模擬業務耗時,確保業務結束時以前設置的值正好打到超時時間,

//實際生產中可能有誤差,這裏須要經驗

Thread.sleep(500l);

// logger.info(Thread.currentThread().getName()+" time to awake");

return;

} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}else{

try {

//這裏設置一個隨機毫秒的sleep目的時下降while循環的頻率 

Thread.sleep(new Random().nextInt(200)+100);

} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

}

}

 

@Override

public boolean tryLock() {

//這裏也能夠選用transactionSupport支持事務操做

SessionCallback<Object> sessionCallback=new SessionCallback<Object>() {

@Override

public  Object execute(RedisOperations operations)

throws DataAccessException {

operations.multi();

operations.opsForValue().setIfAbsent("secret", "answer");

//設置超時時間要根據業務實際的可能處理時間來,是一個經驗值

operations.expire("secret", 500l, TimeUnit.MILLISECONDS);

Object object=operations.exec();

return object;

}

};

//執行兩部操做,這裏會拿到一個數組值 [true,true],分別對應上述兩部操做的結果,若是中途出現第一次爲false則代表第一步set值出錯

List<Boolean> result=(List) redisTemplate.execute(sessionCallback);

// logger.info(Thread.currentThread().getName()+" try lock "+ result);

if(true==result.get(0)||"true".equals(result.get(0)+"")){

logger.info(Thread.currentThread().getName()+" try lock success");

return true;

}else{

return false;

}

}

 

@Override

public boolean tryLock(long arg0, TimeUnit arg1)

throws InterruptedException {

// TODO Auto-generated method stub

return false;

}

 

@Override

public void unlock() {

//unlock操做直接刪除鎖,若是執行完尚未達到超時時間則直接刪除,讓後續的線程進行繼續操做。起到補刀的做用,確保鎖已經超時或被刪除

SessionCallback<Object> sessionCallback=new SessionCallback<Object>() {

@Override

public  Object execute(RedisOperations operations)

throws DataAccessException {

operations.multi();

operations.delete("secret");

Object object=operations.exec();

return object;

}

};

Object result=redisTemplate.execute(sessionCallback);

}

 

 

@Override

public void lockInterruptibly() throws InterruptedException {

// TODO Auto-generated method stub

}

 

@Override

public Condition newCondition() {

// TODO Auto-generated method stub

return null;

}

public RedisTemplate<Serializable, Serializable> getRedisTemplate() {

return redisTemplate;

}

 

public void setRedisTemplate(

RedisTemplate<Serializable, Serializable> redisTemplate) {

this.redisTemplate = redisTemplate;

}

 

}

執行結果




能夠看到,票數穩步減小,後續沒有搶到鎖的線程餘票爲0,無票可搶。


tips:


這其中也出現了一個問題,redis進行多部封裝操做時,系統報錯:ERR EXEC without MULTI


後通過查閱發現問題出在:


在spring中,屢次執行MULTI命令不會報錯,由於第一次執行時,會將其內部的一個isInMulti變量設爲true,後續每次執行命令是都會檢查這個變量,若是爲true,則不執行命令。而屢次執行EXEC命令則會報開頭說的"ERR EXEC without MULTI"錯誤。

相關文章
相關標籤/搜索