Redis入门到实战(正在完善中,请多指教)

600 阅读14分钟

Redis

前言

此笔记是循序渐进,一点一点的优化下去,所以小伙伴们不要感觉啰嗦和奇怪。学习嘛,都是一点一滴的进行下去,
如果直接给到最终的结果,我们不会了解我们为什么要优化?为什么大牛是这么思考?为什么我们自己想不到!
这也是锻炼我们思维方式的一种机会。
​
    最后因本人个人能力有限。如果文章有任何问题或者需要改善和完善的地方,欢迎在评论区指出,共同进步!
    最后因本人个人能力有限。如果文章有任何问题或者需要改善和完善的地方,欢迎在评论区指出,共同进步!
    最后因本人个人能力有限。如果文章有任何问题或者需要改善和完善的地方,欢迎在评论区指出,共同进步!
​
Redis笔记我会一直更新下去。若以前的内容有问题或不完善的地方我也会同步更新。

1、什么是缓存?

缓存就是数据交换的缓冲区(称作Cache),是存贮数据的临时地方,一般读写性能较高。

2、添加Redis缓存流程

image-20220502150121756

3、缓存更新策略

image-20220502153011365

主动更新策略:

Cache Aside Pattern:由缓存的调用者,在更新数据库的同时更新缓存
​
操作缓存和数据库时有三个问题需要考虑:
    1、删除缓存还是更新缓存?
        ◆更新缓存:每次更新数据库都更新缓存,无效写操作较多    √
        ◆删除缓存:更新数据库时让缓存失效,查询时再更新缓存    ×
    2、如何保证缓存和数据库的操作同时成功或失败?
        ◆单体系统,将缓存与数据库操作放在一个事务
        ◆分布式系统,利用TCC等分布式事务方案
    3、先操作缓存还是先操作数据库?
        ◆先删除缓存,在操作数据库   ×
        ◆先操作数据库,在操作缓存   √
    原因:如下图所示,
        ●先删除缓存,在操作数据库:发生概率较高,更新缓存比更新数据库速度快很多
        ●操作数据库,再删除缓存:虽然也会有问题,但是,概率极低
        (需要满足3个条件:1、两个线程并行执行。
                        2、线程1查询的时候,缓存恰好失效了。
                        3、在写入缓存的这个微秒级别,有一个线程突然更新数据库,然后删除缓存。)

image-20220502161959303

总结

image-20220502162933437

4、缓存穿透

缓存穿透:是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。

常见的解决方案有两种:

缓存空对象

◆优点:实现简单,维护方便

◆缺点:

●额外的内存消耗(设置TTL,这是短一点的)

●可能造成短期的不一致

image-20220502202722349

布隆过滤器

    一种数据结构,是由一串很长的二进制向量组成,可以将其看成一个二进制数组。既然是二进制,那么里面存放的不是0,就是1,但是初始默认值都是0

◆优点:占用内存极少,没有多余的key

◆缺点:

●实现复杂

●随着数据的增加,误判率会增加;还有无法判断数据一定存在;另外还有一个重要缺点,无法删除数据。

image-20220502203349910

5、缓存雪崩

    缓存雪崩:是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
    
解决方案:
    ◆给不同的Key的TTL添加随机值
    ◆利用Redis集群提高服务的可用性
    ◆给缓存业务添加降级限流策略
    ◆给业务添加多级缓存

image-20220502205621697

6、缓存击穿

    缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
    
解决方案:
    ◆互斥锁    基于Redis的setnx
    ◆逻辑过期

两种方案对比如图所示

image-20220502211420043

image-20220502211502513

7、封装Redis工具类

package com.hmdp.utils;
​
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
​
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
​
import static com.hmdp.utils.RedisConstants.CACHE_NULL_TTL;
import static com.hmdp.utils.RedisConstants.LOCK_SHOP_KEY;
​
@Slf4j
@Component
public class CacheClient {
​
    private final StringRedisTemplate stringRedisTemplate;
​
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
​
    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }
