一、redis集羣的部署
- 安裝redis
確保安裝文件夾有redis-trib.rb文件,通過rudy構建redis集羣- 安裝ruby環境
配置好環境變量,gem install redis 安裝redis依賴詳細環境安裝教程:點擊打開鏈接
集羣搭建
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實現分佈式鎖
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<String> 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<String> getNodes() {
return nodes;
}
public void setNodes(List<String> 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<RedisNode> getRedisNode() {
List<String> nodes = redisProperties.getNodes();
if (CommonUtils.isNotEmpty(nodes)) {
List<RedisNode> 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<String, Object> 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<String, Object> 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完成工作,釋放鎖