暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

redis实现分布式锁

程序员恰恰 2022-08-11
21

1、 概念

对于简单的单体项目,即运行时程序在同一个Java虚拟机中,使用Java的锁机制(synchronized或者ReentrantLock)可以解决多线程并发问题。。

可以发现对于单实例的应用来说,使用Java锁机制就可以解决线程并发问题。对于分布式集群环境的系统来说,java的锁机制就不起作用了。


我们可以通过启动两个服务实例来测试集群部署时线程并发问题,具体测试步骤如下:

第一步:分别启动两个redis-lock-demo服务实例,端口号分别为8001和8002

第二步:配置Nginx负载均衡,通过Nginx将压力分发到两个实例上
upstream upstream_name{
server 127.0.0.1:8001;
server 127.0.0.1:8002;
}

server {
listen 8080;
server_name localhost;

location {
proxy_pass http://upstream_name;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

复制

使用Apache jmeter进行压力测试,可以发现对于集群环境下的多个服务实例又产生了线程并发问题。

集群部署方式依然会产生线程并发问题,因为synchronized、ReentrantLock只是jvm级别的加锁,没有办法控制其他jvm。也就是上面两个tomcat实例还是可以出现并发执行的情况。要解决分布式环境下的并发问题,则必须使用分布式锁。


分布式锁可以理解为:控制分布式系统有序的去对共享资源进行操作,通过互斥来保证数据的一致性。


分布式锁,是控制分布式系统之间同步访问共享资源的一种方式。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。


实现分布式锁的方式很多,例如:Redis、数据库、Zookeeper等。

2、 SETNX

这种加锁的思路是,如果 key 不存在则为 key 设置 value,如果 key 已存在则 SETNX 命令不做任何操作

  • 客户端A请求服务器设置key的值,如果设置成功就表示加锁成功
  • 客户端B也去请求服务器设置key的值,如果返回失败,那么就代表加锁失败
  • 客户端A执行代码完成,删除锁
  • 客户端B在等待一段时间后再去请求设置key的值,设置成功
  • 客户端B执行代码完成,删除锁

格式

#尝试获取锁
127.0.0.1:6379> SETNX key value
#设置锁过期时间
127.0.0.1:6379> EXPIRE key seconds
#删除锁
127.0.0.1:6379> DEL key

复制

为什么要设置key过期时间呢?

如果某个客户端获得锁后因为某些原因意外退出了,导致创建了锁但是没有来得及删除锁,那么这个锁将一直存在,后面所有的客户端都无法再获得锁,所以必须要设置过期时间。

3 、SET

通过前面的expire命令来设置锁过期时间还存在一个问题,就是SETNX和EXPIRE两个命令不是原子性操作。在极端情况下可能会出现获取锁后还没来得及设置过期时间程序就挂掉了,这样就又出现了锁一直存在,后面所有的客户端都无法再获得锁的问题。

如何解决这个问题?答案是使用SET命令。

SET 命令从Redis 2.6.12 版本开始包含设置过期时间的功能,这样获取锁和设置过期时间就是一个原子操作了。

格式

SET key value [EX seconds] [NX]

复制

示例

127.0.0.1:6379> SET mykey myvalue EX 5 NX

复制
  • EX seconds :将键的过期时间设置为 seconds 秒
  • NX :只在key不存在时才对键进行设置操作

4 、代码实现

基于redis实现分布式锁:

package com.itheima.lock;


import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.RedisConnectionUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.stereotype.Repository;
import java.nio.charset.Charset;
import java.util.UUID;
import java.util.concurrent.TimeUnit;


/**
 * 基于redis实现分布式锁
 */
@Repository
public class RedisLock {


    /**
     * 解锁脚本,原子操作
     */
    private static final String unlockScript =
            "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n"
                    + "then\n"
                    + "    return redis.call(\"del\",KEYS[1])\n"
                    + "else\n"
                    + "    return 0\n"
                    + "end";


    private RedisTemplate redisTemplate;


    public RedisLock(RedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }


    /**
     * 加锁,有阻塞
     *
     * @param name
     * @param expire
     * @param timeout
     * @return
     */
    public String lock(String name, long expire, long timeout) {
        long startTime = System.currentTimeMillis();
        String token;
        do {
            token = tryLock(name, expire);
            if (token == null) {
                if ((System.currentTimeMillis() - startTime) > (timeout - 50))
                    break;
                try {
                    Thread.sleep(50); //try 50 per sec
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    return null;
                }
            }
        } while (token == null);


        return token;
    }


    /**
     * 加锁,无阻塞
     *
     * @param name
     * @param expire
     * @return
     */
    public String tryLock(String name, long expire) {
        name = name + "_lock";
        String token = UUID.randomUUID().toString();
        RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
        RedisConnection conn = factory.getConnection();
        try {


            //参考redis命令:
            //set key value [EX seconds] [PX milliseconds] [NX|XX]
            Boolean result = conn.set(
                    name.getBytes(Charset.forName("UTF-8")),
                    token.getBytes(Charset.forName("UTF-8")),
                    Expiration.from(expire, TimeUnit.MILLISECONDS),
                    RedisStringCommands.SetOption.SET_IF_ABSENT //NX
            );
            if (result != null && result)
                return token;
        } finally {
            RedisConnectionUtils.releaseConnection(conn, factory,false);
        }
        return null;
    }


    /**
     * 解锁
     *
     * @param name
     * @param token
     * @return
     */
    public boolean unlock(String name, String token) {
        name = name + "_lock";
        byte[][] keysAndArgs = new byte[2][];
        keysAndArgs[0] = name.getBytes(Charset.forName("UTF-8"));
        keysAndArgs[1] = token.getBytes(Charset.forName("UTF-8"));
        RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
        RedisConnection conn = factory.getConnection();
        try {
            Long result = (Long) conn.scriptingCommands().eval(unlockScript.getBytes(Charset.forName("UTF-8")), ReturnType.INTEGER, 1, keysAndArgs);
            if (result != null && result > 0)
                return true;
        } finally {
            RedisConnectionUtils.releaseConnection(conn, factory,false);
        }


        return false;
    }
}

复制


点赞+在看,谢谢大家支持!
文章转载自程序员恰恰,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论