​
    public void set(String key, Object value, Long time, TimeUnit unit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
    }
​
    //存入redis,带有逻辑过期的
    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));
    }
​
    //缓存穿透
    public <R,ID> R queryWithPassThrough(
            String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit){
        String key = keyPrefix + id;
        // 1.从redis查询商铺缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if (StrUtil.isNotBlank(json)) {
            // 3.存在,直接返回
            return JSONUtil.toBean(json, type);
        }
        // 判断命中的是否是空值
        if (json != null) {
            // 返回一个错误信息
            return null;
        }
​
        // 4.不存在,根据id查询数据库
        R r = dbFallback.apply(id);
        // 5.不存在,返回错误
        if (r == null) {
            // 将空值写入redis
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            // 返回错误信息
            return null;
        }
        // 6.存在,写入redis
        this.set(key, r, time, unit);
        return r;
    }
​
    //缓存击穿----->逻辑过期
    public <R, ID> R queryWithLogicalExpire(
            String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
        String key = keyPrefix + id;
        // 1.从redis查询商铺缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if (StrUtil.isBlank(json)) {
            // 3.存在,直接返回
            return null;
        }
        // 4.命中,需要先把json反序列化为对象
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
        LocalDateTime expireTime = redisData.getExpireTime();
        // 5.判断是否过期
        if(expireTime.isAfter(LocalDateTime.now())) {
            // 5.1.未过期,直接返回店铺信息
            return r;
        }
        // 5.2.已过期,需要缓存重建
        // 6.缓存重建
        // 6.1.获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        boolean isLock = tryLock(lockKey);
        // 6.2.判断是否获取锁成功
        if (isLock){
            // 6.3.成功,开启独立线程,实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    // 查询数据库
                    R newR = dbFallback.apply(id);
                    // 重建缓存
                    this.setWithLogicalExpire(key, newR, time, unit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }finally {
                    // 释放锁
                    unlock(lockKey);
                }
            });
        }
        // 6.4.返回过期的商铺信息
        return r;
    }
    
    //缓存击穿----->互斥锁
    public <R, ID> R queryWithMutex(
            String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
        String key = keyPrefix + id;
        // 1.从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            // 3.存在,直接返回
            return JSONUtil.toBean(shopJson, type);
        }
        // 判断命中的是否是空值
        if (shopJson != null) {
            // 返回一个错误信息
            return null;
        }
​
        // 4.实现缓存重建
        // 4.1.获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        R r = null;
        try {
            boolean isLock = tryLock(lockKey);
            // 4.2.判断是否获取成功
            if (!isLock) {
                // 4.3.获取锁失败,休眠并重试
                Thread.sleep(50);
                return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);
            }
            // 4.4.获取锁成功,根据id查询数据库
            r = dbFallback.apply(id);
            // 5.不存在,返回错误
            if (r == null) {
                // 将空值写入redis
                stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                // 返回错误信息
                return null;
            }
            // 6.存在,写入redis
            this.set(key, r, time, unit);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            // 7.释放锁
            unlock(lockKey);
        }
        // 8.返回
        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);
    }
}

8、实现优惠券秒杀

8.1、Redis实现全局唯一ID

image-20220503233614789

package com.hmdp.utils;
​
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
​
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
​
@Component
public class RedisIdWorker {
    /**
     * 开始时间戳
     */
    private static final long BEGIN_TIMESTAMP = 1640995200L;
    /**
     * 序列号的位数
     */
    private static final int COUNT_BITS = 32;
​
    private StringRedisTemplate stringRedisTemplate;
​
    public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }
