Redis 分布式锁实现

一、什么是分布式锁

分布式锁是控制分布式系统或不同系统之间共同访问共享资源的一种锁实现,如果不同的系统或同一个系统的不同主机之间共享了某个资源时,往往需要互斥来防止彼此干扰来保证一致性。

二、分布式锁需要具备哪些条件

  • 在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行
  • 高可用、高性能的获取锁与释放锁
  • 具备可重入特性
  • 具备锁失效机制、防止死锁
  • 具备非阻塞锁特性,即没有获取到锁直接返回获取锁失败

三、分布式锁的实现有哪些

  • 数据库
  • Redis(setnx命令)
  • Zookeeper(临时节点)

四、基于Redis的分布式锁

在 Redis 里,所谓 SETNX,是SET if Not eXists的缩写,也就是只有不存在的时候才设置,设置成功返回1,设置失败返回0

RedisLock 类如下:
@Service
public class RedisLock implements Lock {
	
	private static final String  KEY = "LOCK_KEY";
	
	@Resource
	private JedisConnectionFactory factory;

	private ThreadLocal<String> local = new ThreadLocal<>();
	
	
	@Override
	//阻塞式的加锁
	public void lock() {
		//1.尝试加锁
		if(tryLock()){
			return;
		}
		//2.加锁失败,当前任务休眠一段时间
		try {
			Thread.sleep(10);//性能浪费
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		//3.递归调用,再次去抢锁
		lock();
	}



	@Override
	//阻塞式加锁,使用setNx命令返回OK的加锁成功,并生产随机值
	public boolean tryLock() {
		//产生随机值,标识本次锁编号
		String uuid = UUID.randomUUID().toString();
		Jedis jedis = (Jedis) factory.getConnection().getNativeConnection();

		/**
		 * key:我们使用key来当锁
		 * uuid:唯一标识,这个锁是我加的,属于我
		 * NX:设入模式【SET_IF_NOT_EXIST】--仅当key不存在时,本语句的值才设入
		 * PX:给key加有效期
		 * 1000:有效时间为 1 秒
		 */
		String ret = jedis.set(KEY, uuid,"NX","PX",1000);

		//设值成功--抢到了锁
		if("OK".equals(ret)){
			local.set(uuid);//抢锁成功,把锁标识号记录入本线程--- Threadlocal
			return true;
		}

		//key值里面有了,我的uuid未能设入进去,抢锁失败
		return false;
	}

	//正确解锁方式
	public void unlock() {
		//读取lua脚本
		//String script = FileUtils.getScript("unlock.lua");
		String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then \n" +
				"    return redis.call(\"del\",KEYS[1]) \n" +
				"else \n" +
				"    return 0 \n" +
				"end";
		//获取redis的原始连接
		Jedis jedis = (Jedis) factory.getConnection().getNativeConnection();
		//通过原始连接连接redis执行lua脚本
		jedis.eval(script, Arrays.asList(KEY), Arrays.asList(local.get()));
	}

	//-----------------------------------------------

}

以上代码,根据setnx 加锁,设置keyuuidvalue 并使用uuid来标记抢到锁的请求存入本线程—Threadlocal

解锁是根据 lua脚本,利用存入Threadlocal中的uuid 和获得锁的 value 进行对比删掉对应的key 进行解锁。

模拟并发的使用示例:

CountDownLatch使一个线程等待其他线程各自执行完毕后再执行。

new CountDownLatch(5);等待个5个线程都执行到 countDownLatch.await();再往下执行。

@RestController
public class LockController {
    private static long count = 20;//黄牛
    private CountDownLatch countDownLatch = new CountDownLatch(5);

    @Resource(name="redisLock")
    private Lock lock;

	@ApiOperation(value="售票")
    @RequestMapping(value = "/sale", method = RequestMethod.GET)
    public Long sale() throws InterruptedException {
        count = 20;
        countDownLatch = new CountDownLatch(5);

        System.out.println("-------共20张票,分五个窗口开售-------");
        new PlusThread().start();
        new PlusThread().start();
        new PlusThread().start();
        new PlusThread().start();



        new PlusThread().start();
        return count;
    }

