https://blog.csdn.net/doujinlong1/article/details/81028923
接口幂等性的设计之————redis分布式锁的应用
在集群分布式机器部署的前提下,接口在相同数据高并发的情况下如果没有唯一索引的情况下,可能会有一些问题。
比如:
插入或更新商品的接口,如果没有则插入,有则更新的接口。支持多次修改。
考虑一种情况,前端页面第一次提交时瞬间点击多次。这种情况下会先去数据库查询,然后再插入。(当然唯一索引也可以解决,但是这种的有一次提交将会被拒绝)。
所有分布式锁的使用场景可以类比于以前使用sychronized的场景,java自己的锁只适用于单jvm的场景,在多jvm的时候只能用分布式锁来解决。
下面看下简单的使用
/**
*
* @param lockName 锁名称
* @param acquireTimeout 等待获取锁时间
* @param lockTimeout 锁超时时间
* @return
*/
public String acquireRedisLock(String lockName, long acquireTimeout, long lockTimeout) {
Jedis jedis = getRedisResource();
if (jedis == null) {
logger.error("get resource failed");
return null;
}
String identifier = UUID.randomUUID().toString();
String lockKey =lockName;
int lockExpire = (int)(lockTimeout / 1000);
try {
long end = System.currentTimeMillis() + acquireTimeout;
while (System.currentTimeMillis() < end) {
if (jedis.setnx(lockKey, identifier) == 1){
jedis.expire(lockKey, lockExpire);
return identifier;
}
if (jedis.ttl(lockKey) == -1) {
jedis.expire(lockKey, lockExpire);
}
try {
Thread.sleep(1);
}catch(InterruptedException ie){
Thread.currentThread().interrupt();
}
}
} catch (Exception e) {
logger.error("acquireLockWithFullLockKey 获取分布式锁出现问题,",e);
} finally {
jedis.close();
}
// null indicates that the lock was not acquired
return null;
}
/**
* 释放分布式锁
* @param lockName
* @param identifier
* @return
*/
public boolean releaseLock(String lockName, String identifier) {
Jedis jedis = getRedisResource();
String lockKey = lockName;
try {
while (true){
jedis.watch(lockKey);
if (identifier.equals(jedis.get(lockKey))){
Transaction trans = jedis.multi();
trans.del(lockKey);
List<Object> results = trans.exec();
if (results == null){
continue;
}
return true;
}
jedis.unwatch();
break;
}
} catch (Exception e) {
logger.error("releaseLock 释放分布式锁出现问题,",e);
} finally {
jedis.close();
}
return false;
}
释放的时候用到了redis的watch命令和multi,exec,主要是事务方面操作,防止被key的值修改后删除。和jedis.get出来的值不一样。
redis的操作可以见我博客后续关于redis的文章。
这两个方法有了之后,一个redis分布式锁就好了,来看看使用:
public void runAmethod() {
String lockId = lock.acquireLock("get", 2000, 5000);
boolean isLock = StringUtils.isNotBlank(lockId);
if (isLock) {
System.out.println("执行方法============");
} else {
logger.error("未获取到redis锁,不能执行");
}
if (null != lockId) {
lock.releaseLock("get", lockId);
}
}
使用时直接这样,锁名字每个方法应该不同,锁获取时间根据方法一般执行时间设置,超时时间应该要考虑到吞吐量和接口的最长执行时间。太长了吞吐量不够,太短了可能会导致并发问题。
后续,如果要进一步封装可以这样:
首先定义一个接口
package com.service;
public interface RedisLockMethod {
void runMethod();
}
然后使用:
先看无锁情况下:
public void test() {
noLockMethod();
}
private void noLockMethod() {
System.out.println("我是之前无锁的方法");
}
test方法里面的方法现在要加分布式锁处理,该怎么操作呢?
public void test() {
//1,lamda写法
runAmethod(()->noLockMethod());
//2,匿名内部类写法
runAmethod(new RedisLockMethod() {
@Override
public void runMethod() {
//原有的方法
noLockMethod();
}
});
}
private void noLockMethod() {
System.out.println("我是之前无锁的方法");
}
public void runAmethod(RedisLockMethod aaa){
String lockId = lock.acquireLock("get", 2000, 5000);
boolean isLock = StringUtils.isNotBlank(lockId);
if (isLock) {
aaa.runMethod();
} else {
logger.error("未获取到redis锁,不能执行");
}
if (null != lockId) {
lock.releaseLock("get", lockId);
}
}
提供了两种方法,比较推荐lamda写法。这种写法下runAmethod()这个方法可以抽取出来作为公共的方法,所有需要分布式锁的时候直接调用即可,里面封装要锁的方法即可。