redis分布式锁事务解决方案

6 篇文章 0 订阅
3 篇文章 0 订阅

通常我们使用redisTemplate 的setIfAbsent()方法进行加锁,在到使用expire()方法进行设置超时时间的是时候,两个操作时使用两个链接不在一个事务中,当存在客户端setIfAbsent()加锁成功后服务中断,expire()无法进行超时设置,导致死锁的情况。

针对以上情况可以采取两种解决方案:

方案一:

将锁的超时间放在锁(key)的值(里面),及redis客户端在获取锁的时将客户端获取锁的时间加上超时时间形成失效时间点形成一个key-value的数据存在redis中,当客户端再次获取锁时候先判断锁的key是否存在,当存在时判断value的值和客户端的时间进行比对,看锁是否过期。

缺点:锁的过期机制受客户端时间限制,当存在加锁成功后,客户端时间发生大时间后移的请求,锁便存在长时间的死锁问题

方案二:

由于在redisTemplate 低版本的api中加锁和超时设置是两步存在死锁的问题(redisTemplate 2.1.*版本可以setIfAbsent方法可以设置锁同时设置时间),而redisTemplate 的api操作其实也是封装redis的指令,并发送给服务端进行执行一系列操作,因此我们可以直接通过redisTemplate 发送指令进行加锁操作解决事务问题,通过发送lua脚本给服务端执行业务操作


import com.alibaba.dubbo.common.utils.Assert;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;

@Component
public class RedisUtil {
    //    @Qualifier("stringRedisTemplate")
    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * redisUtil.setIfAbsent 新加的带有超时的setIfAbsent 脚本
     */
    String newSetIfAbsentScriptStr = " if (redis.call('setnx', KEYS[1], ARGV[2]) == 1) then" +
                                     " redis.call('expire', KEYS[1], ARGV[2])" +
                                     " return 1;" +
                                     " else" +
                                     " return 0;" +
                                     " end;";

    public RedisScript<Boolean> newSetIfAbsentScript = new DefaultRedisScript<Boolean>(newSetIfAbsentScriptStr, Boolean.class);

    /**
     * @Description: setIfAbsent升级版,加了超时时间
     * @Author: Gong Yongwei
     * @Date: 2018/12/12 9:21
     * @param key
     * @param value
     * @param seconds 超时时间,秒为单位
     * @return: boolean
     */
    public boolean setIfAbsent(String key, String value, Long seconds) {


        List<Object> keys = new ArrayList<Object>();
        keys.add(key);
        Object[] args = { value, seconds.toString() };
        return (boolean) redisTemplate.<Boolean> execute(newSetIfAbsentScript, keys, args);
    }

}
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import com.alibaba.dubbo.common.utils.CollectionUtils;
import com.wangyong.rooster.BaseTest;
import com.wangyong.redis.RedisKeyTimeout;
import com.wangyong.redis.RedisUtil;
import org.junit.Test;

@Slf4j
public class RedisTest extends BaseTest {

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private RedisUtil redisUtil;

    @Test
    public void test() {
        String key  = "lock_generator_calender_00001";
//        redisTemplate.opsForValue().set(key, key);
        redisTemplate.delete(key);

        Object result = redisUtil.setIfAbsent(key,key,10L);
        System.out.println("加锁结果:"+result);

        System.out.println("锁时间"+redisTemplate.getExpire(key, TimeUnit.MILLISECONDS));

        result = redisUtil.setIfAbsent(key,key,20L);

        System.out.println("加锁结果2:"+result);
        System.out.println("锁时间2:"+redisTemplate.getExpire(key, TimeUnit.MILLISECONDS));

        redisTemplate.delete(key);
    }
}

在这过程中可能出现错误

这个错误大致看起来时数组越界问题,其实时在问题是设置锁的时候,value没有进行序列化的问题,而redisTemplate 默认的序列化方式是jdk的序列化方式,redis的服务端是无法处理的,因此我们需要制定redisTemplate 的value的序列化方式

@Bean
    public RedisTemplate<Object, Object> stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<Object, Object>();

        //key都用String 序列化
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        RedisSerializer<String> stringSerializer = new StringRedisSerializer();
        template.setHashKeySerializer(stringSerializer);
        template.setKeySerializer(stringSerializer);
        template.setValueSerializer(jackson2JsonRedisSerializer);
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

因此结果成功了:

另外网上也有:使用redisTemplate.execute(new RedisCallback<Boolean>() {});的方式,但是会存在,获取锁失败是仍然修改锁超时时间的问题,因此不采取

public boolean setIfAbsent(final String key, final Serializable value, final long exptime) {
        Boolean b = (Boolean) redisTemplate.execute(new RedisCallback<Boolean>() {
            @Override
            public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
                RedisSerializer valueSerializer = redisTemplate.getValueSerializer();
                RedisSerializer keySerializer = redisTemplate.getKeySerializer();
                Object obj = connection.execute("setnx", keySerializer.serialize(key),
                                                valueSerializer.serialize(value),
                                                SafeEncoder.encode("NX"),
                                                SafeEncoder.encode("EX"),
                                                Protocol.toByteArray(exptime));
                return obj != null;
            }
        });
        return b;
    }

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Redis分布式锁是一种常用的解决方案,用于在分布式环境中实现互斥操作。下面是一个简单的Redis分布式锁的应用场景和实现方法: 场景:假设有多个服务或进程需要同时访问某个共享资源,并且需要保证同一时刻只有一个服务或进程能够访问该资源,其他服务或进程需要等待。 实现方法: 1. 使用RedisSETNX命令(SET if Not eXists)来尝试获取锁。这个命令会将一个键值对设置到Redis中,如果该键不存在,则成功获取锁。 2. 如果获取到了锁,执行业务逻辑,完成后释放锁。 3. 如果未能获取到锁,可以选择等待一段时间后再次尝试,或者放弃获取锁。 下面是一个简单的示例代码(使用Python Redis客户端): ```python import redis import time def acquire_lock(conn, lock_name, acquire_timeout=10, lock_timeout=10): end = time.time() + acquire_timeout lock_value = str(time.time() + lock_timeout + 1) while time.time() < end: if conn.setnx(lock_name, lock_value): conn.expire(lock_name, lock_timeout) return lock_value elif conn.ttl(lock_name) == -1: conn.expire(lock_name, lock_timeout) time.sleep(0.001) return False def release_lock(conn, lock_name, lock_value): pipe = conn.pipeline(True) while True: try: pipe.watch(lock_name) if pipe.get(lock_name).decode() == lock_value: pipe.multi() pipe.delete(lock_name) pipe.execute() return True pipe.unwatch() break except redis.exceptions.WatchError: pass return False # 示例用法 conn = redis.Redis(host='localhost', port=6379) lock_name = 'my_lock' lock_value = acquire_lock(conn, lock_name) if lock_value: try: # 执行业务逻辑 print('Doing something...') finally: release_lock(conn, lock_name, lock_value) else: print('Failed to acquire lock') ``` 上述代码中,`acquire_lock`函数尝试获取锁,`release_lock`函数释放锁。其中使用了Redis事务操作(pipeline)来确保原子性。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值