Redis
前言
此笔记是循序渐进,一点一点的优化下去,所以小伙伴们不要感觉啰嗦和奇怪。学习嘛,都是一点一滴的进行下去,
如果直接给到最终的结果,我们不会了解我们为什么要优化?为什么大牛是这么思考?为什么我们自己想不到!
这也是锻炼我们思维方式的一种机会。
最后因本人个人能力有限。如果文章有任何问题或者需要改善和完善的地方,欢迎在评论区指出,共同进步!
最后因本人个人能力有限。如果文章有任何问题或者需要改善和完善的地方,欢迎在评论区指出,共同进步!
最后因本人个人能力有限。如果文章有任何问题或者需要改善和完善的地方,欢迎在评论区指出,共同进步!
Redis笔记我会一直更新下去。若以前的内容有问题或不完善的地方我也会同步更新。
1、什么是缓存?
缓存就是数据交换的缓冲区(称作Cache),是存贮数据的临时地方,一般读写性能较高。
2、添加Redis缓存流程
3、缓存更新策略
主动更新策略:
Cache Aside Pattern:由缓存的调用者,在更新数据库的同时更新缓存
操作缓存和数据库时有三个问题需要考虑:
1、删除缓存还是更新缓存?
◆更新缓存:每次更新数据库都更新缓存,无效写操作较多 √
◆删除缓存:更新数据库时让缓存失效,查询时再更新缓存 ×
2、如何保证缓存和数据库的操作同时成功或失败?
◆单体系统,将缓存与数据库操作放在一个事务
◆分布式系统,利用TCC等分布式事务方案
3、先操作缓存还是先操作数据库?
◆先删除缓存,在操作数据库 ×
◆先操作数据库,在操作缓存 √
原因:如下图所示,
●先删除缓存,在操作数据库:发生概率较高,更新缓存比更新数据库速度快很多
●操作数据库,再删除缓存:虽然也会有问题,但是,概率极低
(需要满足3个条件:1、两个线程并行执行。
2、线程1查询的时候,缓存恰好失效了。
3、在写入缓存的这个微秒级别,有一个线程突然更新数据库,然后删除缓存。)
总结
4、缓存穿透
缓存穿透:是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。
常见的解决方案有两种:
缓存空对象
◆优点:实现简单,维护方便
◆缺点:
●额外的内存消耗(设置TTL,这是短一点的)
●可能造成短期的不一致
布隆过滤器
一种数据结构,是由一串很长的二进制向量组成,可以将其看成一个二进制数组。既然是二进制,那么里面存放的不是0,就是1,但是初始默认值都是0
◆优点:占用内存极少,没有多余的key
◆缺点:
●实现复杂
●随着数据的增加,误判率会增加;还有无法判断数据一定存在;另外还有一个重要缺点,无法删除数据。
5、缓存雪崩
缓存雪崩:是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
解决方案:
◆给不同的Key的TTL添加随机值
◆利用Redis集群提高服务的可用性
◆给缓存业务添加降级限流策略
◆给业务添加多级缓存
6、缓存击穿
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
解决方案:
◆互斥锁 基于Redis的setnx
◆逻辑过期
两种方案对比如图所示
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
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、分布式原理
8.3.2、分布式锁的实现比较
8.3.3、基于Redis的分布式锁方案
1、获取锁:
互斥:确保只能有一个线程获取锁
非阻塞:尝试一次,成功返回true,失败返回false
SET lock xxx NX EX 10(秒) (添加锁,NX是互斥,EX是设置超时时间)
成功返回:OK 失败返回:nil
2、释放锁:
手动释放:DEL key (释放锁,删除即可)
超时释放:获取锁时添加一个超时时间
8.3.4、基于Redis的分布式锁流程
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);
}
}
如上图所示,当线程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
引入依赖
<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可重入锁原理`如下图:
获取锁的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分布式锁原理
总结:
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);
8.5、Redis分布式锁总结
1)不可重入Redis分布式锁:
◆原理:利用setnx的互斥性;利用ex避免死锁;释放锁时判断线程标示
◆缺陷:不可重入、无法重试、锁超时失效
2)可重入的Redis分布式锁:
◆原理:利用hash结构,记录线程标示和重入次数;利用watchDog延续锁时间;利用信号量控制锁重试等待
◆缺陷: redis宕机引起锁失效问题
3)Redisson的multiLock:
◆原理:多个独立的Redis节点,必须在所有节点都获取重入锁,才算获取锁成功
8.6、Redis优化秒杀-基于阻塞队列
业务流程逻辑优化
判断库存、一人一单的逻辑优化
1、把优惠券的库存信息和优惠券的订单信息(userId)缓存到redis中
2、一人一单的解决方式就是查看优惠券的订单信息,如果有,就表示已经买过。
3、优惠券信息使用String类型存储,优惠券的订单信息使用set集合
4、保存优惠券id、
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,所以这里就不过多的介绍了。以后会补充的。