总结全文内容:
1,不加锁,不用setnx, 产生超减库存!
2,加JVM锁Synchronized不用setnx,Synchronized同步块只能锁住自己JVM中的并发请求,那另一台JVM中的请求是无法锁住的,产生超减库存!
3,加分布式锁 setnx(基础版),对应java方法: setIfAbsent( lockKey, "xxx" );也会产生超减库存!
4,在setnx基础之上优化方向(Redisson):
1、使用finally块来删除lockKey;
2、给setnx设置的lockKey添加过期时间。
3,给每一个请求一个不同的lockkey,从而使得每一个请求只能自己加锁、解锁。
4,锁续命(也称看门狗)
出现问题:Redisson主从失效,无法实现强一致性
5,RedLock(解决主从失效问题)超过半数Redis节点加锁成功才算加锁成功
强烈建议不适用RedLock!互联网公司很少使用,并且可能存在一些bug。
并且,RedLock性能存在严重问题。
一、分布式锁场景
1、互联网秒杀
2、抢优惠券
3、接口幂等性校验
二、扣减库存实战
1、不加锁版本
配置文件:
server:
port:8080
spring:
redis:
database:0
timeout:3000
host:192.168.131.171
port:6380
SpringBoot启动:
package com.jihu.redis;
import org.redisson.Redisson;
import org.redisson.config.Config;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class RedisApplication {
public static void main(String[] args) {
SpringApplication.run(RedisApplication.class, args);
}
// 注意要注入redisson 否则会出错
@Bean
public Redisson redisson() {
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.131.171:6380").setDatabase(0);
return (Redisson) Redisson.create(config);
}
}
直接上代码,我们模拟一个扣减库存的简单场景。
【初始版本】
@RestController
public class StockController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 模拟减库存
* @return ""
*/
@RequestMapping("/deduct_stock")
public String deductStock() {
// 从redis获取库存数量
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
// 如果库存数量大于0
if (stock > 0) {
int realStock = stock -1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock + "");
} else { // 如果库存数量小于0
System.out.println("扣减失败,库存不足!");
}
return "end";
}
}
这样扣减库存的代码,如果我们直接部署到生产环境,会不会出问题?
我们来分析一下,假如现在是高并发的场景,用户需要秒杀抢一些商品。
问题分析
当大量请求同时去执行从Redis获取数据这一行代码的时候:
/ 从redis获取库存数量,此时值设置为50
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
此时由于我们没有任何的同步措施,这些请求都会直接从Redis中获取数据,查询到的结果自然就都是50了。
然后判断49大于0,然后都减1,库存剩余都是49.
然后都调用下面的代码将库存设置到Redis中,并且设置的值都是49.
stringRedisTemplate.opsForValue().set("stock", realStock + "");
但是这样显然不对呀!如果我是5个请求同时进来,都执行了减库存,库存应该是45呀,现在竟然还是49了!这样就会产生超卖的问题!。
2、改进:加JVM锁
我们从上面的代码中分析得知,之所以会发生超卖,是因为我们读取Redis库存、扣减库存和将扣减后库存设置到Redis中的这些操作不是原子的,所以导致了超卖问题的产生。
【改进方案一:加JVM锁】
我们可以给以上操作库存的的操作加锁,从而实现原子性操作,这样应该可以避免超卖问题吧!于是乎,我们使用Synchronized来加锁:
package com.jihu.redis.lock;
import org.redisson.Redisson;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
@RestController
public class StockController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 模拟减库存
* @return ""
*/
@RequestMapping("/deduct_stock")
public String deductStock() {
// 将操作库存的操作放到同步块中
synchronized (this) {
// 从redis获取库存数量
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
// 如果库存数量大于0
if (stock > 0) {
int realStock = stock -1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock + "");
} else { // 如果库存数量小于0
System.out.println("扣减失败,库存不足!");
}
}
return "end";
}
}
分析,这样当多个请求执行进来的时候,线程会原子性的执行同步块中的扣减库存操作,可以防止超卖问题的产生。
问题分析
这样的加锁处理,在单机环境下,确实没什么问题,可以防止超卖情况的发生。
但是,现在的互联网公司基本都是集群架构,后端会存在多个服务实例。如果我们还是这样来使用JVM级别的锁,还能防止超卖情况的发生吗?
显然是不能的。因为JVM锁只能锁住同一个JVM进程中的线程,分布式集群架构下,请求往往是跨多个JVM进程的。
如下图,如果有两个tomcat同时去Redis查库存,Synchronized同步块只能锁住自己JVM中的并发请求,那另一台JVM中的请求是无法锁住的。这样依然会产生,多个请求同时去Redis查库存的情况。查询后发现都是50,都减去一个库存,然后都把49写入到Redis中,这样就产生了超卖的问题。
那在这样分布式的场景下,我们又该如何的解决超卖问题的?
此时我们需要一把分布式锁!
何为分布式锁,就是可以锁住不同JVM进程的锁,可以使用Redis、zookeeper等实现。原理就是,多个JVM都可以或都需要访问这些第三方中间件,这样Redis或zooKeeper就相当于一个管理者,可以对不同的JVM请求进行管理。这样就达到了管理多个JVM中请求的功能了。
超卖测试
1、我们先启动端口为8080的服务;
2、修改端口为8090,然后修改IDEA,在Allow parallel run打钩,这样基于可以启动多个服务实例了,只是运行在不同端口。我们启动8080和8090的服务;
3、配置nginx,实现分发,将请求分发到8080和8090这两个服务实例上:
如下是nginx配置:
# 配置Redis分发(我使用的是windows本机的nginx)
upstream redislock {
server 127.0.0.1:8080 weight=1;
server 127.0.0.1:8090 weight=1;
}
server {
listen 80;
server_name localhost;
location / {
root html;
index index.html index.htm;
proxy_pass http://redislock;
}
}
4、配置好之后,确保执行http://localhost/deduct_stock之后,8080和8090都可以接收到转发的请求:
5、随后我们使用jmeter进行压测准备:
我们配置200个请求,为了模拟高并发,在0s内发完;并且持续4轮;即总共会压800个请求。
压测地址:http://localhost/deduct_stock
6、开始压测
等待压测成功后,我们来分别查看8080和8090两个服务的日志。
我们来思考:如果两个日志打印出的 剩余库存的值有相同的,那么就以为着可能会发生超卖!
很明显的看到,两个日志中的剩余库存出现了大量相同的数字,这意味着已经发生了超卖的情况!!!
3、改进:加分布式锁
【改进方案二:加分布式锁】
三、Redis分布式锁实现
1、Setnx实现分布式锁
【setnx基础版本】存在问题! setnx对应的java方法: setIfAbsent( lockKey, "xxxxx" );
package com.jihu.redis.lock;
import org.redisson.Redisson;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class StockController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 模拟减库存
* @return ""
*/
@RequestMapping("/deduct_stock")
public String deductStock() {
String lockKey = "lockKey";
// setIfAbsent 相当于jedis.setnx(key, value)操作
/**
* setnx(lockKey, value):
*
* 如果这个lockKey在Redis中是已经存在的,则这条命令不做任何操作,返回false
* 如果这个lockey不存在于Redis中,则设置成功,并返回true
*
* 此时如果多个请求来执行这个命令,会在Redis中排队,此时只会有一个请求成功设置了lockKey并返回true.
* 生育的请求查询发现这个lockKey已经存在了,会直接返回false,不进行任何操作
*/
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "xiaoyan");
// ========= setnx 竞争锁失败 ==========
if (!result) {
return "{code: 10001, message:请重新再试!}";
}
// ========== setnx 竞争锁成功 =======
// 从redis获取库存数量
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
// 如果库存数量大于0
if (stock > 0) {
int realStock = stock -1;
// 相当于jedis.set(key, value)
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock + "");
} else { // 如果库存数量小于0
System.out.println("扣减失败,库存不足!");
}
// 成功扣减库存后的请求,需要释放这个lockKey
stringRedisTemplate.delete(lockKey);
return "end";
}
}
我们这样使用setnx命令来实现Redis分布式锁,现在只会有一个请求使用setnx设置lockKey成功,然后执行扣减库存操作,然后删除lockKey,剩余的请求竞争失败的会直接返回false。
那现在这样还存在其他问题吗?大家思考一下?如果执行扣减库存的时候抛出异常了呢?这样lockKey的锁岂不是永远都不会被释放了?
如果服务宕机了?该怎么删除lockKey呢 ?
所以说对于上面的问题,我们必须优化一下:
1、使用finally块来删除lockKey;
2、给setnx设置的lockKey添加过期时间。
【setnx升级版本】依然存在问题!
import org.redisson.Redisson;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class StockController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 模拟减库存
* @return ""
*/
@RequestMapping("/deduct_stock")
public String deductStock() {
String lockKey = "lockKey";
try {
// setIfAbsent 相当于jedis.setnx(key, value)操作
/**
* setnx(lockKey, value):
*
* 如果这个lockKey在Redis中是已经存在的,则这条命令不做任何操作,返回false
* 如果这个lockey不存在于Redis中,则设置成功,并返回true
*
* 此时如果多个请求来执行这个命令,会在Redis中排队,此时只会有一个请求成功设置了lockKey并返回true.
* 生育的请求查询发现这个lockKey已经存在了,会直接返回false,不进行任何操作
*/
// Redis内部会原子性的保证设置value和设置过期时间同时成功。
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "xiaoyan", 10, TimeUnit.SECONDS);
// 添加过期时间,防止服务宕机后无法删除
// 这是错误的设置过期的方式!!!如果服务还没执行到这行就宕机了,依然会无法释放锁!Redis过期时间一定要在set数据的时候就设置!!!
// stringRedisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);
// ========= setnx 竞争锁失败 ==========
if (!result) {
return "{code: 10001, message:请重新再试!}";
}
// ========== setnx 竞争锁成功 =======
// 从redis获取库存数量
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
// 如果库存数量大于0
if (stock > 0) {
int realStock = stock -1;
// 相当于jedis.set(key, value)
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock + "");
} else { // 如果库存数量小于0
System.out.println("扣减失败,库存不足!");
}
} finally { // 防止异常导致锁无法释放!!!
// 成功扣减库存后的请求,需要释放这个lockKey
stringRedisTemplate.delete(lockKey);
}
return "end";
}
}
注意,设置过期时间的时候,一定要和set值一起设置,让Redis原子性的去执行这个两个命令!
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "xiaoyan", 10, TimeUnit.SECONDS);
注意这个自动过期的时间设置,我们现在设置的是10s,即如果超过10s没有释放,Redis会自动释放。
我们这样设置完,好像各种情况都考虑到了!
那么,大家仔细再思考,是否还存在其他问题呢?
在一般的软件公司下,这样确实可以应对并发减库存的问题。
那么问题来了,在大型互联网公司中,如果此时的并发量非常大,大家想有没有可能存在这样的问题。我的一个请求成功竞争到了lockKey,但是在执行扣减库存的业务的时候由于某些原因,导致执行的时间超过了10s.
这时候,Redis会自动释放这个lockKey的锁,但是我的这个请求并没有执行完扣减库存的业务呀。这时候其他请求也进入到扣减库存的业务代码中来。此时第一个请求还没来得及将扣减后的库存写回到Redis中,新的请求却体检查询了Redis数据库,这样一来,直接导致库存数量不准确,一定会产生超卖!!!
并且,如果第二个请求执行的过程中,第一个请求恰好执行15s结束,执行finally块中的删除lockKey,这样就会把第二个请求加的lockKey给删除掉。这样就会出现一个问题,某个请求的锁好像加成功了,一会又失败了。这样已经存在了很多问题…
所以,我们还需要进一步进行优化!
我们来分析此时的问题,因为第一个请求扣减库存的时间太长,导致加的锁过期,从而新的请求可以加锁成功,而新的请求执行扣减库存的过程中,第一个请求恰好执行完,然后再finally块中释放了锁。但事实上,这个锁是第二个请求加的,不再是第一个请求的锁了。因为第一个请求的锁已经因为过期被Redis自己释放了。
总结来说:现在的问题是,不同的请求都可以操作同一个lockKey。所以,我们要思考,是否可以给每一个请求一个不同的lockkey,从而使得每一个请求只能自己加锁、解锁。
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS);
我们只是只是用了一个lockKey,那么我们现在可以针对每一个请求都生成一个lockKey:
【setnx最终版本】可用
@RequestMapping("/deduct_stock")
public String deductStock() {
String lockKey = "lockKey";
String clientId = UUID.randomUUID().toString();
try {
// setIfAbsent 相当于jedis.setnx(key, value)操作
/**
* setnx(lockKey, value):
*
* 如果这个lockKey在Redis中是已经存在的,则这条命令不做任何操作,返回false
* 如果这个lockey不存在于Redis中,则设置成功,并返回true
*
* 此时如果多个请求来执行这个命令,会在Redis中排队,此时只会有一个请求成功设置了lockKey并返回true.
* 生育的请求查询发现这个lockKey已经存在了,会直接返回false,不进行任何操作
*/
// Redis内部会原子性的保证设置value和设置过期时间同时成功。
// clientId: 每一个请求都生成一个clientId, 这样只有自己才能释放锁
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS);
// 添加过期时间,防止服务宕机后无法删除
// 这是错误的设置过期的方式!!!如果服务还没执行到这行就宕机了,依然会无法释放锁!Redis过期时间一定要在set数据的时候就设置!!!
// stringRedisTemplate.expire(lockKey, 30, TimeUnit.SECONDS);
// ========= setnx 竞争锁失败 ==========
if (!result) {
return "{code: 10001, message:请重新再试!}";
}
// ========== setnx 竞争锁成功 =======
// 从redis获取库存数量
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
// 如果库存数量大于0
if (stock > 0) {
int realStock = stock -1;
// 相当于jedis.set(key, value)
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock + "");
} else { // 如果库存数量小于0
System.out.println("扣减失败,库存不足!");
}
} finally { // 防止异常导致锁无法释放!!!
// 成功扣减库存后的请求,需要释放这个lockKey
// 如果lockKey对应的value是本次请求设置的value,才允许释放锁!
// 下面这个代码也有问题!!!判断和删除不是原子的,并发场景可能存在问题:
// t1加锁成功,执行到get时候 ,判断equals为true,
// 刚好这个时候发生了expire,已经自动释放了锁。然后,t2加锁成功。
// t1 由于判断了equals, 执行del。实际上释放了t2加的锁
if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
stringRedisTemplate.delete(lockKey);
}
}
return "end";
}
使用Redis的setnx来实现分布式锁的话,这样的解决方案是可行的,一些公司也有这样使用分布式锁的!
存在的的问题:这样虽然解决了不同请求释放锁的问题,但是依然可能导致bug,如果第一个请求要执行15s, 而超过了10s的过期时间,导致锁被自动释放,其他的请求就会进入扣减库存的代码。如果第一个请求恰好扣减完库存,还没来得及将结果写入到Redis中的时候,新的请求查询了库存。这样可能出现库存拆卖的问题。所以,这个过期时间设置为多少,需要我们自己权衡好。
那么,还有没有更好的解决方案呢?
当然有!我们继续往下分析!!!
现在存在的问题的原因是我们不太能确定下起来,这个过期时间到底设置为多少比较合适!
设置的太长,如果服务宕机没有在finally块中释放锁的话,这样就需要等待很久的时间才能释放锁。
设置的太短,可能会因为第一个请求时间异常导致锁被提前释放,而扣减库存的业务还没执行完,从而导致超卖问题!
归根结底,是这超时时间的设置问题!
在当前认知中,似乎已经没有更好的解决方案了。我们需要打破认知,引入新的概念了: 锁续命。
锁续命(也称看门狗)
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS);
假如我们现在设置锁的过期时间是30s,我们可以再后台启动一个子线程去执行一个定时器,每过1 / 3的过期时间后(30 / 3 = 10 s)就去检查一下当前的锁状态:
如果锁还没有被释放(说明依然在执行扣减库存的业务代码),那么我们就重新给这个请求设置锁的过期时间为30s。从而让这个请求可以持续的持有锁。
如果发现锁已经被释放了,那么就直接结束这个子线程。
这个锁续命的想法非常之巧妙,但是在高并发的场景下让我们自己来实现,势必不是那么容易。
这个一个简单的分布式锁,我们就踩了很多坑,如果没有高并发经验的人来写,那后果更不可想象。
所以,市面上现在已经存在一些技术来帮助我们解决Redis分布式锁的问题了。
我们用的比较多的是Redisson. 下面我们来学习一下Redeisson.
2,Redisson介绍
Redisson也是一个Redis Java Client,即Redis Java客户端,类似于Jedis.
Redisson对于RedisAPI的支持没有Jedis那么全面,但是对于高并发的支持要优于Jedis。比如Redisson支持布隆过滤器、分布式锁、分布式对象等等。
使用Redisson实现分布式锁
依赖:
dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.6.5</version>
</dependency>
Redis可以支持单机版、哨兵、cluster集群等多种模式,我们可以自己去配置,配置好之后将其注入到Srping容器中,在要使用的使用@Autowired注入使用即可。
准备工作做好之后,我们就来开始使用Redisson实现分布式锁吧。
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class StockForRedissonController {
@Autowired
private Redisson redisson;
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 模拟减库存
* @return ""
*/
@RequestMapping("/deduct_stock")
public String deductStock() {
String lockKey = "lockKey";
// 获取到redisson锁对象
RLock redissonLock = redisson.getLock(lockKey);
try {
// ========= 添加redisson锁并实现锁续命功能 =============
/**
* 主要执行一下几个操作
*
* 1、将localKey设置到Redis服务器上,默认过期时间是30s
* 2、每10s触发一次锁续命功能
*/
redissonLock.lock();
// ======== 扣减库存业务员开始 ============
// 从redis获取库存数量
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
// 如果库存数量大于0
if (stock > 0) {
int realStock = stock -1;
// 相当于jedis.set(key, value)
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock + "");
} else { // 如果库存数量小于0
System.out.println("扣减失败,库存不足!");
}
// ======== 扣减库存业务员结束 ============
} finally { // 防止异常导致锁无法释放!!!
// ============= 释放redisson锁 ==========
redissonLock.unlock();
}
return "end";
}
}
就这!!!!!????
就这么几行代码就搞定了分布式锁?
不得不说,这个Redisson有点东西呀!!!
注意:如果使用的是tryLock方法,要注意释放锁前进行判断:
lock()方法自带30s的看门狗功能!如果使用tryLock(long waitTime, long leaseTime, TimeUnit unit), 看门狗功能会失效!!!!!!!!
public PmsProductParam getProductInfo3(Long id) {
PmsProductParam productInfo = null;
//从缓存Redis里找
productInfo = redisOpsUtil.get(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE + id, PmsProductParam.class);
if (null != productInfo) {
return productInfo;
}
RLock redissonLock = redission.getLock(lockPath + id);
try {
if (redissonLock.tryLock()) {
productInfo = portalProductDao.getProductInfo(id);
if (null == productInfo) {
log.warn("没有查询到商品信息,id:" + id);
return null;
}
checkFlash(id, productInfo);
redisOpsUtil.set(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE + id, productInfo, 3600, TimeUnit.SECONDS);
} else {
// 尝试获取锁失败后,再尝试查询一次redis,但是不一定能查询到数据,可能返回null,
// 这里优化空间就是sleep一会后重新执行该方法进行查询
productInfo = redisOpsUtil.get(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE + id, PmsProductParam.class);
}
} finally {
// 判断锁是否依然存在,因为尝试获取锁失败的线程会先执行到这里,所以一定要让真正持有锁的线程去释放锁
if (redissonLock.isLocked()) {
// 判断锁是否是被当前线程持有,这样才能释放锁。其他线程是无法释放锁的,会报错
if (redissonLock.isHeldByCurrentThread())
redissonLock.unlock();
}
}
return productInfo;
要根据业务场景来选择使用lock还是tryLock
如果是减库存操作,建议使用lock。
如果是查询商品信息加缓存的操作,读是占大部分的,所以如何减少加锁逻辑就很有必要,所以可以尝试使用trylock的方式提升性能。
而Redisson底层都帮我们考虑到了之前的问题,并且进行了完美的解决。那我们先来测试一下功能是否好用,然后再来看一下Redisson底层实现。
最后运行压测,发现完美的结局了分布式扣减库存的问题。
lock()方法自带30s的看门狗功能!如果使用tryLock(long waitTime, long leaseTime, TimeUnit unit), 看门狗功能会失效!!!!!!!!
四、Resisson分布式底层原理剖析
我们此时来理一下思路:
假如有多个线程同时来执行扣减库存操作,Redisson底层也是使用setnx来设置一个lockKey。
1、此时有且仅有一个线程能够设置成功,如果某个线程加锁成功,Redisson会启动一个后台守护线程,每隔10s检查当前线程是否持有锁,如果持有锁,则延长持有锁的时间为30s。
2、其他竞争锁失败的线程会一直自旋,尝试加锁。
3、当第一个线程执行完业务代码,释放锁后,其他的线程再去竞争锁。
注意:
Redisson底层设置了过期时间为30s, 这样可以防止服务宕机后锁无法释放的问题,所以我们不用担心。
我们是实现一些业务的时候,往往希望它们是原子操作,尤其是涉及到交互Redis的时候。但是Redis对原子的支持并不是很友好,所以我们经常会使用lua脚本来保证原子性。
而Redisson底层使用了大量的lua脚本来保证原子性。
所以在了解Redisson底层原理之前,我们需要先了解一下lua脚本。
五、Redis集群下的分布式锁主从失效问题(与zookeeper的比较)
我们知道,redis的从节点需要定时去主节点拉取数据进行同步,那如果我们的一个分布式锁总的lockKey在主节点中设置成功后恰好挂了,还没有来得及同步到从节点,此时由于选举某个从节点被选举为了新的主节点,但是此时这个lockKey该如何获取呢?
此时如果旧的请求还在执行获取到分布式锁之后的任务,但是新的请求去这个新的主节点中又获取到了这个分布式锁prod:1001, 这样不是又会出现超卖问题吗?
如果我们的公司能接受这样的bug,一般情况下很少出现,我们依然可以使用Redis实现的分布式锁。
但是如果不能接受的话,此时我们可以考虑使用zookeeper实现分布式锁。因为zookeeper天生是强一致性的,只有每个节点都同步了这个锁,才会将锁加成功的信息返回给客户端。
那我们为什么不一开始就使用zookeeper实现分布式锁呢?
因为Redis的分布式锁性能要优于zookeeper. 并且,并不是所有的产品都使用zookeeper, 反而几乎所有产品都使用Redis。 这样就不用引入zookeeper, 给系统带来复杂性了。
当然,如果我们非要使用Redis实现分布式锁,并且不能产生从节点丢失锁的情况,使用Redis也是可以实现的!
Redis中存在一种RedLock锁!
六、RedLock(解决主从失效问题)
使用RedLock就可有解决主从失效问题。
原理:
RedLock规定,只有超过半数Redis节点加锁成功才算加锁成功,底层实现原理和zookeeper有点像。
底层实现:
同时向所有的Redis服务发送setnx命令,超过半数节点返回加锁成功的消息后,才认为加锁成功,才会执行后面的业务逻辑。
【强烈建议】
强烈建议不适用RedLock!互联网公司很少使用,并且可能存在一些bug。
并且,RedLock性能存在严重问题。
七、高并发分布式锁如何实现(分段库存锁)
使用Redis的分布式锁已经能满足大部分的场景。但是在大型互联网公司中使用,还存在一些不足。那么,我们还有更多的优化吗?还可以将性能提升10倍吗?
如果非常多的人抢购的是不同的商品,我们可以考虑加服务器,将数据打散一些。这样性能确实会有提高
但是如果现在是非常多的人同时抢购茅台?只需要20元呢?
这时候加机器是没有用的,因为Redis通过计算hash槽位之后,只会将这些请求定位到某一个cluster集群下面!现在怎么办呢?
想想concurrentHashMap的实现,我们可以引入一个 分段库存锁!
比如现在要抢购的商品茅台的id在Redis中是prod:1001, 库存是100.
如果我按照传统的存储方法:
set prod:1001 1000
这样存储,意味着这1000个商品prod:1001我只存储到了一个Redis的key中。大量的请求只查询扣减这一个Redis的key,效率自然不是很高。
那我们思考, 是否可以把这1000个prod:1001分成10段,每一段设置100个商品。即:
set prod:1001:1 100
set prod:1001:2 100
set prod:1001:3 100
set prod:1001:4 100
set prod:1001:5 100
set prod:1001:6 100
set prod:1001:7 100
set prod:1001:8 100
set prod:1001:9 100
set prod:1001:10 100
这样,我的同一个商品被分到了10个Redis的key中,我们在存储的Redis的时候,可以使用特殊的hash,来使得这10个key被分片到不同的cluster中去。效率是不是大大提升了?是不是提高了10倍呢?
这是一个非常好的思路,我们值得学习一下。