Redis分布式事务锁实现

Redis事务锁

在不同进程需要互斥地访问共享资源时,分布式锁是一种非常有用的技术手段。本文采用Spring Data Redis实现一下Redis的分布式事务锁。

Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系。

SETNX命令(SET if Not eXists)语法:

SETNX key value

若给定的 key 已经存在,则 SETNX 不做任何动作,并返回0。

安全性:保证互斥,在任何时候,只有一个客户端可以持有锁
无死锁:即使当前持有锁的客户端崩溃或者从集群中被分开了,其它客户端最终总是能够获得锁。
容错性:只要大部分的 Redis 节点在线,那么客户端就能够获取和释放锁。

使用Spring redisTemplate的实现

使用redisTemplate实现需要配合redis 的eval实现,在Spring Data Redis的官方文档中Redis Scripting一节有相关的说明。

先看一下Spring Redis文档中是如何使用eval的:

@Bean
public RedisScript<Boolean> script() {
  DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<Boolean>();
  redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("META-INF/scripts/checkandset.lua")));
  redisScript.setResultType(Boolean.class);
}
public class Example {
  @Autowired
  RedisScript<Boolean> script;
  public boolean checkAndSet(String expectedValue, String newValue) {
    return redisTemplate.execute(script, Collections.singletonList("key"), expectedValue, newValue);
  }
}
 -- checkandset.lua local
 current = redis.call('GET', KEYS[1])
 if current == ARGV[1]
   then redis.call('SET', KEYS[1], ARGV[2])
   return true
 end
 return false

关于eval函数以及Lua脚本在此不进行赘述,下面来看一下我们如何使用redisTemplate实现事务锁。

定义事务锁的Bean:

public class RedisLock {
	private String key;
	private final UUID uuid;
	private long lockTimeout;

	private long startLockTimeMillis;
	private long getLockTimeMillis;
	private int tryCount;

	public RedisLock(String key, UUID uuid, long lockTimeout, long startLockTimeMillis, long getLockTimeMillis, int tryCount) {
		this.key = key;
		this.uuid = uuid;
		this.lockTimeout = lockTimeout;
		this.startLockTimeMillis = startLockTimeMillis;
		this.getLockTimeMillis = getLockTimeMillis;
		this.tryCount = tryCount;
	}

	public String getKey() {
		return key;
	}

	public void setKey(String key) {
		this.key = key;
	}

	public UUID getUuid() {
		return uuid;
	}

	public long getLockTimeout() {
		return lockTimeout;
	}

	public void setLockTimeout(long lockTimeout) {
		this.lockTimeout = lockTimeout;
	}

	public long getGetLockTimeMillis() {
		return getLockTimeMillis;
	}

	public void setGetLockTimeMillis(long getLockTimeMillis) {
		this.getLockTimeMillis = getLockTimeMillis;
	}

	public long getStartLockTimeMillis() {
		return startLockTimeMillis;
	}

	public void setStartLockTimeMillis(long startLockTimeMillis) {
		this.startLockTimeMillis = startLockTimeMillis;
	}

	public int getTryCount() {
		return tryCount;
	}

	public void setTryCount(int tryCount) {
		this.tryCount = tryCount;
	}
}

创建获取锁操作:

// 锁的过期时间,单位毫秒
private static final long DEFAULT_LOCK_TIME_OUT = 3000;   // 争抢锁的超时时间,单位毫秒,0代表永不超时(一直抢到锁为止)
private static final long DEFAULT_TRY_LOCK_TIME_OUT = 0;  
//拿锁的EVAL函数
private static final String LUA_SCRIPT_LOCK = "return redis.call('SET', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2]) ";
//释放锁的EVAL函数
private static RedisScript<String> scriptLock = new DefaultRedisScript<String>(LUA_SCRIPT_LOCK, String.class);

获取锁的方法:

public static RedisLock lock(int dbIndex, String key, long lockTimeout, long tryLockTimeout) {
	long timestamp = System.currentTimeMillis();
	try {
		//锁的名称
		key = key + ".lock";
			
		UUID uuid = UUID.randomUUID();
			
		int tryCount = 0;
			
		//在超时之前,循环尝试拿锁
		while (tryLockTimeout == 0 || (System.currentTimeMillis() - timestamp) < tryLockTimeout) {
		
//执行拿锁的操作,注意这里,后面的三个参数分别对应了scriptLock字符串中的三个变量值,KEYS[1],ARGV[1],ARGV[2],含义为锁的key,key对应的value,以及key 的存在时间(单位毫秒)

String result = redisTemplate.execute(scriptLock, redisTemplate.getStringSerializer(), redisTemplate.getStringSerializer(), Collections.singletonList(key), uuid.toString(),
						String.valueOf(lockTimeout));
	tryCount++;
	//返回“OK”代表拿到锁
	if (result != null && result.equals("OK")) {
		return new RedisLock(key, uuid, lockTimeout, timestamp, System.currentTimeMillis(), tryCount);
	} else {
		try {
			//如果失败,休息50毫秒继续重试(自旋锁)
			Thread.sleep(50);
		} catch (InterruptedException e) {
						e.printStackTrace();
		}
	}
}
logger.error("Fail to get lock key");
}
return null;
}

上述代码就是通过redisTemplate实现的redis 的分布式锁,如果创建Bean成功则说明拿到锁,否则拿锁失败,核心是采用Redis 的eval函数,使用类似CAS的操作,进行拿锁,如果拿锁成功,则返回“OK”,如果失败,休眠然后继续尝试拿锁,直到超时。

释放锁操作:

private static final String LUA_SCRIPT_UNLOCK = 
			"if (redis.call('GET', KEYS[1]) == ARGV[1]) then "
			+ "return redis.call('DEL',KEYS[1]) " 
			+ "else " + "return 0 " + "end";


