分佈式應用中,需要如何解決資源同步的問題?
這篇文章講解的很不錯了。Redis 分佈式鎖的正確實現方式( Java 版 )。另外在Github上也有一個demo做的很不錯。
什麼時候需要用到分佈式鎖呢?比如:減庫存。
假設某個商品的庫存是N,有N+1個訂單過來,那麼勢必只能有N個有效訂單。那麼如何保證庫存不出現負數的情況呢?(多個減庫存操作保證庫存的同步)
圖一:減庫存流程圖
圖二:分佈式鎖減庫操作
利用Redis就可以給庫存加一個分佈式鎖。需要注意的是不要造成死鎖,即保證持有鎖有一定的時限制。
獲取鎖的過程可以是:在redis服務器set一個key-value,lock。如果此lock已經存在,則說明當前鎖被其他線程持有。否則set此lock的值。lock需要有設置存活時間(保證不死鎖)。另外需要注意手動釋放鎖的時候,需要保證是當前持有鎖的線程去釋放。
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.Collections;
@RestController
@RequestMapping
public class RedisTemplateController {
@Resource(name = "myRedisTemplate")
private RedisTemplate<String, String> redisTemplate;
@GetMapping(value = "set")
public String set(String key, String value) {
redisTemplate.opsForValue().set(key, value);
return "success";
}
@GetMapping(value = "get")
public String get(String key) {
return redisTemplate.opsForValue().get(key);
}
/**
* 加鎖
* @param key
* @param value
* @return
*/
@GetMapping(value = "redisTemplateLock")
public Object lock(String key, String value) {
String script = "local key = KEYS[1]; local value = ARGV[1]; if redis.call('set', key, value, 'NX' ,'PX', 5000) then return 1 else return 0 end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
Object execute = redisTemplate.execute(redisScript, Collections.singletonList(key), Collections.singletonList(value));
System.out.println(execute);
return execute;
}
/**
* 阻塞鎖
*/
@GetMapping(value = "blockLock")
public String blockLock(String key, String value) throws InterruptedException {
// 被阻塞的時間超過5秒就停止獲取鎖
int blockTime = 5000;
// 默認的間隔時間
int defaultTime = 1000;
for(;;) {
if(blockTime >= 0) {
String script = "local key = KEYS[1]; local value = ARGV[1]; if redis.call('set', key, value, 'NX' ,'PX', 5000) then return 1 else return 0 end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, long.class);
Long result = redisTemplate.execute(redisScript, Collections.singletonList(key), value);
System.out.println("try lock ... ,result: "+result);
if(result != null && result == 1) {
// 得到了鎖
return "lock success";
} else {
blockTime -= defaultTime;
Thread.sleep(1000);
}
} else {
// 已經超時
return "lock timeout..., please retry later...";
}
}
}
/**
* 解鎖
* @param key
* @param value
*/
@GetMapping("redisTemplateUnlock")
public String unlock(String key, String value) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
Long execute = redisTemplate.execute(redisScript, Collections.singletonList(key), value);
System.out.println("unlock result: "+execute);
if(execute != null && execute != 0) {
// 解鎖成功
return "unlock success";
} else {
return "unlock failed";
}
}
}
另外延伸出一個問題:如何保證一個60秒的延遲。Thread.sleep(60000)是否可以保證延遲60s。答案是不行。解決辦法:
public void sleep(long millis) {
while (millis > 0) {
long begin = System.currentTimeMillis();
try {
Thread.sleep(millis);
} catch (Exception e) {
}
millis -= System.currentTimeMillis() - begin;
}
}