1.業務場景引入
在進行代碼實現之前,我們先來看一個業務場景:
系統A是一個電商系統,目前是一臺機器部署,系統中有一個用戶下訂單的接口,但是用戶下訂單之前一定要去檢查一下庫存,確保庫存足夠了纔會給用戶下單。
由於系統有一定的併發,所以會預先將商品的庫存保存在redis中,用戶下單的時候會更新redis的庫存。
此時系統架構如下:
但是這樣一來會產生一個問題:
假如某個時刻,redis裏面的某個商品庫存爲1,此時兩個請求同時到來,其中一個請求執行到上圖的第3步,更新數據庫的庫存爲0,但是第4步還沒有執行。
而另外一個請求執行到了第2步,發現庫存還是1,就繼續執行第3步。
這樣的結果,是導致賣出了2個商品,然而其實庫存只有1個。
很明顯不對啊!這就是典型的庫存超賣問題
此時,我們很容易想到解決方案:用鎖把2、3、4步鎖住,讓他們執行完之後,另一個線程才能進來執行第2步。
按照上面的圖,在執行第2步時,使用Java提供的synchronized
或者ReentrantLock
來鎖住,然後在第4步執行完之後才釋放鎖。
這樣一來,2、3、4 這3個步驟就被“鎖”住了,多個線程之間只能串行化執行。
但是好景不長,整個系統的併發飆升,一臺機器扛不住了。現在要增加一臺機器,如下圖:
增加機器之後,系統變成上圖所示,我的天!
假設此時兩個用戶的請求同時到來,但是落在了不同的機器上,那麼這兩個請求是可以同時執行了,還是會出現庫存超賣的問題。
爲什麼呢?因爲上圖中的兩個A系統,運行在兩個不同的JVM裏面,他們加的鎖只對屬於自己JVM裏面的線程有效,對於其他JVM的線程是無效的。
因此,這裏的問題是:Java提供的原生鎖機制在多機部署場景下失效了
這是因爲兩臺機器加的鎖不是同一個鎖(兩個鎖在不同的JVM裏面)。
那麼,我們只要保證兩臺機器加的鎖是同一個鎖,問題不就解決了嗎?
此時,就該分佈式鎖隆重登場了,分佈式鎖的思路是:
在整個系統提供一個全局、唯一的獲取鎖的“東西”,然後每個系統在需要加鎖時,都去問這個“東西”拿到一把鎖,這樣不同的系統拿到的就可以認爲是同一把鎖。
至於這個“東西”,可以是Redis、Zookeeper,也可以是數據庫。
通過上面的分析,我們知道了庫存超賣場景在分佈式部署系統的情況下使用Java原生的鎖機制無法保證線程安全,所以我們需要用到分佈式鎖的方案。
那麼,如何實現分佈式鎖呢?
2.基礎環境準備
2.1.準備測試環境
2.1.1.準備庫存數據庫
-- ----------------------------
-- Table structure for t_goods
-- ----------------------------
DROP TABLE IF EXISTS `t_goods`;
CREATE TABLE `t_goods` (
`goods_id` int(11) NOT NULL AUTO_INCREMENT,
`goods_name` varchar(255) DEFAULT NULL,
`goods_price` decimal(10,2) DEFAULT NULL,
`goods_stock` int(11) DEFAULT NULL,
`goods_img` varchar(255) DEFAULT NULL,
PRIMARY KEY (`goods_id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of t_goods
-- ----------------------------
INSERT INTO `t_goods` VALUES ('1', 'iphone8', '6999.00', '10000', 'img/iphone.jpg');
INSERT INTO `t_goods` VALUES ('2', '小米9', '3000.00', '1100', 'img/rongyao.jpg');
INSERT INTO `t_goods` VALUES ('3', '華爲p30', '4000.00', '100000', 'img/rongyao.jpg');
2.1.2.創建SpringBoot工程
2.1.3.導入依賴
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.bruceliu.springboot.redis.lock</groupId>
<artifactId>springboot-redis-lock</artifactId>
<version>1.0-SNAPSHOT</version>
<!--導入SpringBoot的父工程 把系統中的版本號做了一些定義! -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
</parent>
<dependencies>
<!--導入SpringBoot的Web場景啓動器 Web相關的包導入!-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--導入MyBatis的場景啓動器-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.28</version>
</dependency>
<!--SpringBoot和Junit整合-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<!--導入Lombok依賴-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<!--編譯的時候同時也把包下面的xml同時編譯進去-->
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
</resources>
</build>
</project>
2.1.4.application.properties
# SpringBoot有默認的配置,我們可以覆蓋默認的配置
server.port=8888
# 配置數據的連接信息
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/brucedb?useUnicode=true&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
2.1.5.SpringBoot啓動類
package com.bruceliu;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @BelongsProject: springboot-redis-lock
* @BelongsPackage: com.bruceliu
* @Author: bruceliu
* @QQ:1241488705
* @CreateTime: 2020-05-07 12:48
* @Description: TODO
*/
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class,args);
}
}
2.2.SpringBoot整合Spring Data Redis
2.2.1.導入依賴
<!--Spring Data Redis 的啓動器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
2.2.2.添加Redis相關配置
spring.redis.jedis.pool.max-idle=10
spring.redis.jedis.pool.min-idle=5
spring.redis.jedis.pool-total=20
spring.redis.hostName=122.51.50.249
spring.redis.port=6379
2.2.3.添加Redis的配置類
package com.bruceliu.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import redis.clients.jedis.JedisPoolConfig;
/**
* @BelongsProject: springboot-redis-lock
* @BelongsPackage: com.bruceliu.config
* @Author: bruceliu
* @QQ:1241488705
* @CreateTime: 2020-05-07 12:55
* @Description: TODO
*/
@Configuration
public class RedisConfig {
/**
* 1.創建JedisPoolConfig對象。在該對象中完成一些鏈接池配置
* @ConfigurationProperties:會將前綴相同的內容創建一個實體。
*/
@Bean
@ConfigurationProperties(prefix="spring.redis.jedis.pool")
public JedisPoolConfig jedisPoolConfig(){
JedisPoolConfig config = new JedisPoolConfig();
/*//最大空閒數
config.setMaxIdle(10);
//最小空閒數
config.setMinIdle(5);
//最大鏈接數
config.setMaxTotal(20);*/
System.out.println("默認值:"+config.getMaxIdle());
System.out.println("默認值:"+config.getMinIdle());
System.out.println("默認值:"+config.getMaxTotal());
return config;
}
/**
* 2.創建JedisConnectionFactory:配置redis鏈接信息
*/
@Bean
@ConfigurationProperties(prefix="spring.redis")
public JedisConnectionFactory jedisConnectionFactory(JedisPoolConfig config){
System.out.println("配置完畢:"+config.getMaxIdle());
System.out.println("配置完畢:"+config.getMinIdle());
System.out.println("配置完畢:"+config.getMaxTotal());
JedisConnectionFactory factory = new JedisConnectionFactory();
//關聯鏈接池的配置對象
factory.setPoolConfig(config);
//配置鏈接Redis的信息
//主機地址
/*factory.setHostName("192.168.70.128");
//端口
factory.setPort(6379);*/
return factory;
}
/**
* 3.創建RedisTemplate:用於執行Redis操作的方法
*/
@Bean
public RedisTemplate<String,Object> redisTemplate(JedisConnectionFactory factory){
RedisTemplate<String, Object> template = new RedisTemplate<>();
//關聯
template.setConnectionFactory(factory);
//爲key設置序列化器
template.setKeySerializer(new StringRedisSerializer());
//爲value設置序列化器
template.setValueSerializer(new StringRedisSerializer());
return template;
}
}
2.2.4.測試Redis
package com.bruceliu.test;
import com.bruceliu.App;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.junit4.SpringRunner;
/**
* @BelongsProject: springboot-redis-lock
* @BelongsPackage: com.bruceliu.test
* @Author: bruceliu
* @QQ:1241488705
* @CreateTime: 2020-05-07 12:57
* @Description: TODO
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = App.class)
public class TestRedis {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 添加一個字符串
*/
@Test
public void testSet(){
this.redisTemplate.opsForValue().set("key", "bruceliu...");
}
/**
* 獲取一個字符串
*/
@Test
public void testGet(){
String value = (String)this.redisTemplate.opsForValue().get("key");
System.out.println(value);
}
}
2.3.準備數據庫操作業務方法
2.3.1.pojo層
package com.bruceliu.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @BelongsProject: springboot-redis-lock
* @BelongsPackage: com.bruceliu.pojo
* @Author: bruceliu
* @QQ:1241488705
* @CreateTime: 2020-05-07 13:41
* @Description: TODO
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Goods {
private Long goods_id;
private String goods_name;
private Double goods_price;
private Long goods_stock;
private String goods_img;
}
2.3.2.mapper層
package com.bruceliu.mapper;
import com.bruceliu.pojo.Goods;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import java.util.List;
/**
* @BelongsProject: springboot-redis-lock
* @BelongsPackage: com.bruceliu.mapper
* @Author: bruceliu
* @QQ:1241488705
* @CreateTime: 2020-05-07 13:43
* @Description: TODO
*/
@Mapper
public interface GoodsMapper {
/**
* 01-更新商品庫存
* @param goods
* @return
*/
@Update("update t_goods set goods_stock=#{goods_stock} where goods_id=#{goods_id}")
Integer updateGoodsStock(Goods goods);
/**
* 02-加載商品信息
* @return
*/
@Select("select * from t_goods")
List<Goods> findGoods();
/**
* 03-根據ID查詢
* @param goodsId
* @return
*/
@Select("select * from t_goods where goods_id=#{goods_id}")
Goods findGoodsById(Long goodsId);
}
2.3.3.測試MyBatis
package com.bruceliu.test;
import com.bruceliu.App;
import com.bruceliu.mapper.GoodsMapper;
import com.bruceliu.pojo.Goods;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import javax.annotation.Resource;
import java.util.List;
/**
* @BelongsProject: springboot-redis-lock
* @BelongsPackage: com.bruceliu.test
* @Author: bruceliu
* @QQ:1241488705
* @CreateTime: 2020-05-07 13:55
* @Description: TODO
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = App.class)
public class TestMyBatis {
@Resource
GoodsMapper goodsMapper;
@Test
public void testUpdateStock(){
Goods goods=new Goods();
goods.setGoods_id(1L);
goods.setGoods_stock(2L);
Integer count = goodsMapper.updateGoodsStock(goods);
System.out.println(count>0?"更新成功":"更新失敗");
}
@Test
public void testFindGoods(){
List<Goods> goodsList = goodsMapper.findGoods();
for (Goods goods : goodsList) {
System.out.println(goods);
}
}
}
2.4.SpringBoot監聽Web啓動事件
package com.bruceliu.listener;
import com.bruceliu.pojo.Goods;
import com.bruceliu.service.GoodsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextRefreshedEvent;
import java.util.List;
/**
* @BelongsProject: springboot-redis-lock
* @BelongsPackage: com.bruceliu.listener
* @Author: bruceliu
* @QQ:1241488705
* @CreateTime: 2020-05-07 14:07
* @Description: TODO
*/
@Configuration
public class ApplicationStartListener implements ApplicationListener<ContextRefreshedEvent> {
@Autowired
GoodsService goodsService;
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
System.out.println("Web項目啓動");
List<Goods> goodsList = goodsService.findGoods();
for (Goods goods : goodsList) {
System.out.println(goods);
}
}
}
2.5.加載商品數據到Redis中
package com.bruceliu.listener;
import com.bruceliu.pojo.Goods;
import com.bruceliu.service.GoodsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.List;
/**
* @BelongsProject: springboot-redis-lock
* @BelongsPackage: com.bruceliu.listener
* @Author: bruceliu
* @QQ:1241488705
* @CreateTime: 2020-05-07 14:07
* @Description: TODO
*/
@Configuration
public class ApplicationStartListener implements ApplicationListener<ContextRefreshedEvent> {
@Autowired
GoodsService goodsService;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
System.out.println("Web項目啓動");
List<Goods> goodsList = goodsService.findGoods();
for (Goods goods : goodsList) {
redisTemplate.boundHashOps("goods_info").put(goods.getGoods_id(), goods.getGoods_stock());
System.out.println(goods);
}
}
}
3.Redis實現分佈式鎖
3.1 分佈式鎖的實現類
package com.bruceliu.lock;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.Transaction;
import redis.clients.jedis.exceptions.JedisException;
import java.util.List;
import java.util.UUID;
/**
* @BelongsProject: springboot-redis-lock
* @BelongsPackage: com.bruceliu.lock
* @Author: bruceliu
* @QQ:1241488705
* @CreateTime: 2020-05-07 14:50
* @Description: TODO
*/
public class DistributedLock {
private final JedisPool jedisPool;
public DistributedLock(JedisPool jedisPool) {
this.jedisPool = jedisPool;
}
/**
* 加鎖
* @param lockName 鎖的key
* @param acquireTimeout 獲取超時時間
* @param timeout 鎖的超時時間
* @return 鎖標識
*/
public String lockWithTimeout(String lockName, long acquireTimeout, long timeout) {
Jedis conn = null;
String retIdentifier = null;
try {
// 獲取連接
conn = jedisPool.getResource();
// 隨機生成一個value
String identifier = UUID.randomUUID().toString();
// 鎖名,即key值
String lockKey = "lock:" + lockName;
// 超時時間,上鎖後超過此時間則自動釋放鎖
int lockExpire = (int) (timeout / 1000);
// 獲取鎖的超時時間,超過這個時間則放棄獲取鎖
long end = System.currentTimeMillis() + acquireTimeout;
while (System.currentTimeMillis() < end) {
if (conn.setnx(lockKey, identifier) == 1) {
conn.expire(lockKey, lockExpire);
// 返回value值,用於釋放鎖時間確認
retIdentifier = identifier;
return retIdentifier;
}
// 返回-1代表key沒有設置超時時間,爲key設置一個超時時間
if (conn.ttl(lockKey) == -1) {
conn.expire(lockKey, lockExpire);
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
} catch (JedisException e) {
e.printStackTrace();
} finally {
if (conn != null) {
conn.close();
}
}
return retIdentifier;
}
/**
* 釋放鎖
* @param lockName 鎖的key
* @param identifier 釋放鎖的標識
* @return
*/
public boolean releaseLock(String lockName, String identifier) {
Jedis conn = null;
String lockKey = "lock:" + lockName;
boolean retFlag = false;
try {
conn = jedisPool.getResource();
while (true) {
// 監視lock,準備開始事務
conn.watch(lockKey);
// 通過前面返回的value值判斷是不是該鎖,若是該鎖,則刪除,釋放鎖
if (identifier.equals(conn.get(lockKey))) {
Transaction transaction = conn.multi();
transaction.del(lockKey);
List<Object> results = transaction.exec();
if (results == null) {
continue;
}
retFlag = true;
}
conn.unwatch();
break;
}
} catch (JedisException e) {
e.printStackTrace();
} finally {
if (conn != null) {
conn.close();
}
}
return retFlag;
}
}
3.2 分佈式鎖的業務代碼
service業務邏輯層
package com.bruceliu.service;
import com.bruceliu.pojo.Goods;
/**
* @BelongsProject: springboot-redis-lock
* @BelongsPackage: com.bruceliu.service
* @Author: bruceliu
* @QQ:1241488705
* @CreateTime: 2020-05-07 14:27
* @Description: TODO
*/
public interface SkillService {
public Integer seckill(Long goodsId,Long goodsStock);
}
service業務邏輯層實現層
package com.bruceliu.service;
import com.bruceliu.pojo.Goods;
/**
* @BelongsProject: springboot-redis-lock
* @BelongsPackage: com.bruceliu.service
* @Author: bruceliu
* @QQ:1241488705
* @CreateTime: 2020-05-07 14:27
* @Description: TODO
*/
public interface SkillService {
public Integer seckill(Long goodsId,Long goodsStock);
}
package com.bruceliu.service.impl;
import com.bruceliu.lock.DistributedLock;
import com.bruceliu.mapper.GoodsMapper;
import com.bruceliu.pojo.Goods;
import com.bruceliu.service.SkillService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import javax.annotation.Resource;
/**
* @BelongsProject: springboot-redis-lock
* @BelongsPackage: com.bruceliu.service.impl
* @Author: bruceliu
* @QQ:1241488705
* @CreateTime: 2020-05-07 14:27
* @Description: TODO
*/
@Service
public class SkillServiceImpl implements SkillService {
private static JedisPool pool = null;
private DistributedLock lock = new DistributedLock(pool);
@Resource
GoodsMapper goodsMapper;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
static {
JedisPoolConfig config = new JedisPoolConfig();
// 設置最大連接數
config.setMaxTotal(200);
// 設置最大空閒數
config.setMaxIdle(8);
// 設置最大等待時間
config.setMaxWaitMillis(1000 * 100);
// 在borrow一個jedis實例時,是否需要驗證,若爲true,則所有jedis實例均是可用的
config.setTestOnBorrow(true);
pool = new JedisPool(config, "127.0.0.1", 6379, 3000);
}
@Override
public Integer seckill(Long goodsId, Long goodsStock) {
// 返回鎖的value值,供釋放鎖時候進行判斷
String identifier = lock.lockWithTimeout("resource", 5000, 1000);
//System.out.println(Thread.currentThread().getName() + "--------------->獲得了鎖");
Long goods_stock = (Long) redisTemplate.boundHashOps("goods_info").get(goodsId);
System.out.println(goodsId + "商品在Redis中庫存:" + goods_stock);
if (goods_stock > 0) {
//1.查詢數據庫對象
Goods goods = goodsMapper.findGoodsById(goodsId);
//2.更新數據庫中庫存數量
goods.setGoods_stock(goods.getGoods_stock() - goodsStock);
Integer count = goodsMapper.updateGoodsStock(goods);
System.out.println("更新數據庫庫存:" + count);
//3.同步Redis中商品庫存
redisTemplate.boundHashOps("goods_info").put(goods.getGoods_id(), goods.getGoods_stock());
lock.releaseLock("resource", identifier);
System.out.println(Thread.currentThread().getName() + "--------------->釋放了鎖");
} else {
return -1;
}
return 1;
}
}
controller層
package com.bruceliu.controller;
import com.bruceliu.service.SkillService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @BelongsProject: springboot-redis-lock
* @BelongsPackage: com.bruceliu.controller
* @Author: bruceliu
* @QQ:1241488705
* @CreateTime: 2020-05-07 15:14
* @Description: TODO
*/
@RestController
@Scope("prototype")
public class SkillController {
@Autowired
SkillService skillService;
@RequestMapping("/skill")
public String skill(){
Integer count = skillService.seckill(1L, 1L);
if(count>0){
return "下單成功"+count;
}else{
return "下單失敗"+count;
}
}
}
4.分佈式鎖測試
把SpringBoot工程啓動兩臺服務器,端口分別爲8888、9999.然後使用jmeter進行併發測試,查看控制檯輸出:
5.關於Redis的部署
除了要考慮客戶端要怎麼實現分佈式鎖之外,還需要考慮redis的部署問題。
redis有3種部署方式:
5.1.單機模式
standaloan 是redis單機模式,及所有服務連接一臺redis服務,該模式不適用生產。如果發生宕機,內存爆炸,就可能導致所有連接改redis的服務發生緩存失效引起雪崩。
使用redis做分佈式鎖的缺點在於:如果採用單機部署模式,會存在單點問題,只要redis故障了。加鎖就不行了。
5.2.master-slave + sentinel選舉模式
redis-Sentinel(哨兵模式)是Redis官方推薦的高可用性(HA)解決方案,當用Redis做Master-slave的高可用方案時,假如master宕機了,Redis本身(包括它的很多客戶端)都沒有實現自動進行主備切換,而Redis-sentinel本身也是一個獨立運行的進程,它能監控多個master-slave集羣,發現master宕機後能進行切換.
採用master-slave模式,加鎖的時候只對一個節點加鎖,即便通過sentinel做了高可用,但是如果master節點故障了,發生主從切換,此時就會有可能出現鎖丟失的問題。
5.3.redis cluster模式
redis集羣模式,同樣可以實現redis高可用部署,Redis Sentinel集羣模式中,隨着業務量和數據量增,到性能達到redis單節點瓶頸,垂直擴容受機器限制,水平擴容涉及對應用的影響以及數據遷移中數據丟失風險。針對這些痛點Redis3.0推出cluster分佈式集羣方案,當遇到單節點內存,併發,流量瓶頸是,採用cluster方案實現負載均衡,cluster方案主要解決分片問題,即把整個數據按照規則分成多個子集存儲在多個不同幾點上,每個節點負責自己整個數據的一部分。
Redis Cluster採用哈希分區規則中的虛擬槽分區。虛擬槽分區巧妙地使用了哈希空間,使用分散度良好的哈希函數把所有的數據映射到一個固定範圍內的整數集合,整數定義爲槽(slot)。Redis Cluster槽的範圍是0 ~ 16383。槽是集羣內數據管理和遷移的基本單位。採用大範圍的槽的主要目的是爲了方便數據的拆分和集羣的擴展,每個節點負責一定數量的槽。Redis Cluster採用虛擬槽分區,所有的鍵根據哈希函數映射到0 ~ 16383,計算公式:slot = CRC16(key)&16383。每一個實節點負責維護一部分槽以及槽所映射的鍵值數據。下圖展現一個五個節點構成的集羣,每個節點平均大約負責3276個槽,以及通過計算公式映射到對應節點的對應槽的過程。
基於以上的考慮,其實redis的作者也考慮到這個問題,他提出了一個RedLock的算法,這個算法的意思大概是這樣的:
假設redis的部署模式是redis cluster,總共有5個master節點,通過以下步驟獲取一把鎖:
獲取當前時間戳,單位是毫秒
輪流嘗試在每個master節點上創建鎖,過期時間設置較短,一般就幾十毫秒
嘗試在大多數節點上建立一個鎖,比如5個節點就要求是3個節點(n / 2 +1)
客戶端計算建立好鎖的時間,如果建立鎖的時間小於超時時間,就算建立成功了
要是鎖建立失敗了,那麼就依次刪除這個鎖
只要別人建立了一把分佈式鎖,你就得不斷輪詢去嘗試獲取鎖
但是這樣的這種算法還是頗具爭議的,可能還會存在不少的問題,無法保證加鎖的過程一定正確。
在實際開發中,沒有必要使用原生的redis clinet來實現,可以藉助Redis的封裝框實現:Redisson!