1.写在前面
Redis上篇博客讲了下集群的搭建,我们只知道Redis的集群中有16384个插槽,当我们录入一个值的时候,先对这个键进行hash的算法然后,然后对这个hash出来的数,进行取余,然后就得到一个0~16384中间的某个值,然后根据这个值找到对应的机器,将值写入到对应的Redis服务上,我们都知道Redis的客户端提供了一个重定向的功能,但是我们在java中使用jedis如何操作呢?这就是我们今天的博客的一部分的内容,同时会带大家写一个Redis的分布式的锁。
2.Redis集群下,Jedis如何录入数据?
我们首先来看下Redis集群下录入数据的原理吧!具体的如下:
从上面的示意图,我们可以一个key需要通过Redis的计算,才能知道插入那个机器上去,而Jedis刚开始是不知道,那么该怎么办呢?而重定向的功能只有Redis的客户端可以的。于是我们想出了一个方案:就是根据这些槽位进行对应的连接池的映射,然后录入的数据的时候,我们先计算出来这个key在哪个槽位 中,然后从Map的映射中,取出这个连接池,这样就不会出错了,而Jedis就是这样做,具体的流程如下:
思路和我说的差不多,那么我们怎么证明我们的思路是对的,那么当然需要我们去写代码去证明去了,具体的代码如下:
import redis.clients.jedis.*;
import java.util.*;
public class TestRedisCluster {
public static void main(String[] args) {
Set<HostAndPort> nodesList = new HashSet<>();
nodesList.add(new HostAndPort("192.168.181.6", 7000));
nodesList.add(new HostAndPort("192.168.181.6", 7001));
nodesList.add(new HostAndPort("192.168.181.6", 7002));
nodesList.add(new HostAndPort("192.168.181.6", 7003));
nodesList.add(new HostAndPort("192.168.181.6", 7004));
nodesList.add(new HostAndPort("192.168.181.6", 7005));
// Jedis连接池配置
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
// 最大空闲连接数, 默认8个
jedisPoolConfig.setMaxIdle(200);
// 最大连接数, 默认8个
jedisPoolConfig.setMaxTotal(1000);
//最小空闲连接数, 默认0
jedisPoolConfig.setMinIdle(100);
// 获取连接时的最大等待毫秒数(如果设置为阻塞时BlockWhenExhausted),如果超时就抛异常, 小于零:阻塞不确定的时间, 默认-1
jedisPoolConfig.setMaxWaitMillis(3000); // 设置2秒
//对拿到的connection进行validateObject校验
jedisPoolConfig.setTestOnBorrow(false);
JedisCluster jedisCluster = new JedisCluster(nodesList, 2000, 2000, 5, "123456", jedisPoolConfig);
Map<String, JedisPool> clusterNodes = jedisCluster.getClusterNodes();
clusterNodes.entrySet().forEach(System.out::println);
}
}
运行的结果如下:
可以为我们的每一个节点分配了一个JedisPool,当然还有一个map,存的是键值对是槽位和JedisPool,下面这部分的代码是从源码中拷贝出来,就不放上来了,大家可以看下运行结果,具体的如下:
从上面的运行的结果,我们可以得出如果槽位在一台Redis服务上,那么它的JedisPool的对象是一样的。那么我们来测试下我们写的代码吧,看看能不能添加东西到Redis的集群中,具体的代码如下:
import com.google.common.collect.Lists;
import com.ys.redis.MyRedisCluster;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.*;
import redis.clients.jedis.util.JedisClusterCRC16;
import java.util.*;
public class TestRedisCluster {
public static void main(String[] args) {
Logger logger = LoggerFactory.getLogger(TestRedisCluster.class);
Set<HostAndPort> nodesList = new HashSet<>();
nodesList.add(new HostAndPort("192.168.181.6", 7000));
nodesList.add(new HostAndPort("192.168.181.6", 7001));
nodesList.add(new HostAndPort("192.168.181.6", 7002));
nodesList.add(new HostAndPort("192.168.181.6", 7003));
nodesList.add(new HostAndPort("192.168.181.6", 7004));
nodesList.add(new HostAndPort("192.168.181.6", 7005));
// Jedis连接池配置
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
// 最大空闲连接数, 默认8个
jedisPoolConfig.setMaxIdle(200);
// 最大连接数, 默认8个
jedisPoolConfig.setMaxTotal(1000);
//最小空闲连接数, 默认0
jedisPoolConfig.setMinIdle(100);
// 获取连接时的最大等待毫秒数(如果设置为阻塞时BlockWhenExhausted),如果超时就抛异常, 小于零:阻塞不确定的时间, 默认-1
jedisPoolConfig.setMaxWaitMillis(3000); // 设置2秒
//对拿到的connection进行validateObject校验
jedisPoolConfig.setTestOnBorrow(false);
JedisCluster jedisCluster = new JedisCluster(nodesList, 2000, 2000, 5, "123456", jedisPoolConfig);
Map<String, JedisPool> clusterNodes = jedisCluster.getClusterNodes();
clusterNodes.entrySet().forEach(System.out::println);
while (true) {
try {
String s = UUID.randomUUID().toString();
jedisCluster.set("k" + s, "v" + s);
System.out.println(jedisCluster.get("k" + s));
Thread.sleep(1000);
}catch (Exception e) {
logger.error(e.getMessage());
}
}
}
}
运行的结果如下:
发现我们的循环写是没有问题,那么问题来了,如果我们是调用mset
这种一次写入多个键的,能不能成功呢?因为我们知道是先计算key的slot,然后再写入,那么从理论上来说,是可以实现,而且也不是很复杂,我们来测试一下,具体的如下:
import com.google.common.collect.Lists;
import com.ys.redis.MyRedisCluster;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.*;
import redis.clients.jedis.util.JedisClusterCRC16;
import java.util.*;
public class TestRedisCluster {
public static void main(String[] args) {
Logger logger = LoggerFactory.getLogger(TestRedisCluster.class);
Set<HostAndPort> nodesList = new HashSet<>();
nodesList.add(new HostAndPort("192.168.181.6", 7000));
nodesList.add(new HostAndPort("192.168.181.6", 7001));
nodesList.add(new HostAndPort("192.168.181.6", 7002));
nodesList.add(new HostAndPort("192.168.181.6", 7003));
nodesList.add(new HostAndPort("192.168.181.6", 7004));
nodesList.add(new HostAndPort("192.168.181.6", 7005));
// Jedis连接池配置
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
// 最大空闲连接数, 默认8个
jedisPoolConfig.setMaxIdle(200);
// 最大连接数, 默认8个
jedisPoolConfig.setMaxTotal(1000);
//最小空闲连接数, 默认0
jedisPoolConfig.setMinIdle(100);
// 获取连接时的最大等待毫秒数(如果设置为阻塞时BlockWhenExhausted),如果超时就抛异常, 小于零:阻塞不确定的时间, 默认-1
jedisPoolConfig.setMaxWaitMillis(3000); // 设置2秒
//对拿到的connection进行validateObject校验
jedisPoolConfig.setTestOnBorrow(false);
JedisCluster jedisCluster = new JedisCluster(nodesList, 2000, 2000, 5, "123456", jedisPoolConfig);
System.out.println(jedisCluster.mset("k1", "v1", "k2", "v2", "k3", "v3"));
}
}
运行的结果如下:
可以发现出错了,出错的原因是:无法将此命令分派到 Redis 集群,因为键具有不同的插槽。那么怎么办?这个时候我只有重写,具体的代码如下:
public class MyRedisCluster extends JedisCluster {
public MyRedisCluster(Set node, int connectionTimeout, int soTimeout, int maxAttempts, String password, GenericObjectPoolConfig poolConfig) {
super(node, connectionTimeout, soTimeout, maxAttempts, password, poolConfig);
}
@Override
public String mset(String... keyValues) {
for (int i = 0; i < keyValues.length; i++) {
if (i % 2 == 0) {
int slot = JedisClusterCRC16.getCRC16(keyValues[i]) % 16384;
Jedis connectionFromSlot = this.getConnectionFromSlot(slot);
System.out.println(keyValues[i] + "---" + keyValues[i + 1]);
System.out.println(connectionFromSlot.set(keyValues[i], keyValues[i + 1]));
connectionFromSlot.close();
}
}
return "OK";
}
}
上面的代码就是先计算这个键的slot,然后获取对应Jedis客户端,然后就可以设置对应的值。我们再来测试一下,具体的代码的如下:
import com.google.common.collect.Lists;
import com.ys.redis.MyRedisCluster;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.*;
import redis.clients.jedis.util.JedisClusterCRC16;
import java.util.*;
public class TestRedisCluster {
public static void main(String[] args) {
Logger logger = LoggerFactory.getLogger(TestRedisCluster.class);
Set<HostAndPort> nodesList = new HashSet<>();
nodesList.add(new HostAndPort("192.168.181.6", 7000));
nodesList.add(new HostAndPort("192.168.181.6", 7001));
nodesList.add(new HostAndPort("192.168.181.6", 7002));
nodesList.add(new HostAndPort("192.168.181.6", 7003));
nodesList.add(new HostAndPort("192.168.181.6", 7004));
nodesList.add(new HostAndPort("192.168.181.6", 7005));
// Jedis连接池配置
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
// 最大空闲连接数, 默认8个
jedisPoolConfig.setMaxIdle(200);
// 最大连接数, 默认8个
jedisPoolConfig.setMaxTotal(1000);
//最小空闲连接数, 默认0
jedisPoolConfig.setMinIdle(100);
// 获取连接时的最大等待毫秒数(如果设置为阻塞时BlockWhenExhausted),如果超时就抛异常, 小于零:阻塞不确定的时间, 默认-1
jedisPoolConfig.setMaxWaitMillis(3000); // 设置2秒
//对拿到的connection进行validateObject校验
jedisPoolConfig.setTestOnBorrow(false);
MyRedisCluster redisCluster = new MyRedisCluster(nodesList, 2000, 2000, 5, "123456", jedisPoolConfig);
System.out.println(redisCluster.mset("k1", "v1", "k2", "v2", "k3", "v3"));
}
}
运行的结果如下:
发现我们这儿就重写完成了。这个时候我们需要讲我们的Redis的分布式锁了。
3.Redis分布式锁
随着业务的复杂,传统的单体的项目已经无法满足我们现在的需求了,所以我们需要上分布式了,所以传统的jvm锁已经无法满足我们的需求了,需要有新的方案,那么有什么方案呢?既然原来的多台机器多台部署,所以加锁的时候无法是同一把锁,这个时候我们需要上升一个维度,就是找一个公用的地方,进行加锁,这样就不会有问题了,于是我们想到了Redis,那么用Redis,怎么实现加锁呢?首先我们需要了解几个命令,具体的如下:
setex 键 秒值 真实值 设置带过期时间的key,动态设置。
setnx key value 只有在 key 不存在时设置 key 的值。
我们可以用上面的API实现对应的加锁的设置,具体的如下图:
这个图看着似乎很完美,但是似乎有个很大的问题,就是如果线程A抢到了锁,然后执行下单操作的时候,直接崩了,这个时候锁也释放不了,这个时候就会造成死锁的现象了,其他的线程永远拿不到锁,线程A的下单操作也执行不下去。于是我们想到的第二版的解决方案,就是加一个过期时间,于是有了下面的图。
这个图似乎也没有啥问题,但是如果下单的操作的时间过长,那么这个时候这个lock的键失效了,那么这个时候线程B抢到的锁,然后线程A执行完了,意味着释放锁的时候del lock 释放的是线程B的锁,这样可不行,于是我们又改版。于是有了第三版,具体的如下:
这儿似乎没有问题,但是又有问题来了,如果我们的业务时间过长的话,大于lock过期的时间,这样同一时间段,就有多个线程来操作这个下单操作了,有人可能说,我把这个时间设置长一点,不就行了嘛!这样就不会导致多个线程加锁成功了,但是这样效率就变低了,所以这种方法似乎也是不行的,于是有了第四版,具体的如下:
第四版似乎很完美了,但是万一请求多,Redis直接挂了,似乎我们程序就直接崩了,现在全要高可用,所以这儿我们可以用多台Redis,组成一个集群,这样问题就会小很多,于是我们有了第五版的,具体的如下:
其实上面的几个版本的Redis分布式锁,有个框架叫redission
已经帮我们实现了,但是我们今天不打算讲这个,我们要实现一个分布式锁,用第四版的方案实现,先定义我们的接口类,具体的代码如下:
package com.ys;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock() throws Exception;
Condition newCondition();
}
我们这儿只定义了两个方法,真正的锁有很多的方法,但是由于篇幅的原因,所以我这儿就定义常用的两个方法,然后实现这个类,具体的如下:
package com.ys;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.params.SetParams;
import java.util.*;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Condition;
@Component
public class RedisLock implements Lock {
@Autowired
private JedisPool jedisPool;
private static final String key = "lock";
private ThreadLocal<String> threadLocal = new ThreadLocal<>();
private static AtomicBoolean isHappened = new AtomicBoolean(true);
//加锁
@Override
public void lock() {
boolean b = tryLock(); //尝试加锁
if (b) {
//拿到了锁 直接返回
return;
}
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
//一直递归到获取到锁
lock();
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
//尝试加锁
@Override
public boolean tryLock() {
//原子操作
SetParams setParams = new SetParams();
setParams.ex(2); //2s
setParams.nx();
String s = UUID.randomUUID().toString();
Jedis resource = jedisPool.getResource();
String lock = resource.set(key, s, setParams);
resource.close();
if ("OK".equals(lock)) {
//拿到了锁
threadLocal.set(s);
if (isHappened.get()) {
//设置守护线程
ThreadUtil.newThread(new MyRunnable(jedisPool)).start();
isHappened.set(false);
}
return true;
}
return false;
}
static class MyRunnable implements Runnable {
private JedisPool jedisPool;
public MyRunnable(JedisPool jedisPool) {
this.jedisPool = jedisPool;
}
@Override
public void run() {
Jedis jedis = jedisPool.getResource();
while (true) {
//获取过期时间
Long ttl = jedis.ttl(key);
//给过期时间加上1
if (ttl != null && ttl > 0) {
jedis.expire(key, (int) (ttl + 1));
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
//第一步判断设置时候的value 和 此时redis的value是否相同
//解锁
@Override
public void unlock() throws Exception {
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";
Jedis resource = jedisPool.getResource();
//比较key和值一样的,才删除
Object eval = resource.eval(script, Arrays.asList(key), Arrays.asList(threadLocal.get()));
if (Integer.parseInt(eval.toString()) == 0) {
resource.close();
throw new Exception("解锁失败");
}
resource.close();
}
@Override
public Condition newCondition() {
return null;
}
}
再来看这个工具类ThreadUti
l,具体的如下:
package com.ys;
public class ThreadUtil {
public static volatile Thread thread;
public static Thread newThread(Runnable runnable){
if (thread==null){
synchronized (ThreadUtil.class){
if(thread==null){
thread = new Thread(runnable);
//设置为守护线程
thread.setDaemon(true);
}
return thread;
}
}
return thread;
}
}
测试类如下:
package com.ys;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.redisson.Redisson;
import org.redisson.RedissonMultiLock;
import org.redisson.RedissonRedLock;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=SpringConfig.class)
public class Dome {
private int count=10;
@Autowired
@Qualifier("redisLock")
private com.ys.Lock lock;
@Test
public void Test() throws InterruptedException {
TicketsRunBle ticketsRunBle=new TicketsRunBle();
Thread thread1=new Thread(ticketsRunBle,"窗口1");
Thread thread2=new Thread(ticketsRunBle,"窗口2");
Thread thread3=new Thread(ticketsRunBle,"窗口3");
Thread thread4=new Thread(ticketsRunBle,"窗口4");
Thread thread5=new Thread(ticketsRunBle,"窗口5");
thread1.start();
thread2.start();
thread3.start();
thread4.start();
thread5.start();
Thread.currentThread().join();
}
public class TicketsRunBle implements Runnable{
@Override
public void run() {
while (count>0){
lock.lock();
try {
if(count>0){
//业务处理的时间大于超时的时间
Thread.sleep(ThreadLocalRandom.current().nextInt(2000, 3000));
System.out.println(Thread.currentThread().getName()+"售出第"+count--+"张票");
}
}catch (Exception e){
e.printStackTrace();
}finally {
try {
lock.unlock();
} catch (Exception e) {
e.printStackTrace();
}
}
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
运行的结果如下:
可以发现我们的第4版的Redis分布式锁实现完成。
4.写在最后
本篇博客主要介绍Redis的分布式锁和在Redis集群下,Jedis如何录入数据。