    // 线程类模拟一个窗口买火车票
    public class PlusThread extends Thread {
        private int amount = 0;//抢多少张票

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "开始售票");
            //每次减少一个容量
            countDownLatch.countDown();
            if (countDownLatch.getCount()==0){
                System.out.println("----------售票结果------------------------------");
            }
            try {
                //调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
                countDownLatch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            while (count > 0) {
                lock.lock();
                try {
                    if (count > 0) {
                        //模拟卖票业务处理
                        amount++;
                        count--;
                    }
                }finally{
                    lock.unlock();
                }

                try {
                    Thread.sleep(10);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + "售出"+ (amount) + "张票");
        }
    }
}

执行结果:

-------20张票,分五个窗口开售-------
Thread-13开始售票
Thread-17开始售票
Thread-14开始售票
Thread-15开始售票
Thread-16开始售票
----------售票结果------------------------------
Thread-15售出5张票
Thread-13售出6张票
Thread-17售出3张票
Thread-16售出3张票
Thread-14售出3张票

五、存在的问题

1、数据库事务超时

给这个方法添加一个@Transaction注解开启事务,如代码中抛出异常进行回滚,要知道数据库事务可是有超时时间限制的,并不会无条件的一直等一个耗时的数据库操作。

比如:我们解析一个大文件,再将数据存入到数据库,如果执行时间太长,就会导致事务超时自动回滚。

一旦你的key长时间获取不到锁,获取锁等待的时间远超过数据库事务超时时间,程序就会报异常。

一般为解决这种问题,我们就需要将数据库事务改为手动提交、回滚事务。

2、锁过期了,业务还没执行完

为了解决这个问题我们使用redis客户端redissonredisson很好的解决了redis在分布式环境下的一些棘手问题,它的宗旨就是让使用者减少对Redis的关注,将更多精力用在处理业务逻辑上。

redisson对分布式锁做了很好封装,只需调用API即可。

  RLock lock = redissonClient.getLock("stockLock");

redisson在加锁成功后,会注册一个定时任务监听这个锁,每隔10秒就去查看这个锁,如果还持有锁,就对过期时间进行续期。默认过期时间30秒。这个机制也被叫做:看门狗

举例子:假如加锁的时间是30秒,过10秒检查一次,一旦加锁的业务没有执行完,就会进行一次续期,把锁的过期时间再次重置成30秒。

Redis 分布式锁实现方法有多种,以下是一些常见方案: ### SETNX + EXPIRE 方案 使用 `SETNX` 命令尝试获取锁,若返回 `1` 表示获取成功,接着使用 `EXPIRE` 命令为锁设置过期时间,防止死锁。 ```python import redis redis_client = redis.Redis(host='localhost', port=6379, db=0) redis_key = "redis_key_001" get_lock = redis_client.setnx(redis_key, "极简版redis锁") if get_lock: redis_client.expire(redis_key, 10) # 设置 10 秒过期时间 print("获取到redis锁") # 业务逻辑 redis_client.delete(redis_key) else: print("未获取到redis锁") ``` ### SET 的扩展命令(SET EX PX NX)方案 使用 `SET` 命令的扩展参数,原子性地完成设置键值和过期时间,避免 `SETNX` 和 `EXPIRE` 非原子操作的问题。 ```python import redis redis_client = redis.Redis(host='localhost', port=6379, db=0) redis_key = "redis_key_001" get_lock = redis_client.set(redis_key, "极简版redis锁", ex=10, nx=True) if get_lock: print("获取到redis锁") # 业务逻辑 redis_client.delete(redis_key) else: print("未获取到redis锁") ``` ### 使用 Lua 脚本(包含 SETNX + EXPIRE 两条指令)方案 通过 Lua 脚本保证 `SETNX` 和 `EXPIRE` 操作的原子性。 ```python import redis redis_client = redis.Redis(host='localhost', port=6379, db=0) redis_key = "redis_key_001" lua_script = """ if redis.call('SETNX', KEYS[1], ARGV[1]) == 1 then redis.call('EXPIRE', KEYS[1], tonumber(ARGV[2])) return 1 else return 0 end """ get_lock = redis_client.eval(lua_script, 1, redis_key, "极简版redis锁", 10) if get_lock: print("获取到redis锁") # 业务逻辑 redis_client.delete(redis_key) else: print("未获取到redis锁") ``` ### SET EX PX NX + 校验唯一随机值,再释放锁方案 在加锁时设置唯一随机值,释放锁时先校验该值,避免误解锁。 ```python import redis import uuid redis_client = redis.Redis(host='localhost', port=6379, db=0) redis_key = "redis_key_001" unique_value = str(uuid.uuid4()) get_lock = redis_client.set(redis_key, unique_value, ex=10, nx=True) if get_lock: print("获取到redis锁") # 业务逻辑 lua_script = """ if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end """ release_result = redis_client.eval(lua_script, 1, redis_key, unique_value) if release_result: print("释放锁成功") else: print("释放锁失败") else: print("未获取到redis锁") ``` ### 开源框架 Redisson 方案 Redisson 是一个基于 Redis 实现的分布式和可扩展的 Java 数据结构集合,提供了简单易用的分布式锁实现。 ```java import org.redisson.Redisson; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.redisson.config.Config; public class RedissonLockExample { public static void main(String[] args) { Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379"); RedissonClient redisson = Redisson.create(config); RLock lock = redisson.getLock("redis_key_001"); try { lock.lock(); System.out.println("获取到redis锁"); // 业务逻辑 } finally { lock.unlock(); System.out.println("释放锁成功"); } } } ``` ### 多机实现分布式锁 Redlock 方案 在多个 Redis 节点上获取锁,只有在大多数节点上都成功获取到锁,才认为加锁成功。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值