driver-class-name: com.mysql.jdbc.Driver
redis:
host: 47.98.178.84
port: 6379
password: password
timeout: 2000
mybatis
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: cn.van.mybatis.demo.entity
1.3.2 核心实现
- Jedis 单机配置类 -
RedisConfig.java
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
@Value(“
s
p
r
i
n
g
.
r
e
d
i
s
.
h
o
s
t
"
)
p
r
i
v
a
t
e
S
t
r
i
n
g
h
o
s
t
;
@
V
a
l
u
e
(
"
{spring.redis.host}") private String host; @Value("
spring.redis.host")privateStringhost;@Value("{spring.redis.port}”)
private int port;
@Value(“
s
p
r
i
n
g
.
r
e
d
i
s
.
p
a
s
s
w
o
r
d
"
)
p
r
i
v
a
t
e
S
t
r
i
n
g
p
a
s
s
w
o
r
d
;
@
V
a
l
u
e
(
"
{spring.redis.password}") private String password; @Value("
spring.redis.password")privateStringpassword;@Value("{spring.redis.timeout}”)
private int timeout;
@Bean
public JedisPool redisPoolFactory() {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
if (StringUtils.isEmpty(password)) {
return new JedisPool(jedisPoolConfig, host, port, timeout);
}
return new JedisPool(jedisPoolConfig, host, port, timeout, password);
}
@Bean(name = “redisTemplate”)
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(redisConnectionFactory);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, Visibility.ANY);
objectMapper.enableDefaultTyping(DefaultTyping.NON_FINAL);
Jackson2JsonRedisSerializer jsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
jsonRedisSerializer.setObjectMapper(objectMapper);
redisTemplate.setDefaultSerializer(jsonRedisSerializer);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
- 分布式锁工具类 -
RedisDistributedLock.java
@Component
public class RedisDistributedLock {
/**
- 成功获取锁标示
/
private static final String LOCK_SUCCESS = “OK”;
/* - 成功解锁标示
*/
private static final Long RELEASE_SUCCESS = 1L;
@Autowired
private JedisPool jedisPool;
/**
- redis 数据存储过期时间
*/
final int expireTime = 500;
/**
- 尝试获取分布式锁
- @param lockKey 锁
- @param lockValue 请求标识
- @return 是否获取成功
*/
public boolean tryLock(String lockKey, String lockValue) {
Jedis jedis = null;
try{
jedis = jedisPool.getResource();
String result = jedis.set(lockKey, lockValue, “NX”, “PX”, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
} finally {
if(jedis != null){
jedis.close();
}
}
return false;
}
/**
-
释放分布式锁
-
@param lockKey 锁
-
@param lockValue 请求标识
-
@return 是否释放成功
*/
public boolean unLock(String lockKey, String lockValue) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
String script = “if redis.call(‘get’, KEYS[1]) == ARGV[1] then return redis.call(‘del’, KEYS[1]) else return 0 end”;
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(lockValue));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
} finally {
if(jedis != null){
jedis.close();
}
}
return false;
}
} -
不加锁时:模拟
familyId = 1
的家庭同时领取奖励
@Override
public HttpResult receiveAward() {
Long familyId = 1L;
Map<String, Object> params = new HashMap<String, Object>(16);
params.put(“familyId”, familyId);
params.put(“rewardType”, 1);
int count = familyRewardRecordMapper.selectCountByFamilyIdAndRewardType(params);
if (count == 0) {
FamilyRewardRecordDO recordDO = new FamilyRewardRecordDO(familyId,1,0,LocalDateTime.now());
int num = familyRewardRecordMapper.insert(recordDO);
if (num == 1) {
return HttpResult.success();
}
return HttpResult.failure(-1, “记录插入失败”);
}
return HttpResult.success(“该记录已存在”);
}
- 加锁的实现:模拟
familyId = 2
的家庭同时领取奖励
@Override
public HttpResult receiveAwardLock() {
Long familyId = 2L;
Map<String, Object> params = new HashMap<String, Object>(16);
params.put(“familyId”, familyId);
params.put(“rewardType”, 1);
int count = familyRewardRecordMapper.selectCountByFamilyIdAndRewardType(params);
if (count == 0) {
// 没有记录则创建领取记录
FamilyRewardRecordDO recordDO = new FamilyRewardRecordDO(familyId,1,0,LocalDateTime.now());
// 分布式锁的key(familyId + rewardType)
String lockKey = recordDO.getFamilyId() + “_” + recordDO.getRewardType();
// 分布式锁的value(唯一值)
String lockValue = createUUID();
boolean lockStatus = redisLock.tryLock(lockKey, lockValue);
// 锁被占用
if (!lockStatus) {
log.info(“锁已经占用了”);
return HttpResult.failure(-1,“失败”);
}
// 不管多个请求,加锁之后,只会有一个请求能拿到锁,进行插入操作
log.info(“拿到了锁,当前时刻:{}”,System.currentTimeMillis());
int num = familyRewardRecordMapper.insert(recordDO);
if (num != 1) {
log.info(“数据插入失败!”);
return HttpResult.failure(-1, “数据插入失败!”);
}
log.info(“数据插入成功!准备解锁…”);
boolean unLockState = redisLock.unLock(lockKey,lockValue);
if (!unLockState) {
log.info(“解锁失败!”);
return HttpResult.failure(-1, “解锁失败!”);
}
log.info(“解锁成功!”);
return HttpResult.success();
}
log.info(“该记录已存在”);
return HttpResult.success(“该记录已存在”);
}
private String createUUID() {
UUID uuid = UUID.randomUUID();
String str = uuid.toString().replace(“-”, “_”);
return str;
}
1.3.3 测试
我采用的是
JMeter
工具进行测试,加锁和不加锁的情况都设置成:五次并发请求。
1.3.3.1 不加锁
/**
-
家庭成员领取奖励(不加锁)
-
@return
*/
@PostMapping(“/receiveAward”)
public HttpResult receiveAward() {
return redisLockService.receiveAward();
} -
请求方式:
POST
-
返回结果:插入了五条记录
1.3.3.2 加锁
/**
-
家庭成员领取奖励(加锁)
-
@return
*/
@PostMapping(“/receiveAwardLock”)
public HttpResult receiveAwardLock() {
return redisLockService.receiveAwardLock();
} -
请求方式:
POST
-
返回结果:只插入了一条记录
通过对比,说明分布式锁起作用了。
1.4 小结
我上家使用的就是这种加锁方式,看上去很OK,实际上在Redis
集群的时候会出现问题,比如:
A
客户端在Redis
的master
节点上拿到了锁,但是这个加锁的key
还没有同步到slave
节点,master
故障,发生故障转移,一个slave
节点升级为master
节点,B
客户端也可以获取同个key
的锁,但客户端A
也已经拿到锁了,这就导致多个客户端都拿到锁。
正因为如此,Redis
作者antirez
基于分布式环境下提出了一种更高级的分布式锁的实现方式:Redlock
。
二、Redlock
实现
2.1 原理
antirez
提出的Redlock
算法大概是这样的:
在Redis
的分布式环境中,我们假设有N
个Redis master
。这些节点完全互相独立,不存在主从复制或者其他集群协调机制。我们确保将在N
个实例上使用与在Redis
单实例下相同方法获取和释放锁。现在我们假设有5
个Redis master
节点,同时我们需要在5
台服务器上面运行这些Redis
实例,这样保证他们不会同时都宕掉。
2.1.1 加锁
为了取到锁,客户端应该执行以下操作(RedLock
算法加锁步骤):
- 获取当前
Unix
时间,以毫秒为单位; - 依次尝试从
5
个实例,使用相同的key
和具有唯一性的value
(例如UUID
)获取锁。当向Redis
请求获取锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10
秒,则超时时间应该在5-50
毫秒之间。这样可以避免服务器端Redis
已经挂掉的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,客户端应该尽快尝试去另外一个Redis
实例请求获取锁; - 客户端使用当前时间减去开始获取锁时间(步骤
1
记录的时间)就得到获取锁使用的时间。当且仅当从大多数(N/2+1
,这里是3
个节点)的Redis
节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功; - 如果取到了锁,
key
的真正有效时间等于有效时间减去获取锁所使用的时间(步骤3
计算的结果)。 - 如果因为某些原因,获取锁失败(没有在至少
N/2+1
个Redis
实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis
实例上进行解锁(即便某些Redis
实例根本就没有加锁成功,防止某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁)。
2.1.2 解锁
向所有的Redis
实例发送释放锁命令即可,不用关心之前有没有从Redis
实例成功获取到锁.
2.2 案例(商品超卖为例)
这部分以最常见的案例:抢购时的商品超卖(库存数减少为负数)为例
2.2.1 准备
good
表
CREATE TABLE good
(
id
bigint(20) NOT NULL AUTO_INCREMENT COMMENT ‘主键id’,
good_name
varchar(255) NOT NULL COMMENT ‘商品名称’,
good_counts
int(255) NOT NULL COMMENT ‘商品库存’,
create_time
timestamp NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT ‘创建时间’,
PRIMARY KEY (id
)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT=‘商品表’;
– 插入两条测试数据
INSERT INTO good
VALUES (1, ‘哇哈哈’, 5, ‘2019-09-20 17:39:04’);
INSERT INTO good
VALUES (2, ‘卫龙’, 5, ‘2019-09-20 17:39:06’);
- 配置文件跟上面一样
2.2.2 核心实现
Redisson
配置类RedissonConfig.java
我这里配置的是单机,更多配置详见https://github.com/redisson/redisson/wiki/配置
@Configuration
public class RedissonConfig {
@Value(“
s
p
r
i
n
g
.
r
e
d
i
s
.
h
o
s
t
"
)
p
r
i
v
a
t
e
S
t
r
i
n
g
h
o
s
t
;
@
V
a
l
u
e
(
"
{spring.redis.host}") private String host; @Value("
spring.redis.host")privateStringhost;@Value("{spring.redis.port}”)
private String port;
@Value(“${spring.redis.password}”)
private String password;
/**
- RedissonClient,单机模式
- @return
- @throws IOException
*/
@Bean
public RedissonClient redissonSentinel() {
//支持单机,主从,哨兵,集群等模式,此为单机模式
Config config = new Config();
config.useSingleServer()
.setAddress(“redis://” + host + “:” + port)
.setPassword(password);
return Redisson.create(config);
}
}
- 不加锁时
@Override
public HttpResult saleGoods(){
// 以指定goodId = 1:哇哈哈为例
Long goodId = 1L;
GoodDO goodDO = goodMapper.selectByPrimaryKey(goodId);
int goodStock = goodDO.getGoodCounts();
if (goodStock >= 1) {
goodMapper.saleOneGood(goodId);
}
return HttpResult.success();
}
- 加锁
@Override
public HttpResult saleGoodsLock(){
Java高频面试专题合集解析:
当然在这还有更多整理总结的Java进阶学习笔记和面试题未展示,其中囊括了Dubbo、Redis、Netty、zookeeper、Spring cloud、分布式、高并发等架构资料和完整的Java架构学习进阶导图!
更多Java架构进阶资料展示
面试专题合集解析:**
[外链图片转存中…(img-ziVFhba3-1721170616884)]
当然在这还有更多整理总结的Java进阶学习笔记和面试题未展示,其中囊括了Dubbo、Redis、Netty、zookeeper、Spring cloud、分布式、高并发等架构资料和完整的Java架构学习进阶导图!
[外链图片转存中…(img-elagnGAg-1721170616885)]
更多Java架构进阶资料展示
[外链图片转存中…(img-RFkqvzXd-1721170616885)]
[外链图片转存中…(img-BgLnT2gU-1721170616886)]
[外链图片转存中…(img-XunqN1D5-1721170616886)]