全局ID生成器 , 是一种在分布式系统下用来生成全局唯一ID的工具 , 一般要满足下列特性
为了增加ID的安全性 , 我们可以不直接使用Redis自增的数值 , 而是拼接一些其他信息
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
/**
* 全局ID生成器
*/
@Component
public class RedisIDWorker {
@Resource
private StringRedisTemplate stringRedisTemplate;
// TODO 指定的初始时间戳
private static final Long BEGIN_TIMESTAMP = 946684800L;
// TODO 序列号的位数 , 一般不写死
private static final int COUNT_BITS = 32;
//一般这个key , 是根据谁调用谁传递过来的字符串 , 进行拼接
public Long nextId(String keyPrefix) {
// TODO 1.生成时间戳
LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long timeStamp = nowSecond - BEGIN_TIMESTAMP;
// TODO 2.生成序列号 , 使用redis的自增长机制 ,
// 不能使用同一个key作为自增长的key值 ,
// Redis单个key的最大自增数是2^64 , 我们设置的序列号最大是 2^32位
// 一直使用同一个key会序列号溢出的问题的 ,
// 可以在key的后边加一个当天的时间戳 , 一天使用一个key的值
// TODO 2.1 获取当前日期 , 精确到天
// 中间使用 : 分隔,在Redis中会进行分层 , 这个时候 , 就可以根据对应的前缀 , 来进行数据的统计
String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
//自增长 , 默认是从1开始自增的 , 每次增加 1
Long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
// TODO 3.拼接并返回 ,
// 使用的是移位运算 , 将时间戳左移32位
// 同时使用或运算 , 来进行拼接 ,
// 或运算的逻辑是 , 在位移后的位置上 , 全部都是0 ,
// 进行运算时 , 参加运算的两个对象,一个为1,其值为1 ,也就是count值是1就是1 , 是零就是0
// 或的运算速度要比加快
return timeStamp << COUNT_BITS | count ;
}
// TODO 使用下边的方法可以生成指定时间的时间戳
public static void main(String[] args) {
LocalDateTime time = LocalDateTime.of(2000, 1, 1, 0, 0, 0);
long l = time.toEpochSecond(ZoneOffset.UTC);
System.out.println(l);
}
}
@Resource
private RedisIDWorker redisIDWorker;
// TODO 创建一个线程池
private ExecutorService ex = Executors.newFixedThreadPool(500);
@Test
void testIdWorker() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(300);
Runnable task = () -> {
//一个线程循环执行100次 , 操作
for (int i = 0; i < 100; i++) {
Long id = redisIDWorker.nextId("order");
System.out.println("id =" + id);
}
latch.countDown();
};
long begin = System.currentTimeMillis();
for (int i = 0; i < 300; i++) {
ex.submit(task);
}
latch.await();
long end = System.currentTimeMillis();
System.out.println("time =" + (end -begin));
}
数据库表的创建 : 优惠券订单表
@Service
@Transactional
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private RedisIDWorker redisIDWorker;
@Override
public Result sekillVoucher(Long voucherId) {
// TODO 1.查询优惠券
SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
// TODO 2.判断秒杀是否开始
//开始时间在当前时间之后
if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
return Result.fail("秒杀未开始");
}
// TODO 3.判断秒杀是否已经结束
if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
return Result.fail("活动以结束");
}
// TODO 4.判断库存是否充足
Integer stock = seckillVoucher.getStock();
if (stock < 1){
return Result.fail("库存不足");
}
// TODO 5.扣减库存
boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).update();
if (!success){
return Result.fail("库存不足");
}
// TODO 6.创建订单
VoucherOrder order = new VoucherOrder();
// TODO 6.1订单id
Long orderId = redisIDWorker.nextId("order");
order.setId(orderId);
// TODO 6.2用户id
Long userId = UserHolder.getUser().getId();
order.setUserId(userId);
// TODO 6.3代金卷id
order.setVoucherId(seckillVoucher.getVoucherId());
// 写入数据库
save(order);
// TODO 7.返回订单id
return Result.ok(orderId);
}
}
但是直接和之前获取的库存数进行比较 , 会造成大量的失败 ,
我们再次进行改进 , 在进行操作时 , 添加判断条件 , 只要库存大于 0 , 就可以操作
// TODO 5.扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1") // TODO set stock = stock - 1
.eq("voucher_id", voucherId) // TODO where id = ?
//.eq("stock", seckillVoucher.getStock())
// TODO and stock = ? , 这么做会造成大量的失败
.gt("stock",0)
//TODO 使用gt , 添加的条件是 , stock > 0 , 这样刚好符合业务逻辑
.update();
使用乐观锁和悲观锁同时使用的形式 ,来优化超卖和一人一单问题 ,
将有关数据库的操作 , 抽取为一个方法 , 并且自定义悲观锁 , 以userid作为锁 , 确保多个相同id的线程中 , 只有一个线程能进行操作
@Transactional
public Result getResult(Long voucherId) {
// TODO 6. 一人一单
Long userId = UserHolder.getUser().getId();
// TODO 给对应的id加锁 , 确保同一个id只能有一个线程能操作 , 这样就没有线程安全问题了,
synchronized(userId.toString().intern()){
// TODO 6.1 查询订单
Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
// TODO 6.2 判断是否存在
if (count > 0){
return Result.fail("您已经购买过了");
}
// TODO 5.扣减库存 , 要在判断表中是否有相同id的数据之后进行操作 ,
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1") // TODO set stock = stock - 1
.eq("voucher_id", voucherId) // TODO where id = ?
.gt("stock",0) //TODO 使用gt , 添加的条件是 , stock > 0 , 这样刚好符合业务逻辑
.update();
if (!success){
return Result.fail("库存不足");
}
// TODO 6.创建订单
VoucherOrder order = new VoucherOrder();
// TODO 6.1订单id
Long orderId = redisIDWorker.nextId("order");
order.setId(orderId);
// TODO 6.2用户id
order.setUserId(userId);
// TODO 6.3代金卷id
order.setVoucherId(voucherId);
// 写入数据库
save(order);
// TODO 7.返回订单id
return Result.ok(orderId);
}
}
之前使用悲观锁和乐观锁同时并存的方式 , 可以解决一人一单的问题 ,
这是因为 , 锁的原理是在jvm中创建了一个锁监视器 , 通过第一个线程的访问 , 获取锁的名称, 进jvm , 这是 , 其他用户在进行访问的时候 , 就无法获取到锁 , 无法进行操作
如果是使用集群部署的形式 , 那么就会创建两个或多个jvm , 创建多个锁监视器 , 这个时候再进行访问的时候 , 就会导致一个锁只能锁住一个服务器的请求 , 访问其他服务器也可以同时进行操作
在redis中 , 使用set操作 , 可以同时指定多个属性 , 来完成互斥 和 设置失效时间的操作
set key thread1 NX (设置互斥) EX 10 (设置过期时间)
在jdk中 , 有两种获取锁失败的操作 : 失败返回 或 阻塞
阻塞会不断重试获取锁 , 会对内存造成很大的浪费 , 这里我们使用失败返回的操作 ,
尝试获取锁 :
@Override
public boolean tryLock(Long timeoutSec) {
// TODO 获取线程的标识
long threadId = Thread.currentThread().getId();
Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(aBoolean); //使用boolean中的方法 , 判断是否是true , 也可以解决自动拆箱造成的空指针异常情况
}
在原来的代码中 , 就不需要加 synchronize锁了 , 而是直接 尝试获取锁 , 如果获取锁失败 , 那么就直接输出错误信息 , 成功 , 再进行下一步业务操作
因为一个带锁线程发生阻塞, 导致锁超时失效 , 其他线程获取这个锁 , 导致 ,阻塞线程删除锁的时候 , 将其他线程获取的锁删除了 ,
public class SimpRedisLock implements ILock {
private StringRedisTemplate stringRedisTemplate;
private String name;
public SimpRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
this.stringRedisTemplate = stringRedisTemplate;
this.name = name;
}
private static final String KEY_PREFIX = "lock:";
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
@Override
public boolean tryLock(Long timeoutSec) {
// TODO 获取线程的标识
String threadId = ID_PREFIX + Thread.currentThread().getId();
Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(aBoolean);
}
@Override
public void unLock() {
// TODO 获取线程标识
String thread = ID_PREFIX + Thread.currentThread().getId();
//TODO 获取redis中的数据
String threadId = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
if (thread.equals(threadId)) {
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
}
JVM执行垃圾回收的时候 , 所有的代码是无法工作的 , 造成在释放锁的时候发生阻塞
-- 锁的key
-- local key = KEYS[1]
-- 当前线程标识
-- local threadId = ARGV[1]
-- 获取锁中的线程标识 get key
-- local id = redis.call('get',KEYS[1])
-- 比较线程标识与锁中标识是否一致
if (ARGV[1] == redis.call('get',KEYS[1])) then
-- 释放锁
redis.call('del',KEYS[1])
end
return 0
引入lua脚本 :
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static{
UNLOCK_SCRIPT = new DefaultRedisScript<>(); //初始化脚本 ,
//指定脚本的位置 , 一般是放在Resource包中 , 直接使用 ClassPathResource , 就是从Resource包中开始寻找
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);
}
编写释放锁代码 :
//释放锁
@Override
public void unLock() {
// 调用lua脚本
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
//这个操作 , 是指定是一个单元素的集合
Collections.singletonList(KEY_PREFIX + name),
ID_PREFIX + Thread.currentThread().getId());
}
Redisson是一个在Redis的基础上实现的java驻内存数据网络 , 它不仅提供了一系列的分布式的java常用对象 , 还提供了许多分布式服务 , 其中就包含了各种分布式锁的实现
官网地址 : https://redisson.org
GIthub地址 : https://github.com/redisson/redisson
快速入门 :
引入依赖 :
<dependency>
<groupId>org.redissongroupId>
<artifactId>redissonartifactId>
<version>3.16.8version>
dependency>
配置Redisson客户端
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient(){
// 配置类
Config config = new Config();
//添加redis地址 , 这里添加了单点的地址 , 也可以使用config.userClusterServers()添加集群地址
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
// 创建客户端
return Redisson.create(config);
}
}
使用Redisson的分布式锁 :
// TODO 获取锁(可重入),指定锁的名称
RLock lock = redissonClient.getLock("anyLock");
// TODO 尝试获取锁 , 参数分别是 : 获取锁的最大等待时间(期间会重试) , 锁自动释放时间 , 时间单位
// 如果最大等待时间结束 , 还是没有获取到锁 , 那么才会返回false
boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS);
// TODO 释放锁
lock.unlock();
使用hashMap的类型存储redis数据 , value中 设置一个线程标识作为key , 设置一个计数器作为value , 计数器从零开始 , 每次获取锁 , 计数器加一 , 每次释放锁 , 计数器减一
利用信号量和PubSub功能实现等待 , 唤醒 , 获取锁失败的重试机制
watchDog机制 : 每隔一段时间 (releaseTime / 3) , 重置超时时间
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x7rnoNLK-1650630427616)(https://gitee.com/aisichen/sichen/raw/master/img/image-20220417110836261.png)]
在集群的情况下 , 有一种连锁机制 , 就是在集群中的每一个节点中 , 都设置一个相同的锁 , 在获取锁的时候 , 只有全部节点都获取到了锁 , 才算获取成功 ,
创建多个节点 , 在配置中 配置多个配置 , 注入的的时候 , 也要注入所有的RedissonClient
RLock lock1 = redissonClient1.getLock("anyLock");
RLock lock2 = redissonClient2.getLock("anyLock");
RLock lock3 = redissonClient3.getLock("anyLock");
lock = redissonClient1.getMultiLock(lock1 , lock2 , lock3);
需求 :
编写 lua 脚本 :
-- 1.参数列表
-- 1.1优惠券id
local voucherId = ARGV[1]
-- 1.2用户id
local userId = ARGV[2]
-- 2.数据key
-- 2.1 库存key
local stockKey = 'seckill:stock:' .. voucherId
-- 2.2 订单key
local orderKey = 'seckill:order:' .. voucherId
-- 3.脚本业务
-- 3.1 判断库存是否充足 get stockKey
-- tonumber 将字符串转为数字
if (tonumber(redis.call('get', stockKey)) <= 0) then
-- 3.2 库存不足 , 返回1
return 1
end
-- 3.2 判断用户是否下单 SISMEMBER orderKey userId ,
-- sismember 是判断set集合中 , 是否包含 指定的元素 , 包含就返回1
if (redis.call('sismember' , orderKey , userId) == 1) then
-- 3.3说明是重复下单
return 2
end
-- 3.4 扣库存 , incrby stockKey -1
redis.call('incrby',stockKey ,-1)
--3.4 扣库存 , 下单 (保存用户)
redis.call('sadd' , orderKey , userId)
return 0
执行lua脚本 . 并执行业务
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.Result;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import com.hmdp.utils.RedisIDWorker;
import com.hmdp.utils.UserHolder;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
*
* 服务实现类
*
*
* @author 虎哥
* @since 2021-12-22
*/
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private RedisIDWorker redisIDWorker;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private RedissonClient redissonClient;
@Resource
// 创建阻塞队列 , 需要指定阻塞队列的长度
private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024 * 1024);
// 创建线程池 , 单线程的线程池
private static final ExecutorService EXECUTORSERVICE = Executors.newSingleThreadExecutor();
@PostConstruct
// TODO 被这个注解注释的方法 , 在当前类初始化完毕之后 , 开始执行
private void init() {
EXECUTORSERVICE.submit(new VoucherOrderHandler());
}
private class VoucherOrderHandler implements Runnable {
@Override
public void run() {
while (true) {
// 1.获取订单中的信息
try {
// take . 就是获取阻塞队列中的信息
VoucherOrder voucherOrder = orderTasks.take();
// 2.创建订单
createVoucherOrder(voucherOrder);
} catch (InterruptedException e) {
log.error("处理订单异常");
}
}
}
// 创建订单的方法 , 进行一人一单 , 校验 , 是为了进行最后的兜底
private void createVoucherOrder(VoucherOrder voucherOrder) {
// TODO 6. 一人一单
Long userId = voucherOrder.getUserId();
// TODO 获取锁(可重入),指定锁的名称
RLock lock = redissonClient.getLock("anyLock");
// TODO 尝试获取锁 , 参数分别是 : 获取锁的最大等待时间(期间会重试) , 所自动释放时间 , 时间单位
// 如果最大等待时间结束 , 还是没有获取到锁 , 那么才会返回false
boolean isLock = lock.tryLock();
// 判断获取锁是否成功
if (!isLock) {
// TODO 获取锁失败 , 直接返回失败或者重试
log.error("请勿重复提交");
return;
}
try {
// TODO 6.1 查询订单
Integer count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();
// TODO 6.2 判断是否存在
if (count > 0) {
log.error("您已经购买过了");
return ;
}
// TODO 5.扣减库存 , 要在判断表中是否有相同id的数据之后进行操作 ,
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1") // TODO set stock = stock - 1
.eq("voucher_id",voucherOrder.getVoucherId() ) // TODO where id = ?
.gt("stock", 0) //TODO 使用gt , 添加的条件是 , stock > 0 , 这样刚好符合业务逻辑
.update();
if (!success) {
log.error("库存不足");
return;
}
// 写入数据库
save(voucherOrder);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
}
// 创建lua脚本
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
// 初始化lua脚本
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);
}
@Override
public Result sekillVoucher(Long voucherId) throws InterruptedException {
Long userId = UserHolder.getUser().getId();
// TODO 1.执行lua脚本
Long aLong = stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.emptyList(),
voucherId.toString(),
userId.toString()
);
// TODO 将long类型的数据 , 拆为int类型的
int i = aLong.intValue();
// TODO 2.判断结果是否为0
if (i != 0) {
// TODO 2.1 不为0 , 表示没有购买资格
return Result.fail(i == 1 ? "库存不足" : "请勿重复下单");
}
// TODO 2.2 为0 , 有购买资格 , 把下单信息保存到阻塞队列
Long orderId = redisIDWorker.nextId("order");
// TODO 保存阻塞队列
VoucherOrder voucherOrder = new VoucherOrder();
// 2.3 优惠券id
voucherOrder.setVoucherId(voucherId);
// 订单id
voucherOrder.setId(orderId);
// 用户id
voucherOrder.setUserId(userId);
// 放入阻塞队列 ,
orderTasks.add(voucherOrder);
// TODO 返回订单id
return Result.ok(orderId);
}
}
消息队列 : (Message Queue)
在Redis中提供了三种不同的方式来实现消息队列
代码实现 :
// XGROUP 命令
// CREATE 创建
// stream.orders : 队列名称
// g1 : 消费者组名称
// 0 : ID
// MKSTREAM :队列不存在时自动创建队列
XGROUP CREATE stream.orders g1 0 MKSTREAM
代码中的实现 :
-- 3.6 发送消息到队列中 XADD stream.orders *:由redis自动生成 k1 v1 k2 v2 k3 v3
redis.call('xadd' , 'stream.orders' , '*' , 'userId' , userId , 'voucherId' , voucherId , 'id' , orderId)
代码中的实现 :
// 1.获取消息队列中的订单信息
List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
// TODO 指定消费者所在的组 和 名称
Consumer.from("g1", "c1"),
// TODO 设置读取时的参数 , empty : 空 , count(1) : 读取的消息个数
// TODO block() : 设置最大的等待时间 ,
StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
// TODO ReadOffset : 起始偏移量 , 枚举 , lastConsumed :最后消费
StreamOffset.create("stream.orders", ReadOffset.lastConsumed())
);
XGROUP CREATE stream.orders g1 0 MKSTREAM
-- 3.6 发送消息到队列中 XADD stream.orders * k1 v1
redis.call('xadd' , 'stream.orders' , '*' , 'userId' , userId , 'voucherId' , voucherId , 'id' , orderId)
private class VoucherOrderHandler implements Runnable {
@Override
public void run() {
while (true) {
try {
// 1.获取消息队列中的订单信息
List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
// TODO 指定消费者所在的组 和 名称
Consumer.from("g1", "c1"),
// TODO 设置读取时的参数 , empty : 空 , count(1) : 读取的消息个数
// TODO block() : 设置最大的等待时间 ,
StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
// TODO ReadOffset : 起始偏移量 , 枚举 , lastConsumed :最后消费
StreamOffset.create("stream.orders", ReadOffset.lastConsumed())
);
// 2.判断订单信息是否为空
if (list == null || list.isEmpty()){
// 如果为null , 说明没有消息 , 继续下一个循环
continue;
}
// 解析消息
MapRecord<String, Object, Object> record = list.get(0);
Map<Object, Object> value = record.getValue();
VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
// 3.创建订单
createVoucherOrder(voucherOrder);
// 4.确认消息 XACK
stringRedisTemplate.opsForStream().acknowledge("stream.orders","g1",record.getId());
} catch (Exception e) {
log.error("处理订单异常",e);
// 处理异常
handlePenddingList();
}
}
}