【redis入门到实战 day5】

6 篇文章 0 订阅
3 篇文章 0 订阅

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集群保证高可用和高并发特性

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值