前言
在单应用的情况下,需要对某个资源进行加锁经常会用到 synchronized 关键字。但是在集群的环境下,synchronized 只能进行单台机器的资源锁定。举例一个场景,账户表,该账户不断有人往里面转钱,账户余额需要不断的累加,表里有version字段。在高并发情况下,多个进程读取了同一个version的账户记录,只能有一条记录能成功更改。这里有多种解决方式,一种是获取账户记录之前先获得锁,另一种是失败的补偿机制(失败后重新读取再尝试更新)。
补偿机制原理简单,类似CAS(CompareAndSwap)这里是利用数据库的功能进行操作,Compare是version的比较,Swap是相应字段的交换,重新尝试,直到达到指定次数或者成功为止。
分布式锁
Jedis
先来一段代码
package ***.***.***.util;
import redis.clients.jedis.Jedis;
import java.util.Collections;
/**
* @className: RedisTool
* @auther:
* @date: 2018/11/9 0009 15:39
* @description: 分布式锁实现
*/
public class RedisLockTool {
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;
/**
* 尝试获取分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @param expireTime 超期时间
* @return 是否获取成功
*/
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
/**
* 释放分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @return 是否释放成功
*/
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
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));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
releaseDistributedLock 这个方法里面之所以有一段lua脚本,是因为get后再set不是一个原子操作,lua脚本能保证一次执行完成。
tryGetDistributedLock 要设置超时时间是防止线程异常,没有去释放锁,导致其他线程无法获得锁。这里时间单位是毫秒。
通常我们在spring boot中都是使用 RedisTemplate 去操作,那这个Jedis的入参应该怎么获得?
可以通过Jedis jedis = jedisPool.getResource();
@Configuration
public class RedisConfig {
@Autowired
private RedisProperties redisProperties;
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new RedisObjectSerializer());
return template;
}
@Bean
public JedisPool redisPoolFactory() {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxIdle(redisProperties.getPool().getMaxIdle());
config.setMinIdle(redisProperties.getPool().getMaxIdle());
config.setMaxTotal(redisProperties.getPool().getMaxActive());
config.setMaxWaitMillis(redisProperties.getPool().getMaxWait());
String host = redisProperties.getHost();
Integer port = redisProperties.getPort();
Integer timeout = redisProperties.getTimeout();
String password = redisProperties.getPassword();
return new JedisPool(config, host, port, timeout, password);
}
}
下面是具体的调用方法
private void tryLock(Jedis jedis, Long merchantId, Long coinId, String uuid, Long userId, int count) throws InterruptedException {
//expire time unit ms
while (!RedisLockTool.tryGetDistributedLock(jedis, String.format(LOCK_NAME, merchantId, coinId), uuid, EXPIRE_TIME)) {
if (count-- <= 0) {
log.info(String.format("【未获得账户锁】- 参数:userId=%s,merchantId=%s,coinId=%s", userId, merchantId, coinId));
throw new ******Exception(ErrorEnum.******.getErrMsg(), ErrorEnum.******.getErrCode());
}
Thread.sleep(10);
}
}
private void releaseLock(Jedis jedis, Long merchantId, Long coinId, String uuid) {
RedisLockTool.releaseDistributedLock(jedis, String.format(LOCK_NAME, merchantId, coinId), uuid);
jedis.close();
}
释放完锁要记得调用close()方法,把jedis还给JedisPool。
下面是jedis.close()的源码。
@Override
public void close() {
if (dataSource != null) {
if (client.isBroken()) {
this.dataSource.returnBrokenResource(this);
} else {
this.dataSource.returnResource(this);
}
} else {
client.close();
}
}
RedisTemplate
用spring系列框架
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.StringUtils;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisCommands;
import java.util.ArrayList;
import java.util.List;
/**
* @className: RedisTool
* @auther:
* @date: 2018/11/9 0009 15:39
* @description: 分布式锁实现
*/
public class RedisLockTool {
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
/**
* 尝试获取分布式锁
*
* @param redisTemplate Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @param expireTime 超期时间
* @return 是否获取成功
*/
public static boolean tryGetDistributedLock(RedisTemplate<String, Object> redisTemplate, String lockKey, String requestId, int expireTime) {
try {
RedisCallback<String> callback = (connection) -> {
JedisCommands commands = (JedisCommands) connection.getNativeConnection();
return commands.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
};
String result = redisTemplate.execute(callback);
//成功返回OK
return !StringUtils.isEmpty(result);
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
/**
* 释放分布式锁
*
* @param redisTemplate Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @return 是否释放成功
*/
public static boolean releaseDistributedLock(RedisTemplate<String, Object> redisTemplate, String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
// 释放锁的时候,有可能因为持锁之后方法执行时间大于锁的有效期,此时有可能已经被另外一个线程持有锁,所以不能直接删除
try {
List<String> keys = new ArrayList<>();
keys.add(lockKey);
List<String> args = new ArrayList<>();
args.add(requestId);
// 使用lua脚本删除redis中匹配value的key,可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁
// spring自带的执行脚本方法中,集群模式直接抛出不支持执行脚本的异常,所以只能拿到原redis的connection来执行脚本
RedisCallback<Long> callback = (connection) -> {
Object nativeConnection = connection.getNativeConnection();
// 集群模式和单机模式虽然执行脚本的方法一样,但是没有共同的接口,所以只能分开执行
// 集群模式
if (nativeConnection instanceof JedisCluster) {
return (Long) ((JedisCluster) nativeConnection).eval(script, keys, args);
}
// 单机模式
else if (nativeConnection instanceof Jedis) {
return (Long) ((Jedis) nativeConnection).eval(script, keys, args);
}
//0L是失败
return 0L;
};
Long result = redisTemplate.execute(callback);
return result != null && result > 0;
} catch (Exception e) {
e.printStackTrace();
} finally {
}
return false;
}
}