1、高并发下火车买票问题的分析
代码1:
public void produceStock1(){
//1.在redis中把票数存好
//2.从redis中获取票数
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("trainTickets"));
//判断票数是否大于0
if(stock>0){
//票数减一
int rStock = stock - 1;
//将当前票数存进redis
stringRedisTemplate.opsForValue().set("trainTickets",String.valueOf(rStock));
logger.info("扣减库存成功。。剩余库存"+rStock);
}else{ //说明票数不足
logger.info("库存扣减失败,库存不足");
}
}
分析:这里简单模拟了一下火车买票的场景,当程序执行时,库存会相应减一。但是明显会出现超卖现象,前面的人库存还没来得及减一,后面的人已经开始买了。解决办法如下:
public void produceStock1(){
//1.在redis中把票数存好
//2.从redis中获取票数
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("trainTickets"));
//判断票数是否大于0
if(stock>0){
//票数减一
int rStock = stock - 1;
//将当前票数存进redis
stringRedisTemplate.opsForValue().set("trainTickets",String.valueOf(rStock));
logger.info("扣减库存成功。。剩余库存"+rStock);
}else{ //说明票数不足
logger.info("库存扣减失败,库存不足");
}
}
synchronized在单体应用上或许没问题,但是在分布式上面却锁不住
public synchronized String produceStock3(){
//设置一个字符串
String lock = "lock";
Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lock, "");
if(!aBoolean){
return "排队人数过多,请稍后重试";
}
//1.在redis中把票数存好
//2.从redis中获取票数
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("trainTickets"));
//判断票数是否大于0
if(stock>0){
//票数减一
int rStock = stock - 1;
//将当前票数存进redis
stringRedisTemplate.opsForValue().set("trainTickets",String.valueOf(rStock));
logger.info("扣减库存成功。。剩余库存"+rStock);
}else{ //说明票数不足
logger.info("库存扣减失败,库存不足");
}
return "买票成功";
}
这里又有另外一个问题,当第一个人执行完的时候,后面的人都执行不了,所以前面的人执行完之后一定要把lock删除,后面的人才可以执行
public synchronized String produceStock4(){
//设置一个字符串
String lock = "lock";
try {
Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lock, "");
if(!aBoolean){
return "排队人数过多,请稍后重试";
}
//1.在redis中把票数存好
//2.从redis中获取票数
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("trainTickets"));
//判断票数是否大于0
if(stock>0){
//票数减一
int rStock = stock - 1;
//将当前票数存进redis
stringRedisTemplate.opsForValue().set("trainTickets",String.valueOf(rStock));
logger.info("扣减库存成功。。剩余库存"+rStock);
}else{ //说明票数不足
logger.info("库存扣减失败,库存不足");
}
} finally {
//执行完把key删除
stringRedisTemplate.delete(lock);
}
return "买票成功";
}
如果第一个人在执行到一半的时候服务器宕机了,lock还没来得及删除,就发生了死锁问题,所以可以给lock设置一个过期时间
public synchronized String produceStock5(){
//设置一个字符串
String lock = "lock";
try {
Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lock, "");
//给key设置过期时间
stringRedisTemplate.expire(lock,30, TimeUnit.SECONDS);
if(!aBoolean){
return "排队人数过多,请稍后重试";
}
//1.在redis中把票数存好
//2.从redis中获取票数
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("trainTickets"));
//判断票数是否大于0
if(stock>0){
//票数减一
int rStock = stock - 1;
//将当前票数存进redis
stringRedisTemplate.opsForValue().set("trainTickets",String.valueOf(rStock));
logger.info("扣减库存成功。。剩余库存"+rStock);
}else{ //说明票数不足
logger.info("库存扣减失败,库存不足");
}
} finally {
//执行完把key删除
stringRedisTemplate.delete(lock);
}
return "买票成功";
}
我们可以将.setIfAbent和expire方法写出一条语句使其具有原子性
public synchronized String produceStock6(){
//设置一个字符串
String lock = "lock";
try {
//Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lock, "");
//给key设置过期时间
//stringRedisTemplate.expire(lock,30, TimeUnit.SECONDS);
Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lock,"", 30, TimeUnit.SECONDS);
if(!aBoolean){
return "排队人数过多,请稍后重试";
}
//1.在redis中把票数存好
//2.从redis中获取票数
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("trainTickets"));
//判断票数是否大于0
if(stock>0){
//票数减一
int rStock = stock - 1;
//将当前票数存进redis
stringRedisTemplate.opsForValue().set("trainTickets",String.valueOf(rStock));
logger.info("扣减库存成功。。剩余库存"+rStock);
}else{ //说明票数不足
logger.info("库存扣减失败,库存不足");
}
} finally {
stringRedisTemplate.delete(lock);
}
return "买票成功";
}
当第一个线程还没执行完的时候已经超过过期时间,这是时候lock已经被过期了,等第一个线程执行完删除的却是第二个的锁,所以每次执行的时候给每个线程生成一个唯一值,每次删除的时候再判断一下
public synchronized String produceStock7(){
//设置一个字符串
String lock = "lock";
//生成id
String value = UUID.randomUUID().toString();
try {
//Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lock, "");
//给key设置过期时间
//stringRedisTemplate.expire(lock,30, TimeUnit.SECONDS);
Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lock,value, 30, TimeUnit.SECONDS);
if(!aBoolean){
return "排队人数过多,请稍后重试";
}
//1.在redis中把票数存好
//2.从redis中获取票数
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("trainTickets"));
//判断票数是否大于0
if(stock>0){
//票数减一
int rStock = stock - 1;
//将当前票数存进redis
stringRedisTemplate.opsForValue().set("trainTickets",String.valueOf(rStock));
logger.info("扣减库存成功。。剩余库存"+rStock);
}else{ //说明票数不足
logger.info("库存扣减失败,库存不足");
}
} finally {
//判断一下值是否相等
if(value.equals(stringRedisTemplate.opsForValue().get(lock))){
stringRedisTemplate.delete(lock);
}
}
return "买票成功";
}
当程序还没执行完,已经超过过期时间,这里时间设置有问题,所以我们可以使用守护线程续命
public synchronized String produceStock8(){
//设置一个字符串
String lock = "lock";
//生成id
String value = UUID.randomUUID().toString();
try {
//Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lock, "");
//给key设置过期时间
//stringRedisTemplate.expire(lock,30, TimeUnit.SECONDS);
Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lock,value, 30, TimeUnit.SECONDS);
if(!aBoolean){
return "排队人数过多,请稍后重试";
}
//开一个守护线程,每隔过期时间的三分之一就进行续命
MyThread myThread = new MyThread(lock);
myThread.setDaemon(true);
myThread.start();
//1.在redis中把票数存好
//2.从redis中获取票数
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("trainTickets"));
//判断票数是否大于0
if(stock>0){
//票数减一
int rStock = stock - 1;
//将当前票数存进redis
stringRedisTemplate.opsForValue().set("trainTickets",String.valueOf(rStock));
logger.info("扣减库存成功。。剩余库存"+rStock);
}else{ //说明票数不足
logger.info("库存扣减失败,库存不足");
}
} finally {
//判断一下值是否相等
if(value.equals(stringRedisTemplate.opsForValue().get(lock))){
stringRedisTemplate.delete(lock);
}
}
return "买票成功";
}
class MyThread extends Thread{
String lock;
public MyThread(String lock){
this.lock = lock;
}
@Override
public void run() {
while (true){
try {
Thread.sleep(10000);
} catch (Exception e){
}
//假设线程还活着,那么说明需要续命
stringRedisTemplate.expire(lock,30,TimeUnit.SECONDS);
}
}
}
2、基于Redis的分布式锁问题Redisson的简单使用
这个框架就解决了分布式锁的问题
1.导包
<!--导入redssion依赖-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.11.0</version>
</dependency>
2.编写配置文件
@SpringBootConfiguration
public class RedisConfig {
private Logger logger = LoggerFactory.getLogger(getClass());
@Bean
public RedissonClient redissonClient(){
RedissonClient redissonClient = null;
//获取config的实例
Config config = new Config();
//设置请求的url地址
String url = "redis://114.55.219.117:6379";
//设置config
config.useSingleServer().setAddress(url);
//通过Redisson创建一个客户端对象
try {
redissonClient = Redisson.create(config);
logger.info("创建RedissonClient成功");
return redissonClient;
} catch (Exception e){
logger.info("创建RedissonClient失败");
return null;
}
}
}
3.编写lock的类
@Component
public class DistributeRedisLock {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private RedissonClient redissonClient;
/**
* 加锁
* @return
*/
public boolean lock(String lockName){
try {
if(null==redissonClient){
logger.info("注入redissonClient对象失败");
return false;
}
//获取这个锁
RLock lock = redissonClient.getLock(lockName);
lock.lock(30, TimeUnit.SECONDS);
logger.info("加锁成功");
return true;
} catch (Exception e) {
logger.info("出现异常加锁失败");
return false;
}
}
/**
* 释放锁
* @return
*/
public boolean unlock(String lockName){
try {
if(null==redissonClient){
logger.info("释放锁失败"+lockName);
return false;
}
//获取这个锁
RLock lock = redissonClient.getLock(lockName);
if(null!=lock){
lock.unlock();
logger.info("释放锁成功");
return true;
}
return false;
} catch (Exception e) {
logger.info("释放锁失败");
return false;
}
}
}
4.调用
public synchronized String produceStockRedisson(){
String lock = "lock";
try {
boolean lock1 = distributeRedisLock.lock(lock);
if(lock1){ //说明加锁成功
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("trainTickets"));
//判断票数是否大于0
if(stock>0){
//票数减一
int rStock = stock - 1;
//将当前票数存进redis
stringRedisTemplate.opsForValue().set("trainTickets",String.valueOf(rStock));
logger.info("扣减库存成功。。剩余库存"+rStock);
}else{ //说明票数不足
logger.info("库存扣减失败,库存不足");
}
}else {
return "当前排队人数过多";
}
} finally {
distributeRedisLock.unlock(lock);
}
return "买票成功";
}
3、SpringBoot整合下的键值序列化
键值序列化简单来说就是key和valu存储在redis中的形式
自定义序列化器:
public class BoBoSerializer implements RedisSerializer {
private Class aClass;
public BoBoSerializer(Class aClass){
this.aClass = aClass;
}
/**
* 序列化
* @param o
* @return
* @throws SerializationException
*/
@Override
public byte[] serialize(Object o) throws SerializationException {
if(null==o){
return null;
}
//将值转换为json对象
String s = JSON.toJSONString(o);
return s.getBytes(Charset.forName("UTF-8"));
}
/**
* 反序列化
* @param bytes
* @return
* @throws SerializationException
*/
@Override
public Object deserialize(byte[] bytes) throws SerializationException {
if(null==bytes){
return null;
}
String result = new String(bytes);
//将json转换为Java对象
return JSON.parseObject(result,aClass);
}
}
2.编写配置文件
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
//设置连接工厂
redisTemplate.setConnectionFactory(redisConnectionFactory);
//设置键的序列化器
redisTemplate.setStringSerializer(new StringRedisSerializer());
//设置值的序列化器
redisTemplate.setValueSerializer(new BoBoSerializer(Object.class));
return redisTemplate;
}
测试:
redisTemplate.opsForValue().set("user",new User(1,"xbb","123"));
4、Redis开发中常见问题
1、Redis的缓存穿透
后台获取数据首先去redis中查询,redis中没有,然后去数据库中查询,发现没有,这样的话,每一个线程进来都会访问数据库,这样数据库就容易崩溃,这种问题就叫缓存穿透
解决方案:假设第一个线程没找到的时候,将数据库中的值设为"",并且同步到redis中,这样后面的线程访问的时候就不会进入数据库了
2、缓存雪崩
缓存雪崩是指,缓存层出现了错误,不能正常工作了。于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。也就是redis挂了,所有请求都到数据库去了。指在某一个时间段,缓存集中过期失效
解决方案:
- 在计算出key过期时间之后,动态加上一个范围内的随机数,作为key的最终过期时间,错峰过期
- 做缓存的高可用
- 缓存做一个降级处理
- 做redis的缓存预热
3、redis脑裂
客户端向主服务器写入了数据 但是主服务器还没有来得及同步的情况下 主服务器死了 那么这个时候就会选举新的主服务器 原来的主服务器在一段时间之后 又好了 那么这个时候 原来的主服务器 只能作为从服务器了 原来主服务器的数据 没有办法进行同步 这种问题 就是redis的脑裂问题
解决方案:
min-slaves-to-write 1:在我们客户端写入数据的时候 至少保证 主服务器上有一个从服务器 处于正常连接才能写入这个数据
min-slaves-max-lag 10 :主从同步的时间 10s