基于Redis的简单分布式锁

版权声明:本文为博主原创文章,未经博主允许不得转载。 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();
    }
  }

猜你喜欢

转载自blog.csdn.net/xu622/article/details/84379569