以商品超卖为例讲解Redis分布式锁

}
}
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

  • 请求地址:http://localhost:8080/redisLock/receiveAward

  • 返回结果:插入了五条记录

1.3.3.2 加锁

/**

  • 家庭成员领取奖励(加锁)

  • @return
    */
    @PostMapping(“/receiveAwardLock”)
    public HttpResult receiveAwardLock() {
    return redisLockService.receiveAwardLock();
    }

  • 请求方式:POST

  • 请求地址:http://localhost:8080/redisLock/receiveAwardLock

  • 返回结果:只插入了一条记录

通过对比,说明分布式锁起作用了。

1.4 小结

我上家使用的就是这种加锁方式,看上去很OK,实际上在Redis集群的时候会出现问题,比如:

A客户端在Redismaster节点上拿到了锁,但是这个加锁的key还没有同步到slave节点,master故障,发生故障转移,一个slave节点升级为master节点,B客户端也可以获取同个key的锁,但客户端A也已经拿到锁了,这就导致多个客户端都拿到锁。

正因为如此,Redis作者antirez基于分布式环境下提出了一种更高级的分布式锁的实现方式:Redlock

二、Redlock实现

2.1 原理

antirez提出的Redlock算法大概是这样的:

Redis的分布式环境中,我们假设有NRedis master。这些节点完全互相独立,不存在主从复制或者其他集群协调机制。我们确保将在N个实例上使用与在Redis单实例下相同方法获取和释放锁。现在我们假设有5Redis master节点,同时我们需要在5台服务器上面运行这些Redis实例,这样保证他们不会同时都宕掉。

2.1.1 加锁

为了取到锁,客户端应该执行以下操作(RedLock算法加锁步骤):

  1. 获取当前Unix时间,以毫秒为单位;
  2. 依次尝试从5个实例,使用相同的key和具有唯一性的value(例如UUID)获取锁。当向Redis请求获取锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒,则超时时间应该在5-50毫秒之间。这样可以避免服务器端Redis已经挂掉的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,客户端应该尽快尝试去另外一个Redis实例请求获取锁;
  3. 客户端使用当前时间减去开始获取锁时间(步骤1记录的时间)就得到获取锁使用的时间。当且仅当从大多数(N/2+1,这里是3个节点)的Redis节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功;
  4. 如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间(步骤3计算的结果)。
  5. 如果因为某些原因,获取锁失败(没有在至少N/2+1Redis实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的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(){
// 以指定goodId = 2:卫龙为例
Long goodId = 2L;
GoodDO goodDO = goodMapper.selectByPrimaryKey(goodId);
int goodStock = goodDO.getGoodCounts();
String key = goodDO.getGoodName();
log.info(“{}剩余总库存,{}件”, key,goodStock);
// 将商品的实时库存放在redis 中,便于读取
stringRedisTemplate.opsForValue().set(key, Integer.toString(goodStock));
// redisson 锁 的key
String lockKey = goodDO.getId() +“_” + key;
RLock lock = redissonClient.getLock(lockKey);
// 设置60秒自动释放锁 (默认是30秒自动过期)
lock.lock(60, TimeUnit.SECONDS);
// 此步开始,串行销售
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get(key));
// 如果缓存中库存量大于1,可以继续销售
if (stock >= 1) {
goodDO.setGoodCounts(stock - 1);
int num = goodMapper.saleOneGood(goodId);
if (num == 1) {
// 减库存成功,将缓存同步
stringRedisTemplate.opsForValue().set(key,Integer.toString((stock-1)));
}
log.info(“{},当前库存,{}件”, key,stock);
}
lock.unlock();
return HttpResult.success();
}

2.3 测试

采用的是JMeter工具进行测试,初始化的时候两个商品的库存设置都是:5;所以这里加锁和不加锁的情况都设置成:十次并发请求。

2.3.1 不加锁

/**

  • 售卖商品(不加锁)

  • @return
    */
    @PostMapping(“/saleGoods”)
    public HttpResult saleGoods() {
    return redisLockService.saleGoods();
    }

  • 请求方式:POST

  • 请求地址:http://localhost:8080/redisLock/saleGoods

  • 返回结果:id =1的商品库存减为-5

2.3.2 加锁

/**

  • 售卖商品(加锁)

  • @return
    */
    @PostMapping(“/saleGoodsLock”)
    public HttpResult saleGoodsLock() {
    return redisLockService.saleGoodsLock();
    }

  • 请求方式:POST

  • 请求地址:http://localhost:8080/redisLock/saleGoodsLock

  • 返回结果:id =1的商品库存减为0

2.3.3 小结

通过2.3.12.3.2的结果对比很明显:前者出现了超卖情况,库存数卖到了-5,这是决不允许的;而加了锁的情况后,库存只会减少到0,便不再销售。

三、总结

再次说明:以上代码不全,如需尝试,请前往Van 的 Github 查看完整示例代码

第一种基于Redis的分布式锁并不适合用于生产环境。Redisson 可用于生产环境。当然,分布式的选择还有Zookeeper的选项,小编后续会整理出来供大家参考。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

面试准备+复习分享:

为了应付面试也刷了很多的面试题与资料,现在就分享给有需要的读者朋友,资料我只截取出来一部分哦

秋招|美团java一面二面HR面面经,分享攒攒人品

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
va获取)**

img

面试准备+复习分享:

为了应付面试也刷了很多的面试题与资料,现在就分享给有需要的读者朋友,资料我只截取出来一部分哦

[外链图片转存中…(img-mRinGQja-1713515116058)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 19
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值