​
    public long nextId(String keyPrefix) {
        // 1.生成时间戳
        LocalDateTime now = LocalDateTime.now();
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
        long timestamp = nowSecond - BEGIN_TIMESTAMP;
​
        // 2.生成序列号
        // 2.1.获取当前日期,精确到天
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        // 2.2.自增长  "icr:" + keyPrefix + ":"Redis 自增是有上限的 上限是2^64,但是我们这里是2^32,很容易超过这个上限
        //所以,业务后面拼上一个时间戳
        long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
​
        // 3.拼接并返回 timestamp << COUNT_BITS意思是时间戳位运算,向左移动32位。| count 接着让count参与或运算
        return timestamp << COUNT_BITS | count;
    }
}

8.2、秒杀优惠券超卖问题

  解决方案:(这里就不一一详细的展开描述了,因为重点不是这里)  
    1、乐观锁
    2、悲观锁

8.3、分布式锁

8.3.1、分布式原理

image-20220504201655133

8.3.2、分布式锁的实现比较

image-20220504203205917

8.3.3、基于Redis的分布式锁方案
1、获取锁:
	互斥:确保只能有一个线程获取锁
	非阻塞:尝试一次,成功返回true,失败返回false
		SET lock xxx NX EX 10(秒)   (添加锁,NX是互斥,EX是设置超时时间)
			成功返回:OK  失败返回:nil
2、释放锁:
	手动释放:DEL key   (释放锁,删除即可)
	超时释放:获取锁时添加一个超时时间
8.3.4、基于Redis的分布式锁流程

image-20220504204912019

8.3.5、实现Redis分布式锁代码
package com.hmdp.utils;

import cn.hutool.core.lang.UUID;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.util.Collections;
import java.util.concurrent.TimeUnit;

public class SimpleRedisLock implements ILock {

    private String name;
    private StringRedisTemplate stringRedisTemplate;

    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    private static final String KEY_PREFIX = "lock:";
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    static {
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }

    @Override
    public boolean tryLock(long timeoutSec) {
        // 获取线程标示
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        // 获取锁
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
    }

    //释放锁
    @Override
    public void unlock() {
        // 调用lua脚本
        stringRedisTemplate.execute(
                UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + name),
                ID_PREFIX + Thread.currentThread().getId());
    }
}

lua脚本的实现

-- 比较线程标示与锁中的标示是否一致
if(redis.call('get', KEYS[1]) ==  ARGV[1]) then
    -- 释放锁 del key
    return redis.call('del', KEYS[1])
end
return 0

注:以下面的方式释放锁会造成分布式锁的原子性问题

    @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);
        }
    }

image-20220505230500815

    如上图所示,当线程1释放锁(锁标识是一致的)的时候,出现了阻塞(GC的时候会出现阻塞)的情况,并且恰好触发了锁的超时释放,那么此时线程2又可以获取锁了,并且开始执行业务,恰好此时线程1阻塞(GC结束)结束,开始释放锁,于是就会出现线程1把线程2的锁给释放掉了(原因:线程1判断锁标识这一步已经通过了,要执行释放锁这一步了),那么线程3又可以获取锁了,执行自己的业务,此时又出现了并发的情况(线程2和线程3)。造成以上的原因就是判断锁标识和释放锁是两个动作。

8.4、基于Redis的分布式锁优化

8.4.1、Redisson
    因为我们上述自己写的分布式锁(基于setnx)代码存在三个问题:
    1、不可重入:同一个线程无法多次获取同一把锁
    2、不可重试:获取锁只尝试一次就返回false,没有重试机制
    3、超时释放:锁超时释放虽然可以避免死锁,但如果业务执行耗时较长,也会导致锁释放,存在安全隐患
    
    所以我们引入了Redission
    
    Redisson是一个在Redis的基础上实现的Java驻内存数据网格〈Iln-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。
    官网地址: https://redisson.org
    GitHub地址: https://github.com/redisson/redisson

image-20220507180104017

引入依赖

<dependency> 
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.13.6</version>
</dependency>

配置Redission客户端

