详细环境安装教程:点击打开链接
集群搭建
redis集群最小包含3个主节点,并且每个节点都应该部署在不同的服务器上,这里测试建立3个主节点和三个从节点的redis集群,并部署在本地机器上,分别监听不同的端口(7000,7001,7100,7101,7200,7201),7000、7100、7200为三个主节点,7001、7101、7201分别为它们对应的从节点。
在redis安装目录建立cluster文件夹,在这个目录下为这六个redis节点新建六个文件夹,名称为各自的端口名,再在建好的文件里创建如redis_7000.conf配置文件,内容为:
port 7000
#绑定监听的IP地址,默认为127.0.0.1
#bind 172.16.10.49
appendonly yes
appendfilename "appendonly_7000.aof"
# // 如果要设置最大内存空间,可添加如下两句
maxmemory 200mb
maxmemory-policy allkeys-lru
cluster-enabled yes
cluster-config-file nodes_7000.conf
cluster-node-timeout 15000
cluster-slave-validity-factor 10
cluster-migration-barrier 1
cluster-require-full-coverage yes
端口号7000都要改成各自对应的端口号,IP地址这里测试就用默认127.0.0.1,建议使用电脑的本地ip地址。cluster-config-file参数指定了redis集群节点的配置文件名称,创建集群后生成,内容如下:
0adf165a965376913d6a4d426784371e6b0d7cf6 127.0.0.1:7101 slave 2ec1abbf3bcd7758281bcc7b6762532084c0b60c 0 1521124105758 5 connected
8d8f817d997b57ccb10049208738d5ebfdd5c2fe 127.0.0.1:7001 master - 0 1521124104759 7 connected 0-5460
9334416471942fd042f704ae8a345ab6c1adcd37 127.0.0.1:7201 slave b7fedf227aec4fc2911bdf7de2566b73e0a88045 0 1521124107358 9 connected
1030b137672c69175e7cf1d99be325329e016422 127.0.0.1:7000 myself,slave 8d8f817d997b57ccb10049208738d5ebfdd5c2fe 0 0 1 connected
b7fedf227aec4fc2911bdf7de2566b73e0a88045 127.0.0.1:7200 master - 0 1521124100754 9 connected 10923-16383
2ec1abbf3bcd7758281bcc7b6762532084c0b60c 127.0.0.1:7100 master - 0 1521124106758 2 connected 5461-10922
vars currentEpoch 9 lastVoteEpoch 0
如果配置集群后,想修改各节点的信息,如IP地址端口等,可以在这里修改,但必须每个节点下的都要改。
然后使用如下命令安装各服务,并启动
安装 redis-server --service-install cluster/7100/redis_7100.conf --service-name redis7100
启动 redis-server --service-start --service-name redis7100
删除服务 redis-server --service-uninstall --service-name redis7100
现在已经启动了六个redis服务,但六个redis并没有联系,没有实现集群。
创建集群:
ruby redis-trib.rb create --replicas 1 127.0.0.1:700 0 127.0.0.1:7100 127.0.0.1:7200 127.0.0.1:7001 127.0.0.1:7101 127.0.0.1:7201
使用redis-trib.rb命令创建集群,ruby可以去掉。命令运行成功,则集群就创建成功了。
连接集群:
redis-cli -c -p 7200 -h 127.0.0.1
这里连接任何一个节点都可以。
redis集群工作
set keya valuea
1、先对keya 计算值
2、对16384(总槽点数)取余(得到槽点)
3、通过槽点找到对应的节点
4、在这个节点执行set keya valuea
节点,分为主节点和从节点,一个主节点下可以有多个从节点。槽点值分配在主节点上,redis中的key-value就是
存储在槽点上。
当主节点挂掉好,选举一个从节点成为主节点,若该主节点没有从节点,则集群处于fail状态,或有半数以上的主
节点挂掉,集群也处于fail状态
主节点会把key—value写入从节点
redis分布式的实现原理:
1、通过setNX操作,如果存在key,不操作;不存在,才会set值,保证锁的互斥性
2、value设置锁的到期时间,当锁超时时,进行getAndSet操作,先get旧值,再set新值,避免发生死锁。这里也可以通过设置key的有效期来避免死锁,但是setNx和exprise(设置有效期)操作非原子性,可能发生锁没有设置有效时间的问题,从而发生死锁。
实现:
spring boot 通过jdeis连接redsi集群
redis配置文件:
default.redis.maxRedirects=3
#连接池中最大连接数。高版本:maxTotal,低版本:maxActive
default.redis.maxTotal=20
#连接池中最大空闲的连接数
default.redis.maxIdle=10
#连接池中最少空闲的连接数
default.redis.minIdle=1
#当连接池资源耗尽时,调用者最大阻塞的时间,超时将跑出异常。单位,毫秒数;默认为-1.表示永不超时。高版本:maxWaitMillis,低版本:maxWait
default.redis.maxWaitMillis=3000
#连接空闲的最小时间,达到此值后空闲连接将可能会被移除。负值(-1)表示不移除
default.redis.minEvictableIdleTimeMillis=-1
#对于“空闲链接”检测线程而言,每次检测的链接资源的个数。默认为3
default.redis.numTestsPerEvictionRun=3
#“空闲链接”检测线程,检测的周期,毫秒数。如果为负值,表示不运行“检测线程”。默认为-1
default.redis.timeBetweenEvictionRunsMillis=-1
#向调用者输出“链接”资源时,是否检测是有有效,如果无效则从连接池中移除,并尝试获取继续获取。默认为false。建议保持默认值
default.redis.testOnBorrow=false
#连超时设置
default.redis.timeout=15000
#是否使用连接池
default.redis.usePool=true
#host&port
# 建议使用实际ip地址创建集群
default.redis.nodes[0]=127.0.0.1:7000
default.redis.nodes[1]=127.0.0.1:7001
default.redis.nodes[2]=127.0.0.1:7100
default.redis.nodes[3]=127.0.0.1:7101
default.redis.nodes[4]=127.0.0.1:7200
default.redis.nodes[5]=127.0.0.1:7201
通过@ConfigurationProperties注解读取配置信息:
@Component
@ConfigurationProperties(prefix = "default.redis")
public class RedisProperties {
private int maxRedirects;
private int maxTotal;
private int maxIdle;
private int minIdle;
private int maxWaitMillis;
private int minEvictableIdleTimeMillis;
private int numTestsPerEvictionRun;
private int timeBetweenEvictionRunsMillis;
private boolean testOnBorrow;
private int timeout;
private boolean usePool;
private List nodes;
public int getMaxRedirects() {
return maxRedirects;
}
public void setMaxRedirects(int maxRedirects) {
this.maxRedirects = maxRedirects;
}
public int getMaxTotal() {
return maxTotal;
}
public void setMaxTotal(int maxTotal) {
this.maxTotal = maxTotal;
}
public int getMaxIdle() {
return maxIdle;
}
public void setMaxIdle(int maxIdle) {
this.maxIdle = maxIdle;
}
public int getMinIdle() {
return minIdle;
}
public void setMinIdle(int minIdle) {
this.minIdle = minIdle;
}
public int getMaxWaitMillis() {
return maxWaitMillis;
}
public void setMaxWaitMillis(int maxWaitMillis) {
this.maxWaitMillis = maxWaitMillis;
}
public int getMinEvictableIdleTimeMillis() {
return minEvictableIdleTimeMillis;
}
public void setMinEvictableIdleTimeMillis(int minEvictableIdleTimeMillis) {
this.minEvictableIdleTimeMillis = minEvictableIdleTimeMillis;
}
public int getNumTestsPerEvictionRun() {
return numTestsPerEvictionRun;
}
public void setNumTestsPerEvictionRun(int numTestsPerEvictionRun) {
this.numTestsPerEvictionRun = numTestsPerEvictionRun;
}
public int getTimeBetweenEvictionRunsMillis() {
return timeBetweenEvictionRunsMillis;
}
public void setTimeBetweenEvictionRunsMillis(int timeBetweenEvictionRunsMillis) {
this.timeBetweenEvictionRunsMillis = timeBetweenEvictionRunsMillis;
}
public boolean isTestOnBorrow() {
return testOnBorrow;
}
public void setTestOnBorrow(boolean testOnBorrow) {
this.testOnBorrow = testOnBorrow;
}
public int getTimeout() {
return timeout;
}
public void setTimeout(int timeout) {
this.timeout = timeout;
}
public boolean isUsePool() {
return usePool;
}
public void setUsePool(boolean usePool) {
this.usePool = usePool;
}
public List getNodes() {
return nodes;
}
public void setNodes(List nodes) {
this.nodes = nodes;
}
}
redis配置类,生成redisTemplate
@Configuration
public class RedisConfig {
@Autowired
private RedisProperties redisProperties;
@Bean
public RedisClusterConfiguration redisClusterConfiguration() {
RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration();
redisClusterConfiguration.setClusterNodes(getRedisNode());
redisClusterConfiguration.setMaxRedirects(redisProperties.getMaxRedirects());
return redisClusterConfiguration;
}
@Bean
public JedisPoolConfig jedisPoolConfig() {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxIdle(redisProperties.getMaxIdle());
jedisPoolConfig.setMaxTotal(redisProperties.getMaxTotal());
jedisPoolConfig.setMinIdle(redisProperties.getMinIdle());
jedisPoolConfig.setMaxWaitMillis(redisProperties.getMaxWaitMillis());
jedisPoolConfig.setNumTestsPerEvictionRun(redisProperties.getNumTestsPerEvictionRun());
jedisPoolConfig.setTimeBetweenEvictionRunsMillis(redisProperties.getTimeBetweenEvictionRunsMillis());
jedisPoolConfig.setTestOnBorrow(redisProperties.isTestOnBorrow());
return jedisPoolConfig;
}
@Bean
public JedisConnectionFactory jedisConnectionFactory(RedisClusterConfiguration redisClusterConfiguration, JedisPoolConfig jedisPoolConfig) {
JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(redisClusterConfiguration, jedisPoolConfig);
jedisConnectionFactory.setTimeout(redisProperties.getTimeout());
return jedisConnectionFactory;
}
@Bean
public RedisTemplate redisTemplate(JedisConnectionFactory jedisConnectionFactory) {
RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(jedisConnectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
return redisTemplate;
}
private List getRedisNode() {
List nodes = redisProperties.getNodes();
if (CommonUtils.isNotEmpty(nodes)) {
List redisNodes = nodes.stream().map(node -> {
String[] ss = node.split(":");
return new RedisNode(ss[0], Integer.valueOf(ss[1]));
}).collect(Collectors.toList());
return redisNodes;
}
return new ArrayList<>();
}
}
redis锁的实现:
@Component
public class RedisLock {
private static final Logger log = LoggerFactory.getLogger(RedisLock.class);
/* 默认锁的有效时间30s */
private static final int DEFAULT_LOCK_EXPIRSE_MILL_SECONDS = 30 * 1000;
/* 默认请求锁等待超时时间10s */
private static final int DEFAULT_LOCK_WAIT_DEFAULT_TIME_OUT = 10 * 1000;
/* 默认的轮询获取锁的间隔时间 */
private static final int DEFAULT_LOOP_WAIT_TIME = 150;
/* 锁的key前缀 */
private static final String LOCK_PREFIX = "LOCK_";
/* 是否获得锁的标志 */
private boolean lock = false;
/* 锁的key */
private String lockKey;
/* 锁的有效时间(ms) */
private int lockExpirseTimeout;
/* 请求锁的阻塞时间(ms) */
private int lockWaitTimeout;
@Autowired
private RedisTemplate redisTemplate;
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 void setRedisTemplate(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
public RedisLock() {
}
public RedisLock(String lockKey, int lockExpirseTimeout, int lockWaitTimeout) {
this.lockKey = LOCK_PREFIX + lockKey;
this.lockExpirseTimeout = lockExpirseTimeout;
this.lockWaitTimeout = lockWaitTimeout;
}
public RedisLock newInstance(String lockKey) {
RedisLock redisLock = new RedisLock(lockKey, DEFAULT_LOCK_EXPIRSE_MILL_SECONDS, DEFAULT_LOCK_WAIT_DEFAULT_TIME_OUT);
redisLock.setRedisTemplate(this.redisTemplate);
return redisLock;
}
public RedisLock newInstance(String lockKey, int lockExpirseTimeout, int lockWaitTimeout) {
if (lockExpirseTimeout == 0 || lockWaitTimeout == 0) {
lockExpirseTimeout = DEFAULT_LOCK_EXPIRSE_MILL_SECONDS;
lockWaitTimeout = DEFAULT_LOCK_WAIT_DEFAULT_TIME_OUT;
}
RedisLock redisLock = new RedisLock(lockKey, lockExpirseTimeout, lockWaitTimeout);
redisLock.setRedisTemplate(this.redisTemplate);
return redisLock;
}
public boolean setIfAbsent(String expirseTimeStr) {
// setIfAbsent通过jedis的setNx实现
return this.redisTemplate.opsForValue().setIfAbsent(this.lockKey, expirseTimeStr);
}
public String getAndSet(String expiresTimeStr) {
// 获取原来的值,并设置新的值,原子操作
return (String) this.redisTemplate.opsForValue().getAndSet(this.lockKey, expiresTimeStr);
}
/**
* 1、获得当前系统时间,计算锁的到期时间
* 2、setNx操作,加锁
* 3、如果,加锁成功,设置锁的到期时间,返回true;取锁失败,取出当前锁的value(到期时间)
* 4、如果value不为空而且小于当前系统时间,进行getAndSet操作,重新设置value,并取出旧value;否则,等待间隔时间后,重复步骤2;
* 5、如果步骤3和4取出的value一样,加锁成功,设置锁的到期时间,返回true;否则,别人加锁成功,恢复锁的value,等待间隔时间后,重复步骤2。
*/
public boolean lock() {
log.info("{}-----尝试获取锁...", Thread.currentThread().getName());
int lockWaitMillSeconds = this.lockWaitTimeout;
// key 的值,表示key的到期时间
String redisValue = String.valueOf(System.currentTimeMillis() + this.lockExpirseTimeout);
while (lockWaitMillSeconds > 0) {
lock = setIfAbsent(redisValue);
if (lock) {
// 拿到锁,设置锁的有效期,这里可能因为故障没有被执行,锁会一直存在,这时就需要value的有效期去判断锁是否失效
this.redisTemplate.expire(this.lockKey, lockExpirseTimeout, TimeUnit.MILLISECONDS);
log.info("{}-----获得锁", Thread.currentThread().getName());
return lock;
} else {
// 锁存在,判断锁有没有过期
String oldValue = (String) this.redisTemplate.opsForValue().get(this.lockKey);
if (CommonUtils.isNotEmpty(oldValue) && Long.parseLong(oldValue) < System.currentTimeMillis()) {
// 锁的到期时间小于当前时间,说明锁已失效, 修改value,获得锁
String currentRedisValue = getAndSet(String.valueOf(lockExpirseTimeout + System.currentTimeMillis()));
// 如果两个值不相等,说明有另外一个线程拿到了锁,阻塞
if (currentRedisValue.equals(oldValue)) {
// 如果修改的锁的有效期之前没被其他线程修改,则获得锁, 设置锁的超时时间
redisTemplate.expire(this.lockKey, lockExpirseTimeout, TimeUnit.MILLISECONDS);
log.info("{}-----获得锁", Thread.currentThread().getName());
this.lock = true;
return this.lock;
} else {
// 有另外一个线程获得了这个超时的锁,不修改锁的value
redisTemplate.opsForValue().set(this.lockKey, currentRedisValue);
}
}
}
// 减掉固定轮询获取锁的间隔时间
lockWaitMillSeconds -= DEFAULT_LOOP_WAIT_TIME;
try {
log.info("{}-----等待{}ms后,再尝试获取锁...", Thread.currentThread().getName(), DEFAULT_LOOP_WAIT_TIME);
// 取锁失败时,应该在随机延时后进行重试,避免不同客户端同时重试导致谁都无法拿到锁的情况出现,也可以采用等待队列的方式
Thread.sleep(DEFAULT_LOOP_WAIT_TIME);
} catch (InterruptedException e) {
log.error("redis 同步锁出现未知异常", e);
}
}
log.info("{}-----请求锁超时,获得锁失败", Thread.currentThread().getName());
return false;
}
public void unlock() {
if (lock) {
this.redisTemplate.delete(this.lockKey);
this.lock = false;
}
}
}
加锁过程:
1、获得当前系统时间,计算锁的到期时间
2、setNx操作,加锁
3、如果,加锁成功,设置锁的到期时间,返回true;取锁失败,取出当前锁的value(到期时间)
4、如果value不为空而且小于当前系统时间,进行getAndSet操作,重新设置value,并取出旧value;否则,等待间隔时间后,重复步骤2;
5、如果步骤3和4取出的value一样,加锁成功,设置锁的到期时间,返回true;否则,别人加锁成功,恢复锁的value,等待间隔时间后,重复步骤2。
这里设置锁的到期时间,只是为了减少后面复杂逻辑的执行。
测试:
测试类
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = Application.class)
public class TestRedis {
private CountDownLatch countDownLatch = new CountDownLatch(2);
@Test
public void testRedisLock() throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
RedisLock lock = ((RedisLock) SpringContextUtil.getBean("redisLock")).newInstance("test");
if (lock.lock()) {
System.out.println("work1获得锁");
System.out.println("work1 工作15s...");
try {
Thread.sleep(15000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("work1完成工作,释放锁");
lock.unlock();
}
countDownLatch.countDown();
}
},"work1").start();
new Thread(new Runnable() {
@Override
public void run() {
RedisLock lock = ((RedisLock) SpringContextUtil.getBean("redisLock")).newInstance("test");
if (lock.lock()) {
System.out.println("work2获得锁");
System.out.println("work2 工作5s...");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("work2完成工作,释放锁");
lock.unlock();
}
countDownLatch.countDown();
}
}, "work2").start();
// 等待两个线程完成,才完成主线程
countDownLatch.await();
}
}
这里在单元测试中起两个线程work1和work2,work1模拟工作15s,work2模拟工作5s。这里为了juint的主线程不会在两个work线程完成工作之前就停止,用到了CountDownLatch,让主线程在两个work线程完成前等待。
上面测试有两种结果,第一种,work2先拿到锁,工作5s,work1等待(默认等待超时时间10s),等待过程中一值尝试获取锁(默认间隔150ms),默认锁的有效期30s,显然,5s后work2完成工作释放锁,work1获得锁,work1和work2都正常完成了工作。结果如下:
2018-03-16 14:16:50,579 5576 [work1] INFO c.s.component.redis.lock.RedisLock - work1-----尝试获取锁...
2018-03-16 14:16:50,579 5576 [work2] INFO c.s.component.redis.lock.RedisLock - work2-----尝试获取锁...
2018-03-16 14:16:50,598 5595 [work1] INFO c.s.component.redis.lock.RedisLock - work1-----等待150ms后,再尝试获取锁...
2018-03-16 14:16:50,599 5596 [work2] INFO c.s.component.redis.lock.RedisLock - work2-----获得锁
work2获得锁
work2 工作5s...
2018-03-16 14:16:50,748 5745 [work1] INFO c.s.component.redis.lock.RedisLock - work1-----等待150ms后,再尝试获取锁...
。。。省略重复尝试获取锁日志。。。
2018-03-16 14:16:55,571 10568 [work1] INFO c.s.component.redis.lock.RedisLock - work1-----等待150ms后,再尝试获取锁...
work2完成工作,释放锁
2018-03-16 14:16:55,722 10719 [work1] INFO c.s.component.redis.lock.RedisLock - work1-----获得锁
work1获得锁
work1 工作15s...
work1完成工作,释放锁
第二种结果,work1先拿到锁,工作15s,work2等待,这里,因为work1的工作时间超过了默认的等待超时时间10s,所以work2在work1完成工作释放锁之前就因为等待超时而获取锁失败,不能完成工作,结果如下:
2018-03-16 14:22:45,292 5448 [work1] INFO c.s.component.redis.lock.RedisLock - work1-----尝试获取锁...
2018-03-16 14:22:45,292 5448 [work2] INFO c.s.component.redis.lock.RedisLock - work2-----尝试获取锁...
2018-03-16 14:22:45,308 5464 [work2] INFO c.s.component.redis.lock.RedisLock - work2-----等待150ms后,再尝试获取锁...
2018-03-16 14:22:45,309 5465 [work1] INFO c.s.component.redis.lock.RedisLock - work1-----获得锁
work1获得锁
work1 工作15s...
2018-03-16 14:22:45,459 5615 [work2] INFO c.s.component.redis.lock.RedisLock - work2-----等待150ms后,再尝试获取锁...
。。。省略重复尝试获取锁日志。。。
2018-03-16 14:22:55,251 15407 [work2] INFO c.s.component.redis.lock.RedisLock - work2-----等待150ms后,再尝试获取锁...
2018-03-16 14:22:55,401 15557 [work2] INFO c.s.component.redis.lock.RedisLock - work2-----请求锁超时,获得锁失败
。。。这里还要等待大概5s。。。
work1完成工作,释放锁