基于Redis的简单分布式锁(Jedis和RedisTemplate)

前言

在单应用的情况下,需要对某个资源进行加锁经常会用到 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;

    }

}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值