@Configuration
public class RedisConfig 
{
    @Bean
    public RedissonClient redissonClient() {
        // 配置类
        Config config = new Config();
        //添加redis地址,这里添加了单点的地址,也可以使用config.useClusterServers()添加集群地址		
        config.useSingleServer().setAddress("redis://192.168.150.101:6379").setPassowrd("123321");    
        // 创建客户端
        return Redisson.create(config);
    }
}

使用Redisson的分布式锁

@Resource
private RedissonClient redissonClient;
@Test
void testRedisson() throws InterruptedException {
    // 获取锁(可重入),指定锁的名称
    RLock lock = redissonClient.getLock("anyLock");
    // 尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
    boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS);
    // 判断释放获取成功
    if(isLock){
        try {
            System.out.println("执行业务");
        }finally {
            // 释放锁
            lock.unlock();
        }
    }
}
8.4.2、Redisson可重入锁原理
`Redisson可重入锁原理`基本就是利用ReentrantLock的原理。
`Redisson可重入锁原理`如下图:

image-20220508233350474

获取锁的Lua脚本:

local key = KEYS[1]; -- 锁的key
local threadId = ARGV[1]; -- 线程唯一标识
local releaseTime = ARGV[2]; -- 锁的自动释放时间
-- 判断是否存在
if(redis.call('exists', key) == 0) 
    then
    -- 不存在, 获取锁
    redis.call('hset', key, threadId, '1'); 
    -- 设置有效期
    redis.call('expire', key, releaseTime);
    return 1; -- 返回结果
end;
-- 锁已经存在,判断threadId是否是自己
if(redis.call('hexists', key, threadId) == 1) 
    then
    -- 存在, 获取锁,重入次数+1
    redis.call('hincrby', key, threadId, '1'); 
    -- 设置有效期
    redis.call('expire', key, releaseTime); 
    return 1; 
    -- 返回结果
end;
return 0; -- 代码走到这里,说明获取锁的不是自己,获取锁失败

释放锁的Lua脚本:

local key = KEYS[1]; -- 锁的key
local threadId = ARGV[1]; -- 线程唯一标识
local releaseTime = ARGV[2]; -- 锁的自动释放时间
-- 判断当前锁是否还是被自己持有
if (redis.call('HEXISTS', key, threadId) == 0) 
    then
    return nil; -- 如果已经不是自己,则直接返回
end;
-- 是自己的锁,则重入次数-1
local count = redis.call('HINCRBY', key, threadId, -1);
-- 判断是否重入次数是否已经为0 
if (count > 0) then
    -- 大于0说明不能释放锁,重置有效期然后返回 
    redis.call('EXPIRE', key, releaseTime); 
    return nil;
else  
    -- 等于0说明可以释放锁,直接删除  
    redis.call('DEL', key);  
    return nil;
end;
8.4.3、Redisson分布式锁原理

image-20220509010135207

总结:

Redisson分布式锁原理:
    ◆可重入:利用hash结构记录线程id和重入次数
    ◆可重试:利用信号量和PubSub功能实现等待、唤醒,获取锁失败的重试机制
    ◆超时续约:利用watchDog,每隔一段时间( releaseTime/ 3),重置超时时间
8.4.4、Redossion分布式锁主从一致性问题
	redis的主从模式会在同步的时候存在一定的延时问题,那么此时就会出现分布式锁主从一致的问题。为什么会有呢?是因为当一个java应用获取锁,那么主节点就要像从节点同步,但是恰好此时主节点宕机了,同步尚未完成,那么就导致锁丢失,那么其他线程也可以获取锁,就会出现线程并发安全问题。
8.4.5、Redossion解决分布式锁主从一致性问题
	既然主从是导致分布式锁主从一致性问题,那么干脆不要主从,所有的节点都变成独立的节点,相互之间没有任何关系。那么此时获取锁的方式就变了,现在我们必须依次的向多个Redis节点都获取锁。如果,此时一个节点宕机了(若也出现主从也没有同步完成的情况),那么也没有关系,因为我们需要每一个节点都要拿到锁才算成功,这样只要有一个节点是存活的,那么就不会出现锁失效的问题。这种方案在Redossion中有一个名字叫做multLock(连锁)。
