版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/xu622/article/details/84379569
前言
在单应用的情况下,需要对某个资源进行加锁经常会用到 synchronized 关键字。但是在集群的环境下,synchronized 只能进行单台机器的资源锁定。举例一个场景,账户表,该账户不断有人往里面转钱,账户余额需要不断的累加,表里有version字段。在高并发情况下,多个进程读取了同一个version的账户记录,只能有一条记录能成功更改。这里有多种解决方式,一种是获取账户记录之前先获得锁,另一种是失败的补偿机制(失败后重新读取再尝试更新)。
补偿机制原理简单,类似CAS(CompareAndSwap)这里是利用数据库的功能进行操作,Compare是version的比较,Swap是相应字段的交换,重新尝试,直到达到指定次数或者成功为止。
分布式锁
先来一段代码
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();
}
}