第一节 Redis的安装
第二节 Redis的五种数据结构(String、Hash、List、Set、ZSet)
第三节 Redis的持久化方式
第四节 Redis主从架构
第五节 Redis哨兵高可用架构
第六节 Redis集群高可用架构
第七节 Redis集群选举原理与脑裂问题
第八节 Redis高可用集群之水平扩展操作实战
本节介绍Redis分布式锁实战,包含Redis高并发问题现象与Redis分布式问题解决方案两大方面。
在redis客户端操作,设置stock_number为50
192.168.75.200:7001> set stock_number 50
OK
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.apache.logging.log4jgroupId>
<artifactId>log4j-coreartifactId>
<version>2.3version>
dependency>
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-log4j12artifactId>
<version>1.7.25version>
dependency>
使用redis集群模式配置
server:
port: 8080
spring:
redis:
database: 0
timeout: 3000
password: admin
cluster:
nodes: 192.168.75.200:7001,192.168.75.200:7002,192.168.75.201:7001,192.168.75.201:7002,192.168.75.202:7001,192.168.75.202:7002
lettuce:
pool:
max-idle: 50
min-idle: 10
max-active: 100
max-wait: 1000
package com.xj;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 TestController {
Logger log = LoggerFactory.getLogger(TestController.class);
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RequestMapping("/decr_number")
public String decrStock(){
int stock_number = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock_number")); // jedis.get("stock_number")
if (stock_number > 0) {
int remain_stock = stock_number - 1;
stringRedisTemplate.opsForValue().set("stock_number", remain_stock + ""); // jedis.set(key,value)
log.info("减库存成功,剩余库存:" + remain_stock);
return "减库存成功,剩余库存:" + remain_stock;
} else {
log.error("减库存失败,库存不足");
return "减库存失败,库存不足";
}
}
}
设置500个并发
HTTP请求
日志打印结果:
从上面的结果打印可知,重复出现剩余相同库存现象,发生了并发问题,引起了超卖现象
针对上面出现的并发问题,我们首先可以想到用synchronized或者Lock锁来解决,例如controller代码可以改成如下形式:
@RequestMapping("/decr_number")
public String decrStock(){
synchronized (this){
int stock_number = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock_number")); // jedis.get("stock_number")
if (stock_number > 0) {
int remain_stock = stock_number - 1;
stringRedisTemplate.opsForValue().set("stock_number", remain_stock + ""); // jedis.set(key,value)
log.info("减库存成功,剩余库存:" + remain_stock);
return "减库存成功,剩余库存:" + remain_stock;
} else {
log.error("减库存失败,库存不足");
return "减库存失败,库存不足";
}
}
}
使用Jmeter模拟压测验证发生问题已解决
使用并发锁只能解决单实例下的并发问题,对于在集群多实例情况下还是存在分布式并发问题,此种问题只能依靠分布式锁来进行解决。
@RequestMapping("/decr_number_setnx")
public String decrStock_1(){
String lock_key = "product_1000";
String client_id = UUID.randomUUID().toString();
try{
//抢锁:key不存在设置并设置超时时间
//设置超时时间是为了防止死锁
//setnx和expire指令一起执行保证原子性
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lock_key, client_id, 10, TimeUnit.SECONDS);
//没抢到锁,返回友好提示
if(!result){
log.error("系统繁忙,请稍后再试");
return "系统繁忙,请稍后再试";
}
//抢到锁,执行业务逻辑
int stock_number = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock_number"));
if (stock_number > 0) {
int remain_stock = stock_number - 1;
stringRedisTemplate.opsForValue().set("stock_number", remain_stock + "");
log.info("减库存成功,剩余库存:" + remain_stock);
return "减库存成功,剩余库存:" + remain_stock;
} else {
log.error("减库存失败,库存不足");
return "减库存失败,库存不足";
}
} catch (Exception e){
log.error("Exception:",e);
} finally {
//解决锁失效的问题:
//1、自己的锁自己释放
//2、当一个线程A执行花费的时间大于设置的过期时间,导致锁失效,此时其它线程B过来加锁成功,然后线程A执行完毕删除了线程B设置的锁,之后经常重复前面的现象
if (client_id.equals(stringRedisTemplate.opsForValue().get(lock_key))) {
stringRedisTemplate.delete(lock_key);
}
}
return "error";
}
对于这种方案,一般高并发方案下可以选择使用,但是无法解决某些线程执行时间超过设置的超时时间,导致其它线程乘虚而入获得锁,即没有实现锁续命的问题,对于锁续命可以使用Redisson。
从上图可以看出redission提供了锁续命的功能,比SETNX方案更完善。
<dependency>
<groupId>org.redissongroupId>
<artifactId>redissonartifactId>
<version>3.6.5version>
dependency>
@Bean
public Redisson redisson() {
Config config = new Config();
//集群模式
if (redisProperties.getCluster() != null) {
//集群模式配置
List<String> nodes = redisProperties.getCluster().getNodes();
List<String> clusterNodes = new ArrayList<>();
for (int i = 0; i < nodes.size(); i++) {
clusterNodes.add("redis://" + nodes.get(i));
}
ClusterServersConfig clusterServersConfig = config.useClusterServers()
.addNodeAddress(clusterNodes.toArray(new String[clusterNodes.size()]));
if (!StringUtils.isEmpty(redisProperties.getPassword())) {
clusterServersConfig.setPassword(redisProperties.getPassword());
}
} else {
//单节点配置
String address = "redis://" + redisProperties.getHost() + ":" + redisProperties.getPort();
SingleServerConfig serverConfig = config.useSingleServer();
serverConfig.setAddress(address);
if (!StringUtils.isEmpty(redisProperties.getPassword())) {
serverConfig.setPassword(redisProperties.getPassword());
}
serverConfig.setDatabase(redisProperties.getDatabase());
}
//看门狗的锁续期时间,默认30000ms,这里配置成15000ms
//config.setLockWatchdogTimeout(15000);
return (Redisson) Redisson.create(config);
}
@Autowired
private Redisson redisson;
@RequestMapping("/decr_number_redission")
public String decrStock_2(){
String lock_key = "product_1000";
RLock lock = redisson.getLock(lock_key);
try{
//加锁
lock.lock();
//抢到锁,执行业务逻辑
int stock_number = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock_number"));
if (stock_number > 0) {
int remain_stock = stock_number - 1;
stringRedisTemplate.opsForValue().set("stock_number", remain_stock + "");
log.info("减库存成功,剩余库存:" + remain_stock);
return "减库存成功,剩余库存:" + remain_stock;
} else {
log.error("减库存失败,库存不足");
return "减库存失败,库存不足";
}
} catch (Exception e){
log.error("Exception:",e);
} finally {
//释放锁
lock.unlock();
}
return "error";
}
在config对象里面初始化为30s
@Bean
public Redisson redisson() {
Config config = new Config();
}
public Config() {
this.transportMode = TransportMode.NIO;
#默认续期时间30s
this.lockWatchdogTimeout = 30000L;
this.keepPubSubOrder = true;
this.addressResolverGroupFactory = new DnsAddressResolverGroupFactory();
}
RLock为一个接口,继承了Lock
public interface RLock extends Lock, RExpirable, RLockAsync {
void lockInterruptibly(long var1, TimeUnit var3) throws InterruptedException;
boolean tryLock(long var1, long var3, TimeUnit var5) throws InterruptedException;
void lock(long var1, TimeUnit var3);
void forceUnlock();
boolean isLocked();
boolean isHeldByCurrentThread();
int getHoldCount();
}
lock()方法
public interface Lock {
void lock();
.......
}
lock()的实现在RedissonLock类中:
public void lock() {
try {
//调用带中断的lock方法
this.lockInterruptibly();
} catch (InterruptedException var2) {
//捕获中断异常,设置中断标志,业务线程自行停止线程
Thread.currentThread().interrupt();
}
}
会继续调用lockInterruptibly()方法,获取锁,不成功则订阅释放锁的消息,获得消息前阻塞,得到释放通知后再去循环获取锁。
public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
//获取当前线程ID
long threadId = Thread.currentThread().getId();
//加锁操作失败,返回锁的剩余超时时间;返回null,说明加锁成功
Long ttl = this.tryAcquire(leaseTime, unit, threadId);
//其它线程加锁失败,继续执行
if (ttl != null) {
RFuture<RedissonLockEntry> future = this.subscribe(threadId);
this.commandExecutor.syncSubscription(future);
try {
//自旋加锁
while(true) {
ttl = this.tryAcquire(leaseTime, unit, threadId);
//说明加锁成功,自旋结束
if (ttl == null) {
return;
}
//等待一段时间,继续执行
if (ttl >= 0L) {
this.getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
this.getEntry(threadId).getLatch().acquire();
}
}
} finally {
this.unsubscribe(future, threadId);
}
}
}
private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) {
return (Long)this.get(this.tryAcquireAsync(leaseTime, unit, threadId));
}
tryAcquireAsync方法:
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
#如果设置了超时时间,直接调用 tryLockInnerAsync
if (leaseTime != -1L) {
return this.tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
} else {
##如果leaseTime==-1,则默认超时时间为30s
RFuture<Long> ttlRemainingFuture = this.tryLockInnerAsync(this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
ttlRemainingFuture.addListener(new FutureListener<Long>() {
public void operationComplete(Future<Long> future) throws Exception {
if (future.isSuccess()) {
Long ttlRemaining = (Long)future.getNow();
if (ttlRemaining == null) {
#实现锁续命
RedissonLock.this.scheduleExpirationRenewal(threadId);
}
}
}
});
return ttlRemainingFuture;
}
}
tryLockInnerAsync方法:
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
this.internalLockLeaseTime = unit.toMillis(leaseTime);
/**
*执行lua脚本,加锁操作
*1、exists判断锁key是否存在
*2、不存在,hset添加锁key,hash结构
*3、pexpire设置锁key的过期时间
*4、同线程hexists查看哈希表的指定字段是否存在(如果哈希表含有给定字段,返回1)
*5、存在,则执行hincrby指令,实现可重入锁
*6、再次执行pexpire刷新过去时间
*7、其它线程两个判断都没有执行,最后执行pttl返回剩余过期时间
**/
return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, command, "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hset', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; return redis.call('pttl', KEYS[1]);", Collections.singletonList(this.getName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)});
}
scheduleExpirationRenewal方法,每到过期时间1/3就去重新刷一次,如果key不存在则停止刷新:
private void scheduleExpirationRenewal(final long threadId) {
if (!expirationRenewalMap.containsKey(this.getEntryName())) {
//TimerTask实现定时任务
Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
public void run(Timeout timeout) throws Exception {
//执行lua脚本实现锁续命
RFuture<Boolean> future = RedissonLock.this.commandExecutor.evalWriteAsync(RedissonLock.this.getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('pexpire', KEYS[1], ARGV[1]); return 1; end; return 0;", Collections.singletonList(RedissonLock.this.getName()), new Object[]{RedissonLock.this.internalLockLeaseTime, RedissonLock.this.getLockName(threadId)});
future.addListener(new FutureListener<Boolean>() {
public void operationComplete(Future<Boolean> future) throws Exception {
RedissonLock.expirationRenewalMap.remove(RedissonLock.this.getEntryName());
if (!future.isSuccess()) {
RedissonLock.log.error("Can't update lock " + RedissonLock.this.getName() + " expiration", future.cause());
} else {
if ((Boolean)future.getNow()) {
RedissonLock.this.scheduleExpirationRenewal(threadId);
}
}
}
});
}
}, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);
if (expirationRenewalMap.putIfAbsent(this.getEntryName(), task) != null) {
task.cancel();
}
}
}
解锁入口:
public void unlock() {
Boolean opStatus = (Boolean)this.get(this.unlockInnerAsync(Thread.currentThread().getId()));
if (opStatus == null) {
throw new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: " + this.id + " thread-id: " + Thread.currentThread().getId());
} else {
if (opStatus) {
this.cancelExpirationRenewal();
}
}
}
unlockInnerAsync方法,同样也是lua脚本保证解锁操作的原子性:
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, "if (redis.call('exists', KEYS[1]) == 0) then redis.call('publish', KEYS[2], ARGV[1]); return 1; end;if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil;end; local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); if (counter > 0) then redis.call('pexpire', KEYS[1], ARGV[2]); return 0; else redis.call('del', KEYS[1]); redis.call('publish', KEYS[2], ARGV[1]); return 1; end; return nil;", Arrays.asList(this.getName(), this.getChannelName()), new Object[]{LockPubSub.unlockMessage, this.internalLockLeaseTime, this.getLockName(threadId)});
}
Redis是典型的AP模型,也就是保证高可用,和数据的最终一致性。在实现分布式锁时,可能会出现线程A设置完锁后,master挂掉,slave提升为master,因为异步复制的特性(主从之间的数据同步),线程A设置的锁丢失了,这时候线程B设置锁也能够成功,导致线程A和B同时拥有锁。
针对这个问题,redission提供了redlock(超过半数节点加锁成功,才算加锁成功)来解决这个问题。