private static RedisScript<String> scriptUnlock = 
		new DefaultRedisScript<String>(LUA_SCRIPT_UNLOCK,
			String.class);
public static void unLock(int dbIndex, RedisLock lock) {				       			   					             

	redisTemplate.execute(scriptUnlock,
			      redisTemplate.getStringSerializer(),
				  redisTemplate.getStringSerializer(),     
				  Collections.singletonList(lock.getKey()),     
				  lock.getUuid().toString());
}

上述就是使用Redis来实现分布式锁,其方法是采用Redis String 的 SET进行实现,SET 命令的行为可以通过一系列参数来修改:

  • EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。
  • PX millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。
  • NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。
  • XX :只在键已经存在时,才对键进行设置操作。

具体更多详情,请参看Redis文档

http://redisdoc.com/string/set.html

完整demo:

@Component
public class RedisLockUtil {

    private static final Logger logger = LoggerFactory.getLogger(RedisLockUtil.class);

    @Autowired
    private JedisClient jedisClient;

    // 锁的过期时间,单位毫秒
    private static final long DEFAULT_LOCK_TIMEOUT = 3000;

    // 争抢锁的超时时间,单位毫秒,0代表永不超时(一直抢到锁为止)
    private static final long DEFAULT_TRY_LOCK_TIMEOUT = 0;

    //拿锁的EVAL函数
    private static final String LUA_SCRIPT_LOCK = "return redis.call('SET', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2]) ";

    // 释放锁的Lua脚本
    private static final String LUA_SCRIPT_UNLOCK =
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                    "    redis.call('del', KEYS[1]); " +
                    "    return 'suc' " +
                    "else " +
                    "    return 'fail' " +
                    "end";

    public RedisLock lock(String key) {
        return lock(key, DEFAULT_LOCK_TIMEOUT, DEFAULT_TRY_LOCK_TIMEOUT);
    }

    /**
     * 获取分布式锁
     *
     * @param key   redis key名称
     * @param lockTimeout   锁时长
     * @param tryLockTimeout   尝试获取锁等待时间
     * @return
     */
    // 修改锁的值格式为:UUID + ":" + 线程ID
    public RedisLock lock(String key, long lockTimeout, long tryLockTimeout) {
        long timestamp = System.currentTimeMillis();
        try {
            key = key + ".lock";
            UUID uuid = UUID.randomUUID();
            // 获取当前线程ID
            String lockValue = uuid + ":" + Thread.currentThread().getId();
            int tryCount = 0;

            while (tryLockTimeout == 0 || (System.currentTimeMillis() - timestamp) < tryLockTimeout) {
                JedisClusterPipeline pipeline = jedisClient.getPipelined();
                Response<String> result = pipeline.eval(LUA_SCRIPT_LOCK,
                        Collections.singletonList(key),
                        Arrays.asList(lockValue, String.valueOf(lockTimeout))  // 使用包含线程ID的lockValue
                );
                pipeline.sync();
                tryCount++;
                if (result != null && "OK".equals(result.get())) {
                    return new RedisLock(key, uuid, Thread.currentThread().getId(), lockTimeout, timestamp, System.currentTimeMillis(), tryCount);
                } else {
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        } catch (Exception e) {
            logger.error("Fail to get lock key: {}", key, e);
        }
        return null;
    }

    public boolean unlock(RedisLock lock) {
        if (lock == null) {
            return false;
        }
        
        // 验证当前线程是否为加锁线程
        if (lock.getThreadId() != Thread.currentThread().getId()) {
            logger.warn("Current thread[{}] is not the lock owner[{}], will not unlock", 
                Thread.currentThread().getId(), lock.getThreadId());
            return false;
        }
        
        try {
            String lockValue = lock.getUuid().toString() + ":" + lock.getThreadId();
            JedisClusterPipeline pipeline = jedisClient.getPipelined();
            Response<String> response = pipeline.eval(LUA_SCRIPT_UNLOCK,
                    Collections.singletonList(lock.getKey()),
                    Collections.singletonList(lockValue)  // 使用包含线程ID的lockValue
            );
            pipeline.sync();
            return response != null && Objects.equals(response.get(), "suc");
        } catch (Exception e) {
            logger.error("Failed to release lock: {}", lock.getKey(), e);
            return false;
        }
    }
}
Redis本身并不支持原生的分布式事务。然而,可以通过使用Redis事务和Lua脚本功能来实现一种近似的分布式事务。在Redis中,事务是通过MULTI、EXEC和DISCARD命令来实现的。MULTI命令用于开始一个事务,EXEC命令用于执行事务中的所有命令,DISCARD命令用于取消事务。 要实现分布式事务,可以结合Redis和其他组件,如Seata。Seata是一个开源的分布式事务框架,可以与Redis集成。Seata提供了Transaction Coordinator(TC)、Transaction Manager(TM)和Resource Manager(RM)等组件,用于协调和管理分布式事务的各个参与方。 在使用Seata时,Redis可以作为一个RM,负责控制分支事务的提交和回滚。Seata的TM负责开启全局事务,并向TC发起全局提交或回滚的指令。TC维护全局事务的运行状态,并与RM通信协调各个分支事务的提交或回滚。 综上所述,要实现Redis分布式事务,可以结合使用Redis事务和Lua脚本功能,或者使用Seata等分布式事务框架来实现。 #### 引用[.reference_title] - *1* [Redis 分布式事务实现](https://blog.youkuaiyun.com/knight_zhou/article/details/111567010)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [Redis分布式分布式事务](https://blog.youkuaiyun.com/qq_43910862/article/details/125828647)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [分布式事务实现方案和redis缓存配置](https://blog.youkuaiyun.com/qq_49195366/article/details/127482237)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值