初始分布式锁:
使用分布式锁的场景:
1、防止缓存击穿:缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。没有使用分布式锁的话,所有线程都是并发访问数据库的,如果使用分布式锁,那么所有的请求都会变成串行请求,大大减少了数据库的压力。
2、防止秒杀超卖:这种情况往往都会有大量线程并发访问,如果很多线程都进入if判断语句中,那么就会出现超卖的问题。
多个服务间 + 保证同一时刻内 + 同一用户只能有一个请求(防止关键业务出现数据冲突和并发错误)
创建两个Springboot模块,用来模拟两台机器卖一个商品,两个模块内容除了端口号之外,一模一样。
依赖
org.springframework.boot spring-boot-starter-weborg.springframework.boot spring-boot-starter-actuatororg.springframework.boot spring-boot-starter-data-redisorg.apache.commons commons-pool2redis.clients jedis3.1.0 org.springframework.boot spring-boot-starter-aoporg.redisson redisson3.13.4 org.springframework.boot spring-boot-devtoolsruntime true org.projectlombok lomboktrue org.springframework.boot spring-boot-starter-testtest org.junit.vintage junit-vintage-engine
配置文件
package com.example.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.io.Serializable;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate redisTemplate(LettuceConnectionFactory connectionFactory){
RedisTemplate redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return redisTemplate;
}
}
业务逻辑
package com.example.demo.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GoodController{
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String serverPort;
@GetMapping("/buy_goods")
public String buy_Goods(){
// get key ====看看库存的数量够不够
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if(goodsNumber > 0){
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber));
System.out.println("成功买到商品,库存还剩下: "+ realNumber + " 件" + "t服务提供端口" + serverPort);
return "成功买到商品,库存还剩下:" + realNumber + " 件" + "t服务提供端口" + serverPort;
}else{
System.out.println("商品已经售完/活动结束/调用超时,欢迎下次光临" + "t服务提供端口" + serverPort);
}
return "商品已经售完/活动结束/调用超时,欢迎下次光临" + "t服务提供端口" + serverPort;
}
}
功能简单测试:
redis:set goods:001 100
目前功能正常
问题:没有加锁,并发下数字不对,会出现超卖的情况。
解决办法:
1、加synchronized
2、加ReentrantLock
3、还是都可以
得看具体的业务逻辑
1、如果加synchronized,那么必须等着进入synchronized的线程释放锁了其他线程才能进来,这样很容易导致线程积压和拥堵。
2、加ReentrantLock,有一个tryLock()方法,可以避免因为之前的线程不释放锁而导致线程积压。后进来的线程有两种选择,一是觉得等待的时间太长了,放弃等待;二是给这个线程一个规定的时间,在规定的时间内拿不到锁在放弃等待。这样可以规避使用synchronized死等的情况
目前情况下,加了synchronized或者ReentrantLock,单机版情况下是没有问题的。
如果分布式部署以后,单机锁还是会出现超卖现象,需要使用分布式锁
客户端发起请求,然后有nginx负责转发,后端服务器1加的锁对后端服务器2的锁不起作用
采用Windows上的nginx做反向代理和负载均衡
测试效果:目前来说 好像没有问题
使用apache-jmeter做压力测试
开始测试
发现出现两个模块卖出同一件商品,相当于出现超卖,有问题。单机锁无法解决分布式部署后的超卖问题。因此需要分布式锁。
使用redis的分布式锁 setNX
添加一个常量key
生成一个随机值作为value,然后使用setNX保存到redis数据库中,后面执行业务逻辑。
解锁 删除key
在使用redis分布式锁的时候,要注意释放锁,如果因为出现异常不能释放锁,会出现死锁的情况。必须要在代码层面加个finally来释放锁
即使是将解锁的代码放在finally代码块中,依旧会遇到一些极端的问题,比如部署这个模块的服务器宕机了,那么代码就运行不到finally代码块中,就释放不了锁,因此需要对这个REDIS_LOCK做一个过期时间,可以完成自动解锁。
这样如果代码能正常执行,就能走finally这里删除这个锁,不然就等着时间过期。
问题又来了,这样只能保证加锁,不能保证原子性。设置key+过期时间分开了,必须要合并成一行具备原子性。setIfAbsent本身具有原子性。
这样虽然能保存设置key+过期时间操作的原子性,但是还有一个问题,如果一个线程在本次操作中加了锁,然后在调用其他方法,但是这次调用其他方法花费了很长时间,平时5秒就能完成的,现在需要12秒,这样就超过了设置的过期时间10秒,会释放锁,其他线程就能抢到锁,但是这个线程还没有有操作完,等这个线程走到finally去删除锁的时候,就会把别人的线程的上的锁删掉了。
为了避免这个问题,需要在删除锁的时候,判断一下删除的是不是自己加的锁,因为之前加锁设置key-value的时候,value是唯一的,因此删除key的时候,先通过key查询value,然后判断一下这个value是否是自己生成的,在进行删除操作。
即使删除key时进行了判断,但是判断和删除不具有原子性(个人理解的一种情况:刚好线程A从redis里获取key的时间是10秒,然后就自动删除了,这时候线程B来创建了一个key,但是之前线程A刚好判断满足删除key的条件,依旧会把线程B创建的key删除)。问题就在于finally语句块中的判断和del删除操作不是原子性的。可以使用lua脚本,也可以使用redis本身的事务。
采用redis事务保证原子性
采用lua脚本:Redis调用Lua脚本通过eval命令保证代码执行的原子性
创建一个utils
package com.example.demo.utils;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
public class RedisUtils {
private static JedisPool jedisPool;
static {
JedisPoolConfig jpc = new JedisPoolConfig();
jpc.setMaxTotal(20);
jpc.setMaxIdle(10);
jedisPool = new JedisPool(jpc,"192.168.25.135",6379);
}
public static Jedis getJedis() throws Exception{
if(jedisPool == null)
throw new NullPointerException("JedisPool is not OK.");
return jedisPool.getResource();
}
}
还有问题需要保证:确保redisLock过期时间大于业务执行时间的问题——redis分布式锁如何续期?
集群 + CAP对比ZooKeeper 对比ZooKeeper,重点,CAP
- Redis - AP -redis异步复制造成的锁丢失,比如:主节点没来的及把刚刚set进来这条数据给从节点,就挂了。
- ZooKeeper - CP
CAP
- C:Consistency(强一致性)
- A:Availability(可用性)
- P:Partition tolerance(分区容错性)
综上所述
Redis集群环境下,我们自己写的也不OK,直接上RedLock之Redisson落地实现。
在RedisConfig中添加配置
@Bean
public Redisson redisson() {
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.25.135:6379").setDatabase(0);
return (Redisson)Redisson.create(config);
}
修改业务逻辑代码
package com.example.demo.controller;
import com.example.demo.utils.RedisUtils;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
@RestController
public class GoodController{
private final ReentrantLock lock=new ReentrantLock();
@Autowired
private Redisson redisson;
public static final String REDIS_LOCK="LOCK";
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String serverPort;
@GetMapping("/buy_goods")
public String buy_Goods() throws Exception {
// synchronized (this){}
String value= UUID.randomUUID().toString()+Thread.currentThread().getName();
RLock redissonLock = redisson.getLock(REDIS_LOCK);
redissonLock.lock();//加锁
try {
// //相当于加锁
// Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value,10l, TimeUnit.SECONDS);//setNX+设置过期时间
// //stringRedisTemplate.expire(REDIS_LOCK,10l, TimeUnit.SECONDS);//10秒后自动解锁
//
// if (!flag){
// //进入到这里,就说明抢锁失败
// return "抢锁失败!";
// }
String result = stringRedisTemplate.opsForValue().get("goods:001");// get key ====看看库存的数量够不够
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if(goodsNumber > 0){
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber));
System.out.println("成功买到商品,库存还剩下: "+ realNumber + " 件" + "t服务提供端口" + serverPort);
return "成功买到商品,库存还剩下:" + realNumber + " 件" + "t服务提供端口" + serverPort;
}else{
System.out.println("商品已经售完/活动结束/调用超时,欢迎下次光临" + "t服务提供端口" + serverPort);
}
return "商品已经售完/活动结束/调用超时,欢迎下次光临" + "t服务提供端口" + serverPort;
}finally {
redissonLock.unlock();//解锁
// //redis调用lua脚本
// Jedis jedis = RedisUtils.getJedis();
// //Lua脚本,来自于官网
// String script = "if redis.call('get', KEYS[1]) == ARGV[1] "
// + "then "
// + " return redis.call('del', KEYS[1]) "
// + "else "
// + " return 0 "
// + "end";
//
// try {
// //执行脚本
// Object o = jedis.eval(script, Collections.singletonList(REDIS_LOCK),
// Collections.singletonList(value));
//
// if("1".equals(o.toString())) {
// System.out.println("---del redis lock ok.");
// }else {
// System.out.println("---del redis lock error.");
// }
//
// }finally {
// //释放资源
// if (jedis!=null){
// jedis.close();
// }
// }
// //采用redis事务
// while (true){
// //watch 相当于一个乐观锁,用来监控REDIS_LOCK
// stringRedisTemplate.watch(REDIS_LOCK);
// if (stringRedisTemplate.opsForValue().get(REDIS_LOCK).equalsIgnoreCase(value)){
// //设置支持事务
// stringRedisTemplate.setEnableTransactionSupport(true);
// //开启事务
// stringRedisTemplate.multi();
// //命令入队
// stringRedisTemplate.delete(REDIS_LOCK);
// List
在进行并发测试,发现没有出现超卖问题。
目前看来是没有问题的,但是可能在超高并发的情况下,会出现一个问题
IllegalMonitorStateException: attempt to unlock lock,not loked by current thread by node id:da6385f-81a5-4e6c-b8c0
这个错误就是有可能当前线程和解锁的线程不是同一个。
IllegalMonitorStateException:非法监视器状态异常。
问题出在finally的redissonLock.unlock();这里。
解决办法:在finally语句块中添加判断
redis分布式锁总结回顾:
synchronized单机版oK,nginx分布式微服务下单机锁不行;
取消单机锁,上Redis分布式锁setnx;
只加了锁,没有释放锁,出异常的话,可能无法释放锁,必须要在代码层面finally释放锁;
宕机了,部署了微服务代码层面根本没有走到finally这块,没办法保证解锁,这个key没有被删除,需要有lockKey的过期时间设定;
为redis的分布式锁key,增加过期时间,此外,还必须要setnx+过期时间必须同一行
必须规定只能自己删除自己的锁,你不能把别人的锁删除了,防止张冠李戴,1删2,2删3;
Redis集群环境下,我们自己写的也不oK,直接上RedLock之Redisson落地实现。
=========================================================================
Redis分布式锁源码解析:
https://www.cnblogs.com/kiko2014551511/p/11527108.html
使用redisson加分布式锁就比较简洁,三行代码,加锁解锁就两行代码。
构造方法:
@Autowired private Redisson redisson; public static final String REDIS_LOCK="LOCK"; //获取锁 RLock redissonLock = redisson.getLock(REDIS_LOCK);
调用的构造方法:主要是创建一个Rlock对象,同时初始化了一个看门狗的时间,默认为30秒
//这个name就是传进来的key
public RLock getLock(String name) {
return new RedissonLock(this.connectionManager.getCommandExecutor(), name);
}
//构造一个RedissonLock对象,包括执行器commandExecutor和我们传入的lockKey
public RedissonLock(CommandAsyncExecutor commandExecutor, String name) {
super(commandExecutor, name);
this.commandExecutor = commandExecutor;
this.id = commandExecutor.getConnectionManager().getId();
//这里初始化了一个看门狗时间,watch dog,就是用来续命的watch dog,默认是30s,
//这值是可以改的,在构建RedssionClient的时候可以动态传入
//watch dog的续命时间
this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();
this.entryName = this.id + ":" + name;
this.pubSub = commandExecutor.getConnectionManager().getSubscribeService().getLockPubSub();
}
加锁
redissonLock.lock();//加锁
加锁底层的核心就是执行一段lua脚本。lua脚本是往redis中存储一个hash,redis的hash就相当于java中的
lua脚本的大概意思就是:首先判断一下需要加锁的key是否存在,如果不存在,就进行加锁,并设置一个过期时间30秒,然后返回。此时相当于是
RFuture tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand command) { this.internalLockLeaseTime = unit.toMillis(leaseTime); return this.evalWriteAsync(this.getName(), LongCodec.INSTANCE, command, //这段就是需要执行的lua脚本 "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', 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()), this.internalLockLeaseTime, this.getLockName(threadId)); }
这里需要考虑一个问题,锁有过期时间,那么线程在持有锁的过程中,锁过期了怎么办?redisson提供了一个watch dog机制(看门狗,默认10秒 执行一次),当执行完加锁的lua脚本加锁成功后,会执行一个异步操作,异步回调scheduleExpirationRenewal方法,该方法也是执行的lua脚本,先判断一下自己是否是锁的持有人,如果是的话,那么就刷新一下hash key的存活时间。
锁等待:
在while循环中,不断重新尝试获取锁,直到成功。当然轮询的时候会根据拿到的ttl,等待指定时间之后,再尝试去获取锁。
当线程尝试用lua脚本加锁的时候,发现已经被别的线程先加上锁了,那么就会返回这个hash key的存活时间(ttl),也就是这个锁的存活时间。如果这个锁的存活时间为null,说明当前线程就是当前加锁线程,可以直接跳出循环;如果存活时间大于等于0,说明锁还没有过期,当前线程会等待指定时间之后再去尝试获取锁;如果存活时间小于0,那么说明锁已过期,当前线程会立刻尝试去获取锁。
释放锁:
释放锁的核心代码也是执行的lua脚本。
protected RFutureunlockInnerAsync(long threadId) { return this.evalWriteAsync(this.getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, "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()), LockPubSub.UNLOCK_MESSAGE, this.internalLockLeaseTime, this.getLockName(threadId)); }
如果不是当前持有锁的人,那么立刻返回;如果是,那么对hash的内部key进行减一操作,然后查看结果值,如果结果值大于0,那么刷新hash key的过期时间;如果结果值小于等于0,那么就删除hash key,并发布锁释放的消息。



