准备环境

  • 虚拟机或者服务器上有Redis
  • Maven
  • eclipse或者idea
  • JDK8

添加依赖

<dependency>
	<groupId>redis.clients</groupId>
	<artifactId>jedis</artifactId>
	<version>3.2.0</version>
</dependency>

为什么要做分布式锁

例如一个简单用户的操作,一个线程去修改用户状态,首先在在内存中读出用户的状态,然后在内存中进行修改,然后在存到数据库中。在单线程中,这是没有问题的。但是在多线程中由于读取,修改,写入是三个操作,不是原子操作(同时成功或失败),因此在多线程中会存在数据的安全性问题。

实现思路

就是进来一个先占位,当别的线程进来操作的时候,发现有人占位了,就会放弃或者稍后再试。

在redis中的setnx命令来实现,默认set命令就是存值,当key存在的时候,set就会覆盖key的value值,而setnx则不会。当没有key的时候,setnx就会进来先占位,当key存在了,其他的setnx就进不来了。等到第一个执行完成后,在del命令释放位子。

实现代码

CallWithJedis

package com.leo.study.utils;

import redis.clients.jedis.Jedis;

public interface CallWithJedis {
	void call(Jedis jedis);
}

Redis

package com.leo.study.utils;

import org.apache.commons.pool2.impl.GenericObjectPoolConfig;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

public class Redis {
	private JedisPool jedisPool;

	public Redis() {
		GenericObjectPoolConfig<Object> config = new GenericObjectPoolConfig<>();
		// 设置最大空闲数
		config.setMaxIdle(300);
		// 设置最大连接数
		config.setMaxTotal(1000);
		// 设置最大等待时间
		config.setMaxWaitMillis(30000);
		// 空闲时检查有效性
		config.setTestOnBorrow(true);
		
		jedisPool = new JedisPool(config, "192.168.0.118", 6379);
//		jedisPool = new JedisPool(config, "127.0.0.1", 6379);
	}

	public void execute(CallWithJedis callWithJedis) {
		try (Jedis jedis = jedisPool.getResource()) {
			callWithJedis.call(jedis);
		}
	}
}

网上好些例子,都缺少这两段代码,只有关键实现的部分,可以参考https://www.cnblogs.com/javazl/p/12661730.html
这篇文章比较具有参考性,我这边就直接写最后的解决方法了
在测试类里面写

	public void testLock() {
		Redis redis = new Redis();
		redis.execute(jedis -> {
			String set = jedis.set("k1", "v1", new SetParams().nx().ex(5));
			System.out.println(set);
			if (set != null && "OK".equals(set)) {
				// 给锁添加一个过期时间,防止应用在运行过程中抛出异常导致锁无法及时得到释放
				jedis.expire("k1", 5);
				jedis.set("name", "javaboy");
				String name = jedis.get("name");
				System.out.println(name);
				jedis.del("k1");// 释放资源
			}

		});
	}

注意OK是大写的

用过期时间优化后,虽然解决了死锁的问题,但是又有一个新的问题产生,就是超时问题:

举个例子:如果要执行的业务很耗时,可能会出现紊乱,当地一个线程获取到锁的时候,开始执行业务代码,但是业务代码很耗时,假如过期时间是3秒,而业务执行需要5秒,这样,锁就会提前释放,然后第二个线程获取到锁并开始执行。当执行到第2秒的时候,第一个锁也执行完了,此时第一个线程会释放第二个线程的锁,然后第三个线程继续获取锁并执行,当到第3秒的时候第二个线程执行完了,那么又会提前释放锁,一直如此循环,会造成线程的紊乱。

那么解决的思路主要有两种

尽量避免耗时操作。
去处理锁,给锁的value设置随机数或随机字符串,每当要释放的时候去判断这个value的值,如果是的话就去释放,如果不是就不释放,举个例子,假设第一个线程进来,它获取锁的value是1,如果发生超时就会进入下一个线程,下一个线程会获取新的value为3,在释放第二个所之前先去获取value并比较,发现1不等于三,那么就不去释放锁。
第一种的话没啥说的,但是第二种的话会有一个问题,就是释放锁会查看value,然后比较,然后释放,会有三个操作,那么就不具备原子性,这样操作的话,会出现死锁。这里我们可以使用Lua脚本去处理。

Lua脚本的特点:
1.使用方便,redis内置了对Lua脚本的支持。
2.Lua可以在redis服务端原子性的执行多个redis命令
3.由于网络的原因会影响到redis的性能,因此,使用Lua可以让多个命令同时执行,降低了网络给redis带来的性能问题。
在redis中如何使用Lua脚本:
1.在redis服务端写好,然后在java业务中调用脚本
2.可以直接在java中直接去写,写好后,需要执行时,每次将脚本发送到redis中去执行。

创建release.lua脚本:

//用redis.call调用一个redis命令,调的是get命令,这个key是从外面传进来的key
if redis.call("get",KEYS[1])==ARGV[1] then//如果相等就去操作释放命令
   return redis.call("del",KEYS[1])
else
  return 0
end

脚本建议创在/usr/local/redis/lua,也就是redis目录的下一级,创建一个lua的文件夹,里面存放lua脚本

可以给Lua脚本求一个SHA1和:

cat lua/release.lua | redis-cli -a root script load --pipe

b8059ba43af6ffe8bed3db65bac35d452f8115d8

在测试类里面写

	public void luaTest() {
		Redis redis = new Redis();
		for (int i = 0; i < 2; i++) {
			redis.execute(jedis -> {
				// 1.先获取一个随机字符串
				String value = UUID.randomUUID().toString();
				String set = jedis.set("k1", value , new SetParams().nx().ex(5));
				System.out.println("luaTest-->" + set);
				if (set != null && "OK".equals(set)) {
					// 4. 具体的业务操作
					jedis.set("site", "www.javaboy.org");
					String site = jedis.get("site");
					System.out.println(site);
					// 5.释放锁
					jedis.evalsha("b8059ba43af6ffe8bed3db65bac35d452f8115d8", Arrays.asList("k1"),
							Arrays.asList(value));
				} else {
					System.out.println("没拿到锁!!!");
				}
			});
		}

	}

资料来自江南一点雨,本人已付费购买!

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