1、代码实现
@Configuration
public class RedissonConfig {

    @Bean
    public RedissonClient redissonClient1(){
        // 配置
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.150.101:6379").setPassword("123321");
        // 创建RedissonClient对象
        return Redisson.create(config);
    }
    @Bean
    public RedissonClient redissonClient2(){
        // 配置
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.150.101:6380");
        // 创建RedissonClient对象
        return Redisson.create(config);
    }
    @Bean
    public RedissonClient redissonClient3(){
        // 配置
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.150.101:6381");
        // 创建RedissonClient对象
        return Redisson.create(config);
    }
}

RLock lock1 =redissonclient.getLock( name: "order");
RLock lock2 = redissonclient2.getLock( name: "order");
RLock lock3 =redissonclient3.getLock( name: "order");
//创建联锁multiLock
lock = redissonClient.getMultiLock(lock1,lock2,lock3);
// 尝试获取锁
boolean isLock = lock.tryLock(1L, TimeUnit.SECONDS);

image-20220511202811796

8.5、Redis分布式锁总结

1)不可重入Redis分布式锁:
    ◆原理:利用setnx的互斥性;利用ex避免死锁;释放锁时判断线程标示
    ◆缺陷:不可重入、无法重试、锁超时失效
2)可重入的Redis分布式锁:
    ◆原理:利用hash结构,记录线程标示和重入次数;利用watchDog延续锁时间;利用信号量控制锁重试等待
    ◆缺陷: redis宕机引起锁失效问题
3)Redisson的multiLock:
    ◆原理:多个独立的Redis节点,必须在所有节点都获取重入锁,才算获取锁成功

8.6、Redis优化秒杀-基于阻塞队列

业务流程逻辑优化

image-20220511233819265

判断库存、一人一单的逻辑优化

1、把优惠券的库存信息和优惠券的订单信息(userId)缓存到redis中
2、一人一单的解决方式就是查看优惠券的订单信息,如果有,就表示已经买过。
3、优惠券信息使用String类型存储,优惠券的订单信息使用set集合
4、保存优惠券id、

image-20220511234351351

8.6.1、代码实现
    //优惠券库存信息保存到Redis中
    @Override
    @Transactional
    public void addSeckillVoucher(Voucher voucher) {
        //省略其他业务逻辑代码,因为这个不是重点
        
        // 保存秒杀库存到Redis中  永久保存,不设置过期时间
        stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString());
    }
​
//1、基于Lua脚本,判断秒杀库存、一人一单,决定用户是否抢购成功
//2、如果抢购成功,将优惠券id和用户id封装后存入阻塞队列
//3、开启线程任务,不断从阻塞队列中获取信息,实现异步下单功能
@Slf4j
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
    @Resource
    private ISeckillVoucherService seckillVoucherService;
    @Resource
    private RedisIdWorker redisIdWorker;
    @Resource
    private RedissonClient redissonClient;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
​
    private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
​
    static {
        SECKILL_SCRIPT = new DefaultRedisScript<>();
        SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
        SECKILL_SCRIPT.setResultType(Long.class);
    }
​
​
    private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();
​
 
    @PostConstruct   //这个注解表示当前类初始化完成之后就执行
    private void init() {
        SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
    }
​
    private class VoucherOrderHandler implements Runnable {
​
   
    private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024 * 1024);
        
    //如果队列里有的话,他就会执行,没有的话,会等待,等待到有了之后会接着执行。
    private class VoucherOrderHandler implements Runnable{
        @Override
        public void run() {
            while (true){
                try {
                    // 1.获取队列中的订单信息
                    VoucherOrder voucherOrder = orderTasks.take();
                    // 2.创建订单
                    createVoucherOrder(voucherOrder);
                } catch (Exception e) {
                    log.error("处理订单异常", e);
                }
            }
        }
    }
