su root #密码root
wget http://download.redis.io/releases/redis-6.0.8.tar.gz
tar xzf redis-6.0.8.tar.gz
cd redis-6.0.8
make
1.2 关闭centos7防火墙
1.2.1 查看Linux防火墙状态命令
running是防火墙正在运行
systemctl status firewalld.service
systemctl stop firewalld.service
首次安装,需要改一下redis的配置文件
进入redis安装目录,找到redis.conf,做以下修改
# bind 127.0.0.1 #防止远程连接不上
daemonize yes #后台运行
protected-mode no #关闭保护模式
注意: 如果此刻应用服务还是会报错,要记得应用服务也要重启
1.4 启动redis进入src目录,用以下命令启动
#开启服务端
./redis-server #普通启动
./redis-server ../redis.conf #根据配置文件启动
#开启客户端
./redis-cli
查看redis启动状态
ps -ef|grep redis
查看redis版本号
./redis-server -v
4.0.0
org.example
studySpace
pom
1.0-SNAPSHOT
redisson
org.springframework.boot
spring-boot-dependencies
2.2.2.RELEASE
pom
import
org.springframework.cloud
spring-cloud-dependencies
Hoxton.SR1
pom
import
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-starter-data-redis
org.apache.commons
commons-pool2
redis.clients
jedis
3.1.0
org.springframework.boot
spring-boot-starter-aop
org.redisson
redisson
3.13.4
org.springframework.boot
spring-boot-devtools
runtime
true
org.projectlombok
lombok
true
junit
junit
4.12
org.springframework.boot
spring-boot-maven-plugin
2.2 写yaml
server:
port: 1111
spring:
application:
name: redisson
redis:
database: 0
host: 192.168.208.129
port: 6379
lettuce:
pool:
#连接池最大连接数(使用负值表示没有限制)默认8
max-active: 8
#连接池最大阻塞等待时间(使用负值表示没有限制)默认-1
max-wait: -1
#连接池中的最大空闲连接默认8
max-idle: 8
#连接池中的最小空闲连接默认0
min-idle: 0
2.3 主启动
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class RedissonApplication {
public static void main(String[] args) {
SpringApplication.run(RedissonApplication.class, args);
}
}
2.4 业务类
@Configuration
public class RedisConfig {
@Value("${spring.redis.host}")
private String redisHost;
/**
* 保证不是序列化后的乱码配置
*/
@Bean
public RedisTemplate redisTemplate(LettuceConnectionFactory connectionFactory) {
RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}
@Bean
public Redisson redisson() {
Config config = new Config();
config.useSingleServer().setAddress("redis://" + redisHost + ":6379").setDatabase(0);
return (Redisson) Redisson.create(config);
}
}
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(){
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",realNumber + "");
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口: "+serverPort);
return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口: "+serverPort;
}else {
System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口: "+serverPort);
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口: "+serverPort;
}
}
2.5 开启双实例
启动2号机,测试
2.6 测试
进入redis src目录启动redis客户端./redis-cli
设置值 set goods:001 100, get goods:001
访问地址:http://localhost:1111/buy_goods
此时,单机版的使用redis减库存没有什么问题,但是多线程,高并发情况下,会出现什么问题,见后续
三、代码问题优化 3.1 单机版没加锁 3.1.1 单机版优化上述代码没有加锁,单线程下不会存在什么问题,但是多线程并发情况下,就会出现超卖现象
此时,就要考虑加锁,那到底要加synchronized还是加ReentrantLock锁呢,这就要根据业务和两种锁的特性来分析了
synchronized:不见不散
reentrantLock:过时不候
1、ReentrantLock可以添加多个检控条件(condition),但是synchronized只可以添加一个;
2、ReentrantLock可以控制得到锁的顺序(公平锁),也可以和synchronized一样使用非公平锁;
3、ReentrantLock支持获取锁超时(tryLock()方法)以及获取锁响应中断的操作(lockInterruptibly()方法,synchronized不支持。
4、在高争用条件下,ReentrantLock的可伸缩性优于synchronized;
5.、ReentrantLock必须在finally块中手动释放锁;
@RestController
public class GoodController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String serverPort;
@GetMapping("/buy_goods")
public String buy_Goods() {
synchronized (this) {
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", realNumber + "");
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
} else {
System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort);
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
}
}
}
3.1.2 解释
在单机环境下,可以使用synchronized或lock来实现。
但是,在分布式系统中,因为竞争的线程可能不在同一个节点上(同一个JVM中),所以,需要一个让所有进程都能访问到的锁来实现,比如redis或者zookeeper来构建;
不同进程jvm层面的锁就不管用了,那么可以利用第三方的一个组件,来获取锁,未获取到锁,则阻塞当前想要运行的线程。
3.2 Nginx分布式微服务架构 3.2.1 存在问题分布式部署后,单机锁还是出现超卖现象,需要分布式锁
通过nginx,进行转发,根据设置权重路由到不同的机器
上面问题,通过加锁可以解决单机版情况下,高并发访问数据问题。对于单机版,竞争的线程都是来自同一个JVM,但是,生产环境下,我们的服务大多是多个微服务,分布式部署。每一个服务,都会有多台机器,这样的话,这些线程就可能来自不同的JVM,这样用synchronized或是lock就解决不了上述问题。
3.2.2 配置nginx1. 安装nginx
yum -y install gcc gcc-c++ make libtool zlib zlib-devel openssl openssl-devel pcre pcre-devel
2. 配置nginx.conf
3. 启动nginx
进入nginx sbin目录输入以下命令
./nginx #启动
./nginx -s reload #重启
./nginx -s stop #关闭
查看nginx启动情况
ps -ef|grep nginx
启动1111,2222两个服务
通过nginx所在ip+port来访问:
http://192.168.208.129/buy_goods
1111控制台输出
2222控制台输出
点击访问可以看到效果,一边一个,默认轮询,从结果看,好像也没出什么问题,上面在一个一个减少,即使狂点好像也没出问题,但是,我们手速毕竟有限,接下来我们用jmeter来压测一下
3.2.4 Jmeter进行压测假设,一瞬间有几百个线程砸过来,来测试单机情况下的锁,还有没有效果。或者说,在分布式情况下,我们加了个单机锁,还有没有效果
1.添加线程组
2.添加http请求
3. 恢复数据开始压测
高并发,情况下,此时就出现上面问题,1号机和2号机,都会卖出相同的商品,出现超卖的情况。
3.2.5 解决方法1. 上redis分布式锁
Redis具有极高的性能,且其命令对分布式锁支持友好,借助SET命令即可实现加锁处理.
2. 官网
https://redis.io/commands/set
3. 升级代码
@RestController
public class GoodController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String serverPort;
//相当于门栓
public static final String REDIS_LOCK_KEY = "best_lock";
@GetMapping("/buy_goods")
public String buy_Goods() {
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
//setIfAbsent() 就是如果不存在就新建
Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value);//setnx,多个人竞争一个房间,如果没有上锁,进门加锁,别人就进不来了
if (!lockFlag) {
return "抢锁失败";
}
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", realNumber + "");
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
stringRedisTemplate.delete(REDIS_LOCK_KEY);//释放锁,出门解锁,别人就可以进来了
return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
} else {
System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort);
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
}
}
3.3 异常无法释放锁
3.3.1 无法释放锁风险
上面代码还有一个问题,就是释放锁在业务逻辑中,假如释放锁的上一步出现异常,可能就会无法释放锁,必须要在代码层面finally释放锁
3.3.2 升级代码加锁解锁,lock/unlock必须同时出现并保证调用
@RestController
public class GoodController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String serverPort;
//相当于门栓
public static final String REDIS_LOCK_KEY = "best_lock";
@GetMapping("/buy_goods")
public String buy_Goods() {
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
try {
//setIfAbsent() 就是如果不存在就新建
Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value);//setnx,多个人竞争一个房间,如果没有上锁,进门加锁,别人就进不来了
if (!lockFlag) {
return "抢锁失败";
}
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", realNumber + "");
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
} else {
System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort);
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
} finally {
stringRedisTemplate.delete(REDIS_LOCK_KEY); //释放锁
}
}
}
3.4 宕机无法释放锁
3.4.1 问题
上面我们把释放锁的操作,写入了finally中,即使代码出现异常也会释放锁,但是,假如我们部署了微服务jar包的机器挂了,代码层面 根本没有走到finally这块,没办法保证解锁,这个key没有被删除,这时就需要加入一个过期时间限定key
3.4.2 解决需要对lockKey有过期时间的设定,这样加入服务宕机,过一定时间这个锁也会释放
1. 代码升级
设置key+过期时间分开了,必须要合并成一行具备原子性
进程A,尽量判断redis发现没有锁,然后,它设置一个key加上锁,并设置过期时间30s,然后,后面一顿操作,但是,进程A由于某些原因,比如网络延迟,导致30s后还没有执行完,就被redis过期掉了。此时,进程B进来发现没有锁,也开始加上锁执行,但是,此时的进程A并没有结束,当它执行完后,开始执行删除操作,自己key已经被redis过期掉了,它就会把进程的B的key删除掉,但是,当进程B执行完后,要删除key时,却发现自己的key不见了,此时就尴尬了,就会出现张冠李戴的现象。
3.6.2 解决只能自己删除自己的,不许动别人的
finally块的判断+del删除操作不是原子性的
1. 用redis自身的事务
@RestController
public class GoodController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String serverPort;
//相当于门栓
public static final String REDIS_LOCK_KEY = "best_lock";
@GetMapping("/buy_goods")
public String buy_Goods() {
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
try {
//setIfAbsent() 就是如果不存在就新建,同时加上过期时间保证原子性
Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value, 10L, TimeUnit.SECONDS);//setnx,多个人竞争一个房间,如果没有上锁,进门加锁,别人就进不来了
if (!lockFlag) {
return "抢锁失败";
}
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", realNumber + "");
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
} else {
System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort);
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
} finally {
while (true) {
stringRedisTemplate.watch(REDIS_LOCK_KEY); //加事务,乐观锁
if (stringRedisTemplate.opsForValue().get(REDIS_LOCK_KEY).equalsIgnoreCase(value)) {
stringRedisTemplate.setEnableDefaultSerializer(true);
stringRedisTemplate.multi();
stringRedisTemplate.delete(REDIS_LOCK_KEY);
List list = stringRedisTemplate.exec();
if (list == null) {
continue;
}
}
stringRedisTemplate.unwatch();
break;
}
}
}
}
2. lua脚本解决原子性
public class RedisUtils {
private static JedisPool jedisPool;
static {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(20);
jedisPoolConfig.setMaxIdle(10);
new JedisPool(jedisPoolConfig, "192.168.208.129", 6379);
}
public static Jedis getJedis() throws Exception {
if (jedisPool != null) {
return jedisPool.getResource();
} else {
throw new Exception("JedisPool is not ok");
}
}
}
@RestController
public class GoodController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String serverPort;
//相当于门栓
public static final String REDIS_LOCK_KEY = "best_lock";
@GetMapping("/buy_goods")
public String buy_Goods() throws Exception {
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
try {
//setIfAbsent() 就是如果不存在就新建,同时加上过期时间保证原子性
Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value, 10L, TimeUnit.SECONDS);//setnx,多个人竞争一个房间,如果没有上锁,进门加锁,别人就进不来了
if (!lockFlag) {
return "抢锁失败";
}
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", realNumber + "");
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
} else {
System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort);
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
} finally {
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 result = jedis.eval(script, Collections.singletonList(REDIS_LOCK_KEY), Collections.singletonList(value));
if ("1".equals(result.toString())) {
System.out.println("-------del REDIS_LOCK_KEY success");
} else {
System.out.println("-------del REDIS_LOCK_KEY error");
}
} finally {
if (jedis != null) {
jedis.close();
}
}
}
}
}
3.8 redis集群环境下问题
3.8.1 问题
确保redisLock过期时间大于业务执行时间的问题
Redis分布式锁如何续期?
3.8.2 集群+CAP对比zookeeper1. Redis
redis是AP模式,redis异步复制可能会造成锁丢失,比如,主节点没来得及把刚刚set进来的这条数据给从节点,就挂了。此时,如果集群模式下,就得上Redisson来解决
2. Zookeeper
zookeeper是CP模式,主节点会先把数据同步到从节点,不会存在锁丢失问题
3.8.3 解决redis集群环境下,我们自己写的也不ok,直接上RedLock之Redisson落地实现
@Configuration
public class RedisConfig {
@Value("${spring.redis.host}")
private String redisHost;
/**
* 保证不是序列化后的乱码配置
*/
@Bean
public RedisTemplate redisTemplate(LettuceConnectionFactory connectionFactory) {
RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}
@Bean
public Redisson redisson() {
Config config = new Config();
config.useSingleServer().setAddress("redis://" + redisHost + ":6379").setDatabase(0);
return (Redisson) Redisson.create(config);
}
}
@RestController
public class GoodController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String serverPort;
@Autowired
private Redisson redisson;
//相当于门栓
public static final String REDIS_LOCK_KEY = "best_lock";
@GetMapping("/buy_goods")
public String buy_Goods() throws Exception {
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
RLock redissonLock = redisson.getLock(REDIS_LOCK_KEY);
redissonLock.lock();
try {
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", realNumber + "");
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
} else {
System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort);
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
} finally {
redissonLock.unlock();
}
}
}
再次,压测,已经解决超卖问题
但是,当并发更高的时候,可能会出现以下错误
出现这个错误的原因是,在并发多的时候就可能会遇到这种错误,可能会被重新抢占
不见得当前这个锁的状态还是在锁定,并且本线程持有
再次优化代码
1. synchronized 单机版ok,上分布式
2. nginx分布式微服务,单机锁不行
3. 取消单机锁,上redis分布式锁setnx
4. 只加了锁,没有释放锁,出异常的话,可能无法释放锁,必须要在代码层面finally释放锁
5. 宕机了,部署了微服务代码层面根本没有走到finally这块,没办法保证解锁,这个key没有被删除,需要有lockKey的过期时间设定
6. 为redis的分布式锁key,增加过期时间,此外,还必须要setnx+过期时间必须同一行的原子性操作
7. 必须规定只能自己删除自己的锁,你不能把别人的锁删除了,防止张冠李戴,1删2,2删3
8. lua或者事务,解决原子性问题
9. redis集群环境下,可能会导致锁丢失,我们自己写的也不ok,直接上RedLock之Redisson落地实现
视频教程,源码链接