概述
目前几乎很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题。分布式的CAP理论告诉我们“任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。”所以,很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证“最终一致性”,只要这个最终时间是在用户可以接受的范围内即可。
在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。
比如一个操作要修改用户的状态,修改状态需要先读出用户的状态,在内存里进行修改,改完了再存回去。如果这样的操作同时进行了,就会出现并发问题,因为读取和保存状态这两个操作不是原子的。(Wiki 解释:所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch 线程切换。)
选用Redis实现分布式锁原因
- Redis有很高的性能
- Redis命令对此支持较好,实现起来比较方便
JAVA代码实现:
1.配置redis:
package cn.sunll.lemolang.conf;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import cn.sunll.lemolang.redis.lock.Lock;
import cn.sunll.lemolang.redis.lock.RedisLock;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
/**
* 分布式锁配置
* @Title: LockConfig.java
* @Package: cn.sunll.lemolang.conf
* @Description:
* @author: sunll
* @date: 2018年3月26日 下午7:53:03
* @version: V1.0
*/
@Configuration
public class LockConfig {
@Value("${spring.redis.lock.maxTotal}")
private int maxTotal;
@Value("${spring.redis.lock.maxIdle}")
private int maxIdle;
@Value("${spring.redis.lock.minIdle}")
private int minIdle;
@Value("${spring.redis.host}")
private String hostName;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.pass}")
private String password;
@Value("${spring.redis.lock.index}")
private int index;
@Bean
public Lock redisLock() {
Lock redisLock = new RedisLock(jedisPool(), index);
return redisLock;
}
@Bean
public JedisPool jedisPool() {
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(maxTotal);
poolConfig.setMaxIdle(maxIdle);
poolConfig.setMinIdle(minIdle);
poolConfig.setTestOnBorrow(false);
return new JedisPool(poolConfig, hostName, port, 2000, password);
}
}
2.设计分布式锁接口类:
package cn.sunll.lemolang.redis.lock;
/**
* @Title: Lock.java
* @Package: cn.sunll.lemolang.lock
* @Description:
* @author: sunll
* @date: 2018年3月26日 下午7:44:04
* @version: V1.0
*/
public interface Lock {
/**
* <p>尝试获取锁,无论是否获得都立即返回
* <p>建议优先使用该方法
* @param lockKey
* @param requestId
* @param expireMillisecond 锁超时自动释放时间
* @return
*/
boolean tryLock(String lockKey, String requestId, int expireMillisecond);
/**
* <p>尝试获取锁,指定时间内未获得则返回
*
* <p>NOTE:除非必要否则尽量使用不带timeoutMillisecond参数的tryLock,
* 如果要使用本方法,一定要考虑timeoutMillisecond参数值大小,
* 因为在timeoutMillisecond超时时间内redis连接是不释放的,
* 如果该值过大,并发量大的时候可能导致redis连接耗尽
* @param lockKey
* @param requestId
* @param expireMillisecond 锁超时自动释放时间
* @param timeoutMillisecond 获取锁超时时间
* @return
*/
boolean tryLock(String lockKey, String requestId, int expireMillisecond,
int timeoutMillisecond);
/**
* 解锁
*
* @param lockKey 锁标识
* @param requestId
* @return
*/
boolean unLock(String lockKey, String requestId);
}
3.实现LOCK类:
package cn.sunll.lemolang.redis.lock;
import java.util.Collections;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import cn.sunll.lemolang.log.InvoiceLogger;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
/**
* @Title: RedisLock.java
* @Package: cn.sunll.lemolang.lock
* @Description:
* @author: sunll
* @date: 2018年3月26日 下午7:44:27
* @version: V1.0
*/
public class RedisLock implements Lock {
private static final Logger logger = Logger
.getInstance(RedisLock.class);
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
private static final Long RELEASE_SUCCESS = 1L;
private static final String LOCK_KEY_PREFIX = "RL_";
/**
* 锁默认超时时间3秒
*/
private static final int DEFAULT_LOCK_EXPIRE_TIME = 1000 * 3;
/**
* 默认获取锁超时时间5秒
*/
private static final int DEFAULT_TRY_LOCK_TIMEOUT = 1000 * 5;
/**
* redis连接池
*/
private JedisPool jedisPool;
/**
* 默认选择redis的1作为分布式锁的库
*/
private Integer dbIndex = 1;
/**
* @param jedisPool
*/
public RedisLock(JedisPool jedisPool) {
this.jedisPool = jedisPool;
}
/**
* @param dbIndex
* @param jedisPool
*/
public RedisLock(JedisPool jedisPool, Integer dbIndex) {
this.dbIndex = dbIndex;
this.jedisPool = jedisPool;
}
@Override
public boolean tryLock(String lockKey, String requestId, int expireMillisecond) {
Jedis jedis = null;
try {
jedis = getConnection();
return tryLock(jedis, getLockKey(lockKey), requestId, expireMillisecond);
} catch (Exception e) {
logger.error("try lock fail:{}", e);
} finally {
returnConnection(jedis);
}
return false;
}
@Override
public boolean tryLock(String lockKey, String requestId, int expireMillisecond,
int timeoutMillisecond) {
if (timeoutMillisecond < 0) {
timeoutMillisecond = DEFAULT_TRY_LOCK_TIMEOUT;
}
lockKey = getLockKey(lockKey);
boolean result = false;
Jedis jedis = null;
try {
Long timeout = System.currentTimeMillis() + timeoutMillisecond;
jedis = getConnection();
result = tryLock(jedis, lockKey, requestId, expireMillisecond);
if (!result) {
while (timeout > System.currentTimeMillis()) {
result = tryLock(lockKey, requestId, expireMillisecond);
logger.info("try lock again result:{}", result);
if (result) {
break;
}
try {
TimeUnit.MILLISECONDS.sleep(new Random().nextInt(200));
} catch (InterruptedException e) {
logger.error("try lock error:{}", e);
Thread.currentThread().interrupt();
}
}
}
} catch (Exception e) {
logger.error("try lock fail : {}", e);
} finally {
if (jedis != null) {
jedis.close();
}
}
return result;
}
/**
* 上锁
*
* @param jedis
* @param lockKey
* @param requestId
* @param expireMillisecond 锁超时自动释放时间
* @return
*/
private boolean tryLock(Jedis jedis, String lockKey, String requestId, int expireMillisecond) {
if (expireMillisecond < 0) {
expireMillisecond = DEFAULT_LOCK_EXPIRE_TIME;
}
logger.info("try lock key:{},requestId:{}", lockKey, requestId);
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME,
expireMillisecond);
logger.info("try lock result:{}", result);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
@Override
public boolean unLock(String lockKey, String requestId) {
lockKey = getLockKey(lockKey);
logger.info("unlock lockKey:{}", lockKey);
Jedis jedis = null;
try {
jedis = getConnection();
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey),
Collections.singletonList(requestId));
logger.info("unlock lockKey:{},result:{}", lockKey, result);
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
} catch (Exception e) {
logger.error("unlock fail:{}", e);
} finally {
returnConnection(jedis);
}
return false;
}
/**
* 获取连接
*
* @return
*/
private Jedis getConnection() {
Jedis jedis = jedisPool.getResource();
if (dbIndex > 0) {
jedis.select(dbIndex);
}
return jedis;
}
/**
* 归还连接
*
* @param jedis
*/
private void returnConnection(Jedis jedis) {
if (jedis != null) {
jedis.close();
}
}
/**
* 获取锁key
*
* @param key
* @return
*/
private String getLockKey(String key) {
Assert.notNull(key, "redis lock key must is not null");
if (key.startsWith(LOCK_KEY_PREFIX)) {
return key;
}
return LOCK_KEY_PREFIX + key;
}
}
4.使用redis分布式锁:
boolean isGetLock = false;
String lockValue = UUID.randomUUID().toString().substring(1, 8);
try {
isGetLock = redisLock.tryLock"lockKey", lockValue,3000);
if (isGetLock) {
//业务逻辑处理
}
}catch (Exception e) {
logger.error("加锁异常:{}", e);
} finally {
if (isGetLock) {
redisLock.unLock("lockKey", lockValue);
}
}