redis集群分布式锁的实现与思考

分布式锁能帮助我们在分布式系统共享数据的并发中避免并发问题,要实现分布式锁必须保证加锁和解锁的操作是原子性的,redis的setnx和del刚好满足加锁和解锁的要求,下面使用setnx来实现可以超时的分布式锁:

import org.jmqtt.common.config.StoreConfig;
import redis.clients.jedis.JedisCluster;

import java.util.Random;

public class RedisLock {
    private StoreConfig redisConfig;
    private RedisStoreManager redisStoreManager;
    public static JedisCluster cluster;
    private static final int DEFAULT_LOCK_EXPIRSE_MILL_SECONDS =  30 * 1000;
    private static final int DEFAULT_LOCK_WAIT_DEFAULT_TIME_OUT = 10 * 1000;
    private static final int DEFAULT_LOOP_WAIT_TIME = 150;
    private static final String LOCK_PREFIX = "LOCK:";
    private boolean lock = false;
    private String lockKey;
    private int lockExpirseTimeout;
    private int lockWaitTimeout;

    public boolean isLock(){
        return lock;
    }

    public void setLock(boolean lock) {
        this.lock = lock;
    }

    public String getLockKey() {
        return lockKey;
    }

    public void setLockKey(String lockKey) {
        this.lockKey = LOCK_PREFIX + lockKey;
    }

    public int getLockExpirseTimeout() {
        return lockExpirseTimeout;
    }

    public void setLockExpirseTimeout(int lockExpirseTimeout) {
        this.lockExpirseTimeout = lockExpirseTimeout;
    }

    public int getLockWaitTimeout() {
        return lockWaitTimeout;
    }

    public void setLockWaitTimeout(int lockWaitTimeout) {
        this.lockWaitTimeout = lockWaitTimeout;
    }

    public RedisLock() {
    }

    public RedisLock(String lockKey, int lockExpirseTimeout, int lockWaitTimeout) {
        redisConfig = new StoreConfig();
        this.redisStoreManager = RedisStoreManager.getInstance(redisConfig);
        redisStoreManager.initialization();
        this.cluster = redisStoreManager.getCluster();
        this.lockKey = LOCK_PREFIX + lockKey;
        this.lockExpirseTimeout = lockExpirseTimeout;
        this.lockWaitTimeout = lockWaitTimeout;
    }

