redis动手实现秒杀抢购功能
文章目录
基于StringRedisTemplate封装一个缓存工具类
@Component
@Slf4j
public class CacheClient {
private final StringRedisTemplate stringRedisTemplate;
public CacheClient(StringRedisTemplate stringRedisTemplate){
this.stringRedisTemplate = stringRedisTemplate;
}
/**
* 将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间
* @param key
* @param value
* @param time
* @param unit
*/
public void set(String key , Object value, Long time, TimeUnit unit){
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit);
}
/**
* 将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题
* @param key
* @param value
* @param time
* @param unit
*/
public void setWithLogicalExpire(String key,Object value,Long time,TimeUnit unit){
//设置逻辑过期
RedisData redisData = new RedisData();
redisData.setData(value);
//过期时间=当前时间+传递过来的时间(转成秒)
redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
//写入redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
}
/**
* 根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
* @param keyPrefix 缓存key值
* @param id 泛型参数
* @param type 查询对象
* @param dbFallback 方法语句
* @param time 超时时间
* @param unit 时间类型
* @param <R> 泛型返回对象根据入参查询对象type决定
* @param <ID> 方法语句入参
* @return
*/
public <R,ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type,
Function<ID,R> dbFallback,Long time, TimeUnit unit){
//去redis中查询
String key = keyPrefix+id;
String json = stringRedisTemplate.opsForValue().get(key);
//如果不为空则直接返回
if(StrUtil.isNotBlank(json)){
R r = JSONUtil.toBean(json, type);
return r;
}
//判断命中的是否是空值
if(json != null){
return null;
}
//不存在,根据id查询
R r = dbFallback.apply(id);
//若数据库也没查到,redis中存放key值,避免缓存穿透问题
if(r == null){
//放入空值
stringRedisTemplate.opsForValue().set(key,"");
//设置过期时间
stringRedisTemplate.expire(key, Duration.ofMinutes(CACHE_NULL_TTL));
return null;
}
//如果查到了则放入缓存中
this.set(key,r,time,unit);
return r;
}
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
/**
* 根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题
* @param keyPrefix
* @param id
* @param type
* @param dbFallback
* @param time
* @param unit
* @param <R>
* @param <ID>
* @return
*/
public <R,ID> R queryWithLogicExpire(String keyPrefix, ID id, Class<R> type,
Function<ID,R> dbFallback,Long time, TimeUnit unit){
//1-去redis中查询
String key = keyPrefix+id;
String json = stringRedisTemplate.opsForValue().get(key);
//2 如果未命中 则直接返回
if(StrUtil.isBlank(json)){
return null;
}
//3 命中,先把json反序列化为对象
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
//获取过期时间
LocalDateTime expireTime = redisData.getExpireTime();
//4 判断是否过期
if(expireTime.isAfter(LocalDateTime.now())){
//未过期 直接返回店铺信息
return r;
}
//5 已过期 需要缓存重建
//5.1 获取互斥锁
String lockKey = LOCK_SHOP_KEY+id;
boolean isLock = tryLock(lockKey);
//5.2 判断是否获取成功
if(isLock){
//5.3 成功 开启独立线程 实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(()->{
//5.4 缓存重建
try {
//查询数据库数据
R r1 = dbFallback.apply(id);
//写入redis
this.setWithLogicalExpire(key,r1,time,unit);
} catch (Exception e) {
throw new RuntimeException(e);
}finally {
//释放锁
unlock(lockKey);
}
});
}
//5.5 返回过期对象
return r;
}
private boolean tryLock(String key){
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key,"1",10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
private void unlock(String key){
stringRedisTemplate.delete(key);
}
//调用示例
Shop shop = cacheClient.queryWithPassThrough
(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);
自定义全局唯一ID生成器
全局ID生成器,是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足下列特性 【唯一性 ,高可用,高性能,递增性,安全性】
全局唯一ID生成策略的方式:
1-UUID
2-Redis自增
3-snowflake算法
4-数据库自增
*Redis自增ID策略:
每天一个key,方便统计订单量
ID构造是 时间戳 + 计数器
自定义全局唯一ID生成器具体实现
@Component
public class RedisIdWorker {
/**
* 开始时间戳
*/
private static final long BEGIN_TIMESTAMP=1641772800L;
/**
* 序列号位数
*/
private static final long COUNT_BITS=32;
@Resource
private StringRedisTemplate stringRedisTemplate;
public long nextId(String keyPrefix){
//生成时间戳-当前时间-秒
long nowSecond = now().toEpochSecond(ZoneOffset.UTC);
long timeStamp = nowSecond - BEGIN_TIMESTAMP;
//生成序列号
//1 获取当前日期
String date = now().format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
//2 自增长
long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
// 拼接返回
//时间戳移到高位
return timeStamp << COUNT_BITS | count;
}
public static void main(String[] args) {
LocalDateTime time = LocalDateTime.of(2022,1,10,0,0);
long second = time.toEpochSecond(ZoneOffset.UTC);
System.out.println("second = "+second);
}
}
悲观锁与乐观锁
##############简介###################
悲观锁:
认为线程安全问题一定会发生,因此在操作数据之前先获取锁,确保线程串行执行。 例如Synchronized、Lock都属于悲观锁乐观锁
认为线程安全问题不一定会发生,因此不加锁,只是在更新数据时去判断有没有其它线程对数据做了修改。
如果没有修改则认为是安全的,自己才更新数据。
如果已经被其它线程修改说明发生了安全问题,此时可以重试或异常
##############优缺点###################
悲观锁:
添加同步锁,让线程串行执行 优点:简单粗暴 缺点:性能一般
乐观锁:
不加锁,在更新时判断是否有其它线程在修改 优点:性能好
缺点:存在成功率低的问题
为什么需要分布式锁
服务部署到多台tomcat,jvm中的锁监视器并不是共享的,每个都是独立的 会导致线程安全问题,如图
解决方案:分布式锁
分布式锁的概念:满足分布式系统或集群模式下多进程可见并且互斥的锁
定义分布式锁工具类
public class SimpleRedisLock implements ILock {
//锁的名称
private String name;
//锁的统一前缀
private static final String KEY_PREFIX="lock:";
//线程id前缀
private static final String ID_PREFIX= UUID.randomUUID().toString(true)+"-";
//使用lua脚本实现原子性
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);
}
private StringRedisTemplate stringRedisTemplate;
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean tryLock(long timeoutSec) {
//获取线程标识-用于value存储
String threadId = ID_PREFIX+ Thread.currentThread().getId();
//获取锁
Boolean lockResult = stringRedisTemplate.opsForValue().
setIfAbsent(KEY_PREFIX + name, threadId , timeoutSec, TimeUnit.SECONDS);
//自动拆箱有空指针风险,用常量判断
return Boolean.TRUE.equals(lockResult);
}
@Override
public void unlock() {
//调用lua脚本 获取锁中的标示,判断是否与当前线程标示一致
// lua脚本可以保证判断和删除操作的原子性
stringRedisTemplate.execute(
//要执行的脚本
UNLOCK_SCRIPT,
//key值参数
Collections.singletonList(KEY_PREFIX + name),
//value值参数
ID_PREFIX+ Thread.currentThread().getId()
);
}
/* @Override
public void unlock() {
//获取线程标识
String threadId = ID_PREFIX+ Thread.currentThread().getId();
//获取锁中标识
String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
//判断是否一致
if(threadId.equals(id)){
//释放锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}*/
}
代码实现:通过分布式锁实现一人一单限时秒杀
@Resource
SeckillVoucherServiceImpl seckillVoucherService;
@Resource
private RedisIdWorker redisIdWorker;
@Resource
StringRedisTemplate stringRedisTemplate;
@Override
public Result secKillVoucher(Long voucherId) {
//1,查询优惠券
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
//2 判断秒杀是否开始
if(voucher.getBeginTime().isAfter(LocalDateTime.now())){
//尚未开始
return Result.fail("秒杀尚未开始");
}
//3 判断秒杀是否结束
if(voucher.getEndTime().isBefore(LocalDateTime.now())){
//已经结束
return Result.fail("秒杀已结束");
}
//4 判断库存是否充足
if(voucher.getStock() < 1){
//库存不足
return Result.fail("库存不足");
}
Long userId = UserHolder.getUser().getId();
//1 分布式锁.
//1.1创建锁对象
SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
//1.2 获取锁
boolean isLock = lock.tryLock(3000);
//1.3 判断是否获取锁成功
if(!isLock){
//1.4 失败 返回错误或重试
return Result.fail("不可重复下单");
}
//2 单机锁.事务的方法由当前对象调用 会引发事务失效问题,所以需要用到代理对象来调用
// 2.1 synchronized (userId.toString().intern()) {
//1.5 成功
try {
//当前对象的代理对象 (由spring管理)
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
} finally {
//释放锁
lock.unlock();
}
// }
}
@Transactional
public Result createVoucherOrder(Long voucherId) {
//5 一人一单
Long userId = UserHolder.getUser().getId();
// intern() 方法返回相同字符串的地址(若字符串池中已包含相同字符串则直接返回,若不包含则加入池中并返回其引用)
//5.1 查询订单
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
//5.2 判断是否存在
if (count > 0) {
return Result.fail("每个用户只能购买一次");
}
//6 扣减库存
seckillVoucherService.update().
setSql("stock = stock -1").
eq("voucher_id", voucherId).gt("stock", 0).
update();
//7 创建订单
VoucherOrder voucherOrder = new VoucherOrder();
//7.1 订单id
long orderId = redisIdWorker.nextId("orderId");
voucherOrder.setId(orderId);
//7.2 用户id
voucherOrder.setUserId(userId);
//7.3 代金券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
//8 返回订单id
return Result.ok(orderId);
}
总结
基于Redis的分布式锁实现思路:
利用set nx ex获取锁,并设置过期时间,保存线程标示
释放锁时先判断线程标示是否与自己一致,一致则删除锁
特性:
利用set nx满足互斥性
利用set ex保证故障时锁依然能释放,避免死锁,提高安全性
利用Redis集群保证高可用和高并发特性