前言
最近開發公司的項目,遇到了分佈式的場景,即,同一條數據可能被多臺服務器或者說多個線程同時修改,此時可能會出現分佈式事務的問題,隨即封裝了redis分佈式鎖的註解。
場景分析
前提:我的銀行卡有0元錢,現在有A,B兩個人,想分別給我轉10元錢
分析:
假如A,B通過讀數據庫,同時發現我的餘額是0,這時,
線程A,會給我設置:
餘額 = 10 + 0
線程B,會給我設置:
餘額 = 10 + 0
最後,我的卡上收到了兩個人的轉賬,但是最後金額居然只有10元!!這是怎麼回事?
其實原因就在於多個線程,對一條數據同時進行了操作。如果我們可以設置一下,在修改的方法上面加一個鎖,每次修改之前,(A)先拿到這個鎖,再去做修改方法,此時,其他(B)線程想要修改的時候,看到鎖已經不再,需要等待鎖釋放,然後再去執行,就保證了A,B先後依此執行,數據依此累加就沒問題了。
解決辦法
基於代碼的可移植性,我將分佈式鎖做成了註解,大家如果有需要,可以直接將jar包拿過去做相應的修改即可,jar包下載地址(鏈接:https://pan.baidu.com/s/1hBn-...
提取碼:1msl):
註解使用說明:
1.在需要添加分佈式鎖的方法上面加上@RedisLock
如果key不添加,則默認鎖方法第一個參數param的id字段,如果需要指定鎖某個字段,則@RedisLock(key = "code")
2.如果方法沒有參數,則不可使用RedisLock鎖
@RedisLock
public void updateData( Data param){
}
下面詳細分析一下封裝的源碼:
先看一下項目結構(總共就4個類):
//RedisLock註解類:沒什麼好解釋的
/**
* Created by liuliang on 2018/10/15.
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisLock {
//被鎖的數據的id
String key() default "";
//喚醒時間
long acquireTimeout() default 6000L;
//超時時間
long timeout() default 6000L;
}
//----------------------類分割線---------------------
//RedisService 一個簡單的操作redis的類,封裝了加鎖和釋放鎖的方法
/**
* Created by liuliang on 2018/10/15.
*/
@Service
public class RedisService {
@Autowired
StringRedisTemplate stringRedisTemplate;
@Resource(name = "stringRedisTemplate")
@Autowired
ValueOperations valOpsStr;
@Autowired
RedisTemplate redisTemplate;
@Resource(name = "redisTemplate")
ValueOperations valOpsObj;
public String getStr(String key) {
return stringRedisTemplate.opsForValue().get(key);//獲取對應key的value
// return valOpsStr.get(key);
}
public void setStr(String key, String val) {
stringRedisTemplate.opsForValue().set(key,val,1800, TimeUnit.SECONDS);
// valOpsStr.set(key, val);
}
public void del(String key) {
stringRedisTemplate.delete(key);
}
/**
* 根據指定o獲取Object
*
* @param o
* @return
*/
public Object getObj(Object o) {
return valOpsObj.get(o);
}
/**
* * 設置obj緩存
* * @param o1
* * @param o2
*
*/
public void setObj(Object o1, Object o2) {
valOpsObj.set(o1, o2);
}
/**
* 刪除Obj緩存
*
* @param o
*/
public void delObj(Object o) {
redisTemplate.delete(o);
}
private static JedisPool pool = null;
static {
JedisPoolConfig config = new JedisPoolConfig();
// 設置最大連接數
config.setMaxTotal(200);
// 設置最大空閒數
config.setMaxIdle(8);
// 設置最大等待時間
config.setMaxWaitMillis(1000 * 100);
// 在borrow一個jedis實例時,是否需要驗證,若爲true,則所有jedis實例均是可用的
config.setTestOnBorrow(true);
pool = new JedisPool(config, "127.0.0.1", 6379, 3000);
}
DistributedLock lock = new DistributedLock(pool);
/**
* redis分佈式加鎖
* @param objectId
* @param acquireTimeout
* @param timeout
*/
public String redisLock(String objectId,Long acquireTimeout, Long timeout) {
// 對key爲id加鎖, 返回鎖的value值,供釋放鎖時候進行判斷
String lockValue = lock.lockWithTimeout(objectId, acquireTimeout, timeout);
System.out.println(Thread.currentThread().getName() + "獲得了鎖");
return lockValue;
}
/**
* 釋放redis分佈式鎖
* @param objectId
* @param lockValue
*/
public Boolean releaseLock(String objectId,String lockValue){
boolean b = lock.releaseLock(objectId, lockValue);
System.out.println(Thread.currentThread().getName() + "釋放了鎖");
return b;
}
//----------------------類分割線---------------------
/**
* Created by liuliang on 2018/10/15.
*
* 分佈式鎖的主要類,主要方法就是加鎖和釋放鎖
*具體的邏輯在代碼註釋裏面寫的很清楚了
*/
@Slf4j
public class DistributedLock {
private final JedisPool jedisPool;
public DistributedLock(JedisPool jedisPool) {
this.jedisPool = jedisPool;
}
/**
* 加鎖
* @param locaName 鎖的key
* @param acquireTimeout 獲取超時時間
* @param timeout 鎖的超時時間
* @return 鎖標識
*/
public String lockWithTimeout(String locaName,
long acquireTimeout, long timeout) {
Jedis conn = null;
String retIdentifier = null;
try {
// 獲取連接
conn = jedisPool.getResource();
// 隨機生成一個value
String identifier = UUID.randomUUID().toString();
// 鎖名,即key值
String lockKey = "lock:" + locaName;
// 超時時間,上鎖後超過此時間則自動釋放鎖
int lockExpire = (int)(timeout / 1000);
// 獲取鎖的超時時間,超過這個時間則放棄獲取鎖
long end = System.currentTimeMillis() + acquireTimeout;
while (System.currentTimeMillis() < end) {
log.info("lock...lock...");
if (conn.setnx(lockKey, identifier) == 1) {
log.info("==============lock success!=============");
conn.expire(lockKey, lockExpire);
// 返回value值,用於釋放鎖時間確認
retIdentifier = identifier;
return retIdentifier;
}
// 返回-1代表key沒有設置超時時間,爲key設置一個超時時間
if (conn.ttl(lockKey) == -1) {
conn.expire(lockKey, lockExpire);
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
} catch (JedisException e) {
e.printStackTrace();
} finally {
if (conn != null) {
conn.close();
}
}
return retIdentifier;
}
/**
* 釋放鎖
* @param lockName 鎖的key
* @param identifier 釋放鎖的標識
* @return
*/
public boolean releaseLock(String lockName, String identifier) {
Jedis conn = null;
String lockKey = "lock:" + lockName;
boolean retFlag = false;
try {
conn = jedisPool.getResource();
while (true) {
// 監視lock,準備開始事務
conn.watch(lockKey);
//避免空指針
String lockKeyValue = conn.get(lockKey)==null?"":conn.get(lockKey);
// 通過前面返回的value值判斷是不是該鎖,若是該鎖,則刪除,釋放鎖
if (lockKeyValue.equals(identifier)) {
Transaction transaction = conn.multi();
transaction.del(lockKey);
List results = transaction.exec();
if (results == null) {
continue;
}
log.info("==============unlock success!=============");
retFlag = true;
}
conn.unwatch();
break;
}
} catch (JedisException e) {
e.printStackTrace();
} finally {
if (conn != null) {
conn.close();
}
}
return retFlag;
}
//----------------------類分割線---------------------
/**
* Created by liuliang on 2018/10/16.
這是一個攔截器,我們指定攔截RedisLock註解
*/
@Aspect
@Component
@Slf4j
public class RedisLockAop {
ThreadLocal<Long> beginTime = new ThreadLocal<>();
ThreadLocal<String> objectId = new ThreadLocal<>();
ThreadLocal<String> lockValue = new ThreadLocal<>();
@Autowired
private RedisService redisService;
@Pointcut("@annotation(redisLock)")
public void serviceStatistics(RedisLock redisLock) {
}
@Before("serviceStatistics(redisLock)")
public void doBefore(JoinPoint joinPoint, RedisLock redisLock) {
// 記錄請求到達時間
beginTime.set(System.currentTimeMillis());
//註解所在方法名
String methodName = joinPoint.getSignature().getName();
//註解所在類
String className = joinPoint.getSignature().getDeclaringTypeName();
//方法上的參數
Object[] args = joinPoint.getArgs();
String key = redisLock.key();
if(ObjectUtils.isNullOrEmpty(args)){
//方法的參數是空,生成永遠不重複的uuid,相當於不做控制
key = methodName + UUID.randomUUID().toString();
}else {
//取第一個參數指定字段,若沒有指定,則取id字段
Object arg = args[0];
log.info("arg:"+arg.toString());
Map<String, Object> map = getKeyAndValue(arg);
Object o = map.get(StringUtils.isEmpty(key) ? "id" : key);
if(ObjectUtils.isNullOrEmpty(o)){
//自定義異常,可以換成自己項目的異常
throw new MallException(RespCode.REDIS_LOCK_KEY_NULL);
}
key = o.toString();
}
log.info("線程:"+Thread.currentThread().getName() + ", 已進入方法:"+className+"."+methodName);
// objectId.set(StringUtils.isEmpty(redisLock.key()) ? UserUtils.getCurrentUser().getId() : redisLock.key());
objectId.set(key);
String lock = redisService.redisLock(objectId.get(), redisLock.acquireTimeout(), redisLock.timeout());
lockValue.set(lock);
log.info("objectId:"+objectId.get()+",lockValue:"+lock +",已經加鎖!");
}
@After("serviceStatistics(redisLock)")
public void doAfter(JoinPoint joinPoint,RedisLock redisLock) {
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getSignature().getDeclaringTypeName();
redisService.releaseLock(objectId.get(),lockValue.get());
log.info("objectId:"+objectId.get()+",lockValue:"+lockValue.get() +",已經解鎖!");
log.info("線程:"+Thread.currentThread().getName() + ", 已退出方法:"+className+"."+methodName+",耗時:"+(System.currentTimeMillis() - beginTime.get() +" 毫秒!"));
}
//這是一個Object轉mapd的方法
public static Map<String, Object> getKeyAndValue(Object obj) {
Map<String, Object> map = new HashMap<String, Object>();
// 得到類對象
Class userCla = (Class) obj.getClass();
/* 得到類中的所有屬性集合 */
Field[] fs = userCla.getDeclaredFields();
for (int i = 0; i < fs.length; i++) {
Field f = fs[i];
f.setAccessible(true); // 設置些屬性是可以訪問的
Object val = new Object();
try {
val = f.get(obj);
// 得到此屬性的值
map.put(f.getName(), val);// 設置鍵值
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
/*
* String type = f.getType().toString();//得到此屬性的類型 if
* (type.endsWith("String")) {
* System.out.println(f.getType()+"\t是String"); f.set(obj,"12") ;
* //給屬性設值 }else if(type.endsWith("int") ||
* type.endsWith("Integer")){
* System.out.println(f.getType()+"\t是int"); f.set(obj,12) ; //給屬性設值
* }else{ System.out.println(f.getType()+"\t"); }
*/
}
System.out.println("單個對象的所有鍵值==反射==" + map.toString());
return map;
}
}