​
    private void createVoucherOrder(VoucherOrder voucherOrder) {
        Long userId = voucherOrder.getUserId();
        Long voucherId = voucherOrder.getVoucherId();
        // 创建锁对象
        RLock redisLock = redissonClient.getLock("lock:order:" + userId);
        // 尝试获取锁
        boolean isLock = redisLock.tryLock();
        // 判断
        if (!isLock) {
            // 获取锁失败,直接返回失败或者重试
            log.error("不允许重复下单!");
            return;
        }
​
        try {
            // 5.1.查询订单
            int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
            // 5.2.判断是否存在
            if (count > 0) {
                // 用户已经购买过了
                log.error("不允许重复下单!");
                return;
            }
​
            // 6.扣减库存
            boolean success = seckillVoucherService.update()
                    .setSql("stock = stock - 1") // set stock = stock - 1
                    .eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0
                    .update();
            if (!success) {
                // 扣减失败
                log.error("库存不足!");
                return;
            }
​
            // 7.创建订单
            save(voucherOrder);
        } finally {
            // 释放锁
            redisLock.unlock();
        }
    }
​
    //抢单
    @Override
    public Result seckillVoucher(Long voucherId) {
        Long userId = UserHolder.getUser().getId();
        // 1.执行lua脚本
        Long result = stringRedisTemplate.execute(
                SECKILL_SCRIPT,
                Collections.emptyList(),
                voucherId.toString(), userId.toString()
        );
        int r = result.intValue();
        // 2.判断结果是否为0
        if (r != 0) {
            // 2.1.不为0 ,代表没有购买资格
            return Result.fail(r == 1 ? "库存不足" : "不能重复下单");
        }
        //2.2.为0,有购买资格,把下单信息保存到阻塞队列
        Voucherorder voucherorder = new VoucherOrder();// 2.3.订单id
        long orderId = redisIdworker.nextId( keyPrefix: "order");voucherorder.setId (orderId);
        // 2.4.用户id
        vogcherorder.setUserId(userId); / 2.5.代金券id
        voucherorder.setVoucherId(voucherId);
        //2.6.放入阻塞队列
        orderTasks.add(voucherorder);
        // 3.返回订单id
        return Result.ok(orderId);
    }
}

注:seckill.lua 脚本

-- 1.参数列表
-- 1.1.优惠券id
local voucherId = ARGV[1]
-- 1.2.用户id
local userId = ARGV[2]
-- 1.3.订单id
local orderId = ARGV[3]
​
-- 2.数据key
-- 2.1.库存key
local stockKey = 'seckill:stock:' .. voucherId
-- 2.2.订单key
local orderKey = 'seckill:order:' .. voucherId
​
-- 3.脚本业务
-- 3.1.判断库存是否充足 get stockKey
if(tonumber(redis.call('get', stockKey)) <= 0) then
    -- 3.2.库存不足,返回1
    return 1
end
-- 3.2.判断用户是否下单 SISMEMBER orderKey userId
if(redis.call('sismember', orderKey, userId) == 1) then
    -- 3.3.存在,说明是重复下单,返回2
    return 2
end
-- 3.4.扣库存 incrby stockKey -1
redis.call('incrby', stockKey, -1)
-- 3.5.下单(保存用户)sadd orderKey userId
redis.call('sadd', orderKey, userId)
return 0

8.7、Redis优化秒杀-基于消息队列

基于阻塞对了实现的异步秒杀存在哪些问题?
    1、内存限制的问题-----阻塞队列可能到时OOM异常,虽然本次设置阻塞队列的上限,但是会导致塞满了,放不进去的情况
    2、数据安全的问题-----因为是基于内存的,要是突然宕机,数据就会丢失。任务可能执行一般出现严重问题,也会导致数据丢失。
    
最好消息队列使用MQ等。因为是Redis,所以这里就不过多的介绍了。以后会补充的。