分布式锁的应用场景
- 互联网秒杀
- 抢优惠券
- 接口幂等性校验
首先来看分布式锁解决超卖的场景
package com.zhuge;
import org.redisson.Redisson;
import org.redisson.RedissonRedLock;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
@RestController
public class IndexController {
@Autowired
private Redisson redisson;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RedisTemplate redisTemplate;
@RequestMapping("/deduct_stock")
public String deductStock() {
String lockKey = "lock:product_101";
//Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "zhuge");
//stringRedisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);
/*String clientId = UUID.randomUUID().toString();
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS); //jedis.setnx(k,v)
if (!result) {
return "error_code";
}*/
//获取锁对象
RLock redissonLock = redisson.getLock(lockKey);
//加分布式锁
redissonLock.lock(); // .setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS);
try {
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
} finally {
/*if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
stringRedisTemplate.delete(lockKey);
}*/
//解锁
redissonLock.unlock();
}
return "end";
}
@RequestMapping("/redlock")
public String redlock() {
String lockKey = "product_001";
//这里需要自己实例化不同redis实例的redisson客户端连接,这里只是伪代码用一个redisson客户端简化了
RLock lock1 = redisson.getLock(lockKey);
RLock lock2 = redisson.getLock(lockKey);
RLock lock3 = redisson.getLock(lockKey);
/**
* 根据多个 RLock 对象构建 RedissonRedLock (最核心的差别就在这里)
*/
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
try {
/**
* waitTimeout 尝试获取锁的最大等待时间,超过这个值,则认为获取锁失败
* leaseTime 锁的持有时间,超过这个时间锁会自动失效(值应设置为大于业务处理的时间,确保在锁有效期内业务能处理完)
*/
boolean res = redLock.tryLock(10, 30, TimeUnit.SECONDS);
if (res) {
//成功获得锁,在这里处理业务
}
} catch (Exception e) {
throw new RuntimeException("lock fail");
} finally {
//无论如何, 最后都要解锁
redLock.unlock();
}
return "end";
}
}
在上面这段代码中当多个线程来同时扣减库存,对stock进行减1操作,假设初始值:stock = 50 多个线程都来进行库存扣减操作,stock = 49,但是正常逻辑是库存应该扣50 - 2 = 48,因此在高并发场景下就有可能出现超卖的现象。用Sychronized锁或者JDk层面的锁只能实现单机锁库存,而在高并发场景下是没有办法实现的。在分布式环境下肯定需要用到分布式锁----实现方式有之前讲到过的setnx
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent("lockKey","haha");
Redis中存在lockKey这个key值就不会执行上面的语句,如果不存在就会执行。 进行判断:
if(!result){//true,返回状态码
return "1001";//
}
只让一个线程执行下面的代码。
Redission锁续期
线程会去判断当前执行的业务逻辑是否结束,未结束的话Redissn分布式锁框架有个看门狗机制,会给当前线程加的锁进行续期。
lua脚本会将Redis的多条命令封装成一个脚本,这个脚本命令是原子性的-----redis单线程
Redis缓存设计
一. 缓存击穿:访问某个热点key的时候,在缓存中失效,一下子这些用户请求都直接打到数据库上,可能会造成数据库瞬间压力过大而直接挂掉。
- 要保证同一时刻只能有一个请求访问某个productID的数据库商品信息,加锁的方式实现上面的功能,接着把从数据库中查询到的结果重新放入缓存中。
- 在key快要过期的时候给key自动续期,重新设置过期时间。在很多请求第三方平台接口的时候,往往需要先调用一个获取token的接口,然后用这个token作为参数,请求真正的业务接口,一般获取到的token是有有效期的,比如24小时之后失效,如果每次请求对方的业务接口,都要先调用一次获取token的接口,显然比较麻烦而且性能不太好,这时候我们可以把第一次获取到的token缓存起来,请求对方业务接口时从缓存中获取token,同时有一个job每隔一段时间,比如每隔12个小时请求一次获取token的接口,不停刷新token,重新获取刷新token的时间
- 此外对于很多热点key可以设置永不过期,比如参与秒杀的热门商品,由于这类商品id并不多,在缓存中国我们可以不设置过期时间,在秒杀活动开始前先用一个程序在数据库中查询商品数据,提前放入缓存中预热,秒杀活动结束之后手动删除缓存即可
二. 缓存穿透:用户恶意攻击,缓存和数据库中都没有数据。缓存null值、布隆过滤器
三. 缓存雪崩:缓存击穿说的是某一个热门key失效了,而缓存雪崩说的是多个热门key同时失效,导致大量的请求访问数据库,而数据库扛不住压力,而宕机,从而导致其他的服务器宕机。缓器服务器宕机了----机器硬件问题,或者是机房网络原因造成了整个缓存的不可用。归根结底都是当量的请求,透过缓存,直接打到数据库上
- 避免缓存同时失效,给key设置不同的过期时间。可以在过期时间商加一个160s的随机数,即:
实际过期时间 = 设置的过期时间 + 160s的随机数
这样在高并发的环境下,多个请求同时设置过期时间 ,由于有随机数的存在,而不会导致多个热点key同时过期的情况,在前期做系统设计时,可以做一些高可用的架构
如果使用了redis可以使用哨兵模式或者集群模式,避免因为单节点故障导致这个redis服务不可用的情况,使用哨兵模式之后,当某个master几点下线时自动将该master节点下的某个slave服务升级为master服务,替代已下线的master服务处理请求。
如果做了高可用架构,redis服务还是挂了该怎么办呢?这个时候可以考虑== ==服务降级====配置一些默认的兜底数据 ,
程序中有一个全局开关,比如有是个请求在最近一分钟内从redis获取数据失败,则全局开关打开直接从配置中心获取默认的数据,当然,这个时候还需要有一个job每个一定的时间去redis中获取数据,如果在一分钟内可以获取到两次数据(这个参数可以自己定义),则把全局开关关闭,后面来的请求又可以正常从redis中获取数据了。该方案根据实际业务场景设置