    public boolean lock(){
        int timeout = lockWaitTimeout;
        while (timeout >= 0){
            Long expires = System.currentTimeMillis() + lockExpirseTimeout + 1;
            String expiresStr = String.valueOf(expires);
            if (cluster.setnx(lockKey,expiresStr)==1){
                lock = true;
                return true;
            }
            String lockTimeStr = cluster.get(lockKey);
            if (lockTimeStr != null && Long.parseLong(lockTimeStr) < System.currentTimeMillis()){
                String oldLockTimeStr = cluster.getSet(lockKey,expiresStr);
                if (oldLockTimeStr != null && oldLockTimeStr.equals(lockTimeStr)){
                    lock = true;
                    return true;
                }
            }
            int sleepTime=new Random().nextInt(10)*100;
            timeout -= sleepTime;
            try {
                Thread.sleep(sleepTime);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }


        return false;
    }

    public void unlock(){
        if (lock){
            cluster.del(lockKey);
            lock = false;
        }

    }
}

把锁的超时时间当作value的值,每次线程需要锁,如果别的线程没有拥有锁,它立马获得锁,如果别的线程已经拥有锁了,就把超时时间取出来与系统现在的时间对比看是否超时了,如果超时了就尝试获得该锁,如果锁还没超时和释放,就休眠随机的时间,再尝试获得锁直到获得锁或timeout。
到这里只是实现,那思考去哪了呢,下面是当初我在测试这个锁的时候的一个错误的做法。
在这里插入图片描述
为了想保持最新的一定数量的值,避免在并发下lpush方法执行了但ltrim还没执行的时候取数据,这时候肯定比规定的数据要多的情况,这里使用了锁,然后测试代码如下:

public void RedisTest3() throws InterruptedException {
        final CountDownLatch count = new CountDownLatch(150);
        StoreConfig redisConfig = new StoreConfig();
        redisConfig.setNodes("集群节点***.*.*.*:***");
        RedisStoreUtil redisStoreUtil = new RedisStoreUtil(redisConfig,"llock");
        redisStoreUtil.delete("150");
        Message message = new Message();
        ThreadPoolExecutor executor = new ThreadPoolExecutor(6, 10, 2000, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>(1000));
        for (int i =0;i< 150;i++){
//            Thread t = new Thread(new MyTask(i,redisStoreUtil,count));
//            t.start();
            MyTask mt = new MyTask(i,redisStoreUtil,count);
            executor.execute(mt);


        }

            count.await();
            executor.shutdown();
            Collection<Message> al = redisStoreUtil.lgetAllMsg("150",100);
            System.out.println(al.size());
            Iterator it = al.iterator();
            while (it.hasNext()){
                System.out.println(it.next().toString());
            }


class MyTask implements Runnable {
    private int taskNum;
    private RedisStoreUtil redisStoreUtil;
    private Message message = new Message();
    private CountDownLatch count;
    public MyTask(int num,RedisStoreUtil redisStoreUtil,CountDownLatch count) {
        this.taskNum = num;
        this.redisStoreUtil = redisStoreUtil;
        this.count = count;
    }

    @Override
    public void run() {
        System.out.println("正在执行task "+taskNum);
        message.setMsgId(taskNum);
        redisStoreUtil.laddMsg(100,"150",message);  //调用上面加锁的方法
        count.countDown();
        }
    }
    }

这里使用了线程池来测试,往里面存150个数据,用不同的ID号代表不同的数据,保留最新的100个数据,等待所有线程结束再取数据,看上去貌似没有问题,然后问题来了,最后取出来的数据确实是100个,但最后的几个的ID都是一样的。
在这里插入图片描述
这个是我保存最新的10个数据时的测试图,难道是我写的锁实现有问题?没锁住,还是我的测试方式有问题?后来我终于发现问题所在了,之前为了偷懒只生成一个Message,通过修改它的ID来当作不同的数据,因为等待锁需要耗费时间,所以在 redisStoreUtil.laddMsg(100,“150”,message); 这个地方停得比较久,本来应该是这样的
在这里插入图片描述
但是机器却等不及了,先执行下一个线程task,从而修改了上一个线程本来要存的数据,变成了这个样子
在这里插入图片描述
所以结果才会那么多个最后一个ID的数据,如果后面的操作不会影响前面要存入的数据,那么就不会发生这种情况。
这个redisLock代码并没有放到GitHub上,因为加锁太影响性能了,加了锁之后慢了10几秒,只要在取的时候限定取多少个就不会发生多取的情况,具体的代码和操作细节可联系我。如果您对我们的JMQTT项目感兴趣的话可以在GitHub上watch和star一下,地址:https://github.com/Cicizz/jmqtt

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Redis 支持分布式锁,可以利用 Redis 的原子性操作和 Lua 脚本来实现。以下是一个基于 Redis 集群分布式锁的 Python 代码示例: ```python import redis import time class RedisLock: def __init__(self, redis_cluster, key, expire=10): self.redis_cluster = redis_cluster self.key = key self.expire = expire def lock(self): while True: timestamp = int(time.time() * 1000) result = self.redis_cluster.set(self.key, timestamp, nx=True, px=self.expire) if result: return True time.sleep(0.1) def unlock(self): lua_script = """ if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end """ self.redis_cluster.eval(lua_script, 1, self.key, self.redis_cluster.get(self.key)) if __name__ == '__main__': redis_cluster = redis.RedisCluster( startup_nodes=[ {'host': '127.0.0.1', 'port': 6379}, {'host': '127.0.0.1', 'port': 6380}, {'host': '127.0.0.1', 'port': 6381}, ], decode_responses=True ) lock = RedisLock(redis_cluster, 'lock_key') if lock.lock(): try: # do something pass finally: lock.unlock() ``` 在上面的代码中,我们使用 Redis 的 `set` 命令来获取锁,设置 `nx` 参数为 `True` 表示只有当锁不存在时才能获取锁,避免出现多个客户端同时占用锁的情况。同时设置 `px` 参数表示锁的过期时间。如果获取锁失败,我们使用 `time.sleep` 函数等待一段时间后再次尝试获取锁。 在释放锁时,我们使用 Lua 脚本来保证原子性。在脚本中,我们首先判断当前锁是否被当前客户端占用,如果是,则删除锁并返回 1;否则返回 0。 注意,该代码示例仅适用于 Redis 集群模式,如果使用单机版 Redis,需要将 `redis.RedisCluster` 替换为 `redis.Redis`。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值