前言
目前幾乎很多大型網(wǎng)站及應(yīng)用都是分布式部署的,分布式場景中的數(shù)據(jù)一致性問題一直是一個比較重要的話題。
在很多場景中,我們?yōu)榱吮WC數(shù)據(jù)的最終一致性,需要很多的技術(shù)方案來支持,比如分布式事務(wù)、分布式鎖等。
Redis 的優(yōu)點(diǎn)
- Redis有很高的性能
- Redis命令對此支持較好,實(shí)現(xiàn)起來比較方便
這也是選用Redis 做分布式鎖的原因
啟動 mysql 和 redis
一、先看下目錄結(jié)構(gòu)

二、一般的業(yè)務(wù)代碼
@Override
public Result loginByWx(String openId) {
User user = userMapper.findByOpenId(openId);
if (user != null) {
return Result.of(0, "OK", user.getUserId());
}
// 模擬業(yè)務(wù)執(zhí)行時間
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("create user by openId: {}", openId);
user = new User();
user.setOpenId(openId);
userMapper.save(user);
return Result.of(0, "OK", user.getUserId());
}
如果在并發(fā)或者短時間內(nèi)重復(fù)請求會出現(xiàn)重復(fù)創(chuàng)建用戶的問題
三、了解Redis set方法
Jedis.set 方法
public String set(final String key, final String value, final String nxxx, final String expx, final int time)
存儲數(shù)據(jù)到緩存中,并制定過期時間和當(dāng)Key存在時是否覆蓋。
key 鎖的名字
value 鎖的內(nèi)容
nxxx的值只能取NX或者XX,如果取NX,則只有當(dāng)key不存在是才進(jìn)行set,如果取XX,則只有當(dāng)key已經(jīng)存在時才進(jìn)行set
expx的值只能取EX或者PX,代表數(shù)據(jù)過期時間的單位,EX代表秒,PX代表毫秒。
time 過期時間,單位是expx所代表的單位。
四、編寫分布式鎖
package com.itunion.demo.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import redis.clients.jedis.Jedis;
@Service
public class RedisLockServiceImpl implements LockService {
private static Logger log = LoggerFactory.getLogger(UserServiceImpl.class);
private static final String LOCK_SUCCESS = "OK";
@Autowired
protected RedisTemplate<String, String> redisTemplate;
@Override
public void unLock(String key) {
redisTemplate.delete(key);
}
public synchronized boolean isLock(String key, int seconds) {
return redisTemplate.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
Jedis conn = (Jedis) connection.getNativeConnection();
String result = conn.set(key, "1", "NX", "EX", seconds);
return result != null && result.equalsIgnoreCase(LOCK_SUCCESS);
}
});
}
@Override
public <T> T lockExecute(String key, LockExecute<T> lockExecute) {
boolean isLock = isLock(key, 15);
final int SLEEP_TIME = 200;
final int RETRY_NUM = 20;
int i;
for (i = 0; i < RETRY_NUM; i++) {
if (isLock) {
break;
}
try {
log.debug("wait redis lock key > {}", key);
Thread.sleep(SLEEP_TIME);
} catch (InterruptedException e) {
log.warn("wait redis error {}", e.getMessage());
}
isLock = isLock(key, 150);
}
if (!isLock) {
log.warn("wait lock time out key > {}", key);
return lockExecute.waitTimeOut();
}
try {
if (i > 0) log.debug("wait lock retry count {}", i);
return lockExecute.execute();
} finally {
unLock(key);
}
}
}
五、在業(yè)務(wù)上添加鎖
public Result loginByWxLock(String openId){
// 更新點(diǎn)贊數(shù)量
String lockKey = "lock:loginByWx:" + openId;
return lockService.lockExecute(lockKey, new LockService.LockExecute<Result>() {
@Override
public Result execute() {
return loginByWx(openId);
}
@Override
public Result waitTimeOut() {
return Result.of(-1, "訪問太頻繁");
}
});
}
六、單元測試
@Test
public void contextLoads() throws InterruptedException {
int size = 100;
CountDownLatch countDownLatch = new CountDownLatch(size);
List<Result> results = new ArrayList<>();
for (int i = 0; i < size; i++) {
new Thread(new Runnable() {
@Override
public void run() {
Random random = new Random();
String openId = "openId:" + random.nextInt(10);
results.add(userService.loginByWxLock(openId));
countDownLatch.countDown();
}
}).start();
}
countDownLatch.await();
// 統(tǒng)計(jì)信息
int loginSuccessNum = 0;
int loginFailedNum = 0;
for (Result result : results) {
if (result.getCode() == 0) {
loginSuccessNum++;
}else{
loginFailedNum++;
}
}
System.out.println("登錄成功總數(shù):" + loginSuccessNum);
System.out.println("登錄失敗總數(shù):" + loginFailedNum);
}
測試結(jié)果

Github: https://github.com/qiaohhgz/spring-boot-redis-lock.git
更多精彩內(nèi)容
- 架構(gòu)實(shí)戰(zhàn)篇(一):Spring Boot 整合MyBatis
- 架構(gòu)實(shí)戰(zhàn)篇(二):Spring Boot 整合Swagger2
- 架構(gòu)實(shí)戰(zhàn)篇(三):Spring Boot 整合MyBatis(二)
- 架構(gòu)實(shí)戰(zhàn)篇(四):Spring Boot 整合 Thymeleaf
- 架構(gòu)實(shí)戰(zhàn)篇(五):Spring Boot 表單驗(yàn)證和異常處理
- 架構(gòu)實(shí)戰(zhàn)篇(六):Spring Boot RestTemplate的使用
- 架構(gòu)實(shí)戰(zhàn)篇(七):Spring Boot Data JPA 快速入門
- 架構(gòu)實(shí)戰(zhàn)篇(八):Spring Boot 集成 Druid 數(shù)據(jù)源監(jiān)控
- 架構(gòu)實(shí)戰(zhàn)篇(九):Spring Boot 分布式Session共享Redis
- 架構(gòu)實(shí)戰(zhàn)篇(十三):Spring Boot Logback 郵件通知
- 架構(gòu)實(shí)戰(zhàn)篇(十四):Spring Boot 多緩存實(shí)戰(zhàn)
- 架構(gòu)實(shí)戰(zhàn)篇(十五):Spring Boot 解耦之事件驅(qū)動
- 架構(gòu)實(shí)戰(zhàn)篇(十六):Spring Boot Assembly服務(wù)化打包
- 架構(gòu)實(shí)戰(zhàn)篇(十七):Spring Boot Assembly 整合 thymeleaf