提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
id规律性太强,用户可以通过id猜到下单量
如果数据量大的时候,就得分表,自增ID只能在一个表中维护
在使用redis实现全局唯一时,为了考虑安全性,就不能1.2.3.4.这种增加
实现全局ID,redis不是唯一方案,主要考虑分布式系统中,如何做到数据共享的问题,这里采用redis也仅仅只是一个方案,未必适合于你的业务场景,因为redis是一个AP型的,数据一致性不是很强,你可以根据你的业务场景设置ID增长策略,但是不推荐,因为不便于后期维护;也可基于Zookeeper实现,Zookeeper是CP型的,一致性相对较高,当然分布式系统中,实现的方式有很多,不仅仅如此
@Component
public class RedisIdWorker {
private static final long BEGIN_TIMESTAMP = 1640995200L;
/*
序列号的位数
*/
private static final int COUNT_BITS =32;
private StringRedisTemplate stringRedisTemplate;
public long nextId(String keyPrefix) {
//1、生成时间戳
LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long timeStamp = nowSecond - BEGIN_TIMESTAMP;
/**
* key值不要创建唯一的keyPrefix
* 我们key的生成策略中,序列号:32bit才表示真正的数,只有2^32,单量大的话,很容易用完
* 可以在key后面追加日期,变为统计某天或者某月的单量
*/
//2、生成序列号
//2.1、获取日期,精确到天
String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
//2.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, 1, 0, 0);
long second = time.toEpochSecond(ZoneOffset.UTC);
System.out.println(second);
}
}
测试
@SpringBootTest
class HmDianPingApplicationTests {
@Resource
RedisIdWorker redisIDWorker;
private ExecutorService es = Executors.newFixedThreadPool(500);
@Test
public void ShowID() throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(300);
Runnable task = () -> {
for (int i = 0; i < 100; i++) {
long id = redisIDWorker.nextId("order");
System.out.println("id = " + id);
}
countDownLatch.countDown();
};
long startTime = System.currentTimeMillis();
for (int i = 0; i < 300; i++) {
es.submit(task);
}
countDownLatch.await();
long end = System.currentTimeMillis();
System.out.println("time = " + (end - startTime));
}
}
@Override
@Transactional
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("库存不足!");
}
//5、扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock=stock-1")
.eq("voucher_id", voucher).update();
if (!success) {
return Result.fail("库存不足");
}
//6、创建订单
VoucherOrder voucherOrder = new VoucherOrder();
//6.1创建订单Id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
//6.2用户id
Long userId = UserHolder.getUser().getId();
voucherOrder.setUserId(userId);
//6.3 代金券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
//7、返回订单
return Result.ok(orderId);
}
@Override
@Transactional
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("库存不足!");
}
//5、扣减库存(CAS,只有库存大于0时才扣减)
boolean success = seckillVoucherService.update()
.setSql("stock=stock-1")
.eq("voucher_id", voucher).gt("stock",0).update();
if (!success) {
return Result.fail("库存不足");
}
//6、创建订单
VoucherOrder voucherOrder = new VoucherOrder();
//6.1创建订单Id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
//6.2用户id
Long userId = UserHolder.getUser().getId();
voucherOrder.setUserId(userId);
//6.3 代金券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
//7、返回订单
return Result.ok(orderId);
}
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private RedisIdWorker redisIdWorker;
@Resource
private ISeckillVoucherService seckillVoucherService;
@Override
@Transactional
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("库存不足!");
}
//5、扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock=stock-1")
.eq("voucher_id", voucher).gt("stock",0).update();
if (!success) {
return Result.fail("库存不足");
}
Long userId = UserHolder.getUser().getId();
synchronized (userId.toString().intern()){ //要把事务锁在里面,不然事务会存在安全性问题
IVoucherOrderService proxy =(IVoucherOrderService) AopContext.currentProxy();//同一个service中的事务方法的调用,要用代理对象呀
return proxy.createVoucherOrder(voucherId);
}
}
@Transactional
public Result createVoucherOrder(Long voucherId) {
//5、一人一单
Long userId = UserHolder.getUser().getId();
//5.1查询订单
int count = query().eq("user_id",userId).eq("voucher_id",voucherId).count();
if (count>0){
return Result.fail("用户已经购买过一次");
}
//6、创建订单
VoucherOrder voucherOrder = new VoucherOrder();
//6.1创建订单Id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
//6.2用户id
voucherOrder.setUserId(userId);
//6.3 代金券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
//7、返回订单
return Result.ok(orderId);
}
}
为了实现一人一旦就得在下单前,判断这个人是否已经下过单了,但是一定要加锁,我们的下单业务的原子性和并发下的安全性,把之前的下单逻辑封装成了一个事务方法,并在事务外,包了一个锁,实现了事务的并发安全
其实分布式锁主要就是怎么解决分布式系统的数据共享问题(这也是我对分布式系统一个浅显的理解,仅仅代表个人观点),mysql实现分布式锁,我个人不是很推荐,利用mysql的事务机制,势必能够实现锁的,但是会给数据库造成额外的压力,估计你们项目经理也未必同意,个人推荐在redis和zookeeper中选一个,如果你是CP场景,推荐zookeeper,如果你是AP场景,推荐redis。总之,这取决于项目的集群特点,能实现分布式锁的中间件有很多,只要能做到进程间数据共享即可。
public interface ILock {
/**
* 尝试获取锁
* @param timeoutSec 锁持有的超时时间,过期后自动释放
* @return true代表获取锁成功;false代表获取锁失败
*/
boolean tryLock(long timeoutSec);
/**
* 释放锁
*/
void unlock();
}
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:";
@Override
public boolean tryLock(long timeoutSec) {
//获取线程标记
long threadId = Thread.currentThread().getId();
//获取锁
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.MILLISECONDS);
return Boolean.TRUE.equals(success); //一定要注意,凡是拆箱都可能存在null,要避免空指针异常
}
@Override
public void unlock() {
stringRedisTemplate.delete(KEY_PREFIX+name);
}
}
//创建锁对象
SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
boolean isLock = lock.tryLock(1200);
if (!isLock){
//获取锁失败
return Result.fail("不允许重复下单");
}
try {
IVoucherOrderService proxy =(IVoucherOrderService) AopContext.currentProxy();//同一个service中的事务方法的调用,要用代理对象呀
return proxy.createVoucherOrder(voucherId);
}finally {
//释放锁
lock.unlock();
}
分析下面一个场景:
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);
@Override
public boolean tryLock(long timeoutSec) {
/**
* 不要直接用线程ID区分不同线程
* 因为线程的ID是操作系统产生的
* 在分布式系统中,肯能不同系统中ID相同的,这样就无法在redis里区分不同的线程
* 我们是用hutool包中的UUID作为前缀解决上述问题
*/
//获取线程标记
String threadId = ID_PREFIX+Thread.currentThread().getId();
//获取锁
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId , timeoutSec, TimeUnit.MILLISECONDS);
return Boolean.TRUE.equals(success);
//一定要注意,凡是拆箱都可能存在null,要避免空指针异常
}
@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);
}
}
}
我们上面解决了锁的误删问题,通过线程标识解决锁的误删问题,但是这还是不完美的,因为判断线程标识和释放锁的操作不是原子的:
-- 比较线程标识与锁中的标识是否一致
if(redis.call('get',KEYS[1])==ARGV[1]) then
-- 释放锁 del key
return redis.call('del', KEYS[1])
end
return 0
这里lua需要idea装一个插件,百度即可
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 void unlock() {
stringRedisTemplate.execute(UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX+name),
ID_PREFIX+Thread.currentThread().getId());
}
至此redis分布式锁相对已经完善了,但是还有以下问题需要考虑:
1、引入依赖:
<dependency>
<groupId>org.redissongroupId>
<artifactId>redissonartifactId>
<version>3.13.6version>
dependency>
2、配置Redisson客户端
@Configuration
public class RedisConfig {
@Bean
public RedissonClient redissonClient() {
// 配置类
Config config = new Config();
// 添加redis地址,这里添加了单点的地址,也可以使用config.useClusterServers()添加集群地址
config.useSingleServer().setAddress("redis://192.168.150.101:6379").setPassword("123321");
// 创建客户端
return Redisson.create(config);
}
}
@Resource
private RedissonClient redissonClient;
//创建锁对象
RLock lock = redissonClient.getLock("lock:order:" + userId);
boolean isLock = lock.tryLock();
if (!isLock){
//获取锁失败
return Result.fail("不允许重复下单");
}
try {
IVoucherOrderService proxy =(IVoucherOrderService) AopContext.currentProxy();//同一个service中的事务方法的调用,要用代理对象呀
return proxy.createVoucherOrder(voucherId);
}finally {
//释放锁
lock.unlock();
}
这个和ReentrantLock原理很像
SET lock Thread1 NX EX 10
,因为之前key值不存在,所以第一次加锁成功SET lock Thread1 NX EX 10
时,因为key值lock已经存在了,所以就会加锁失败我们对比ReentrantLock实现可重入锁的原理,其实JUC中可重入锁的原理,就是在再次加锁时,同步状态state+1,来实现加锁的重入,释放锁的时候,state-1即可
Redisson也是这种方式实现锁重入的,那么在原来的基础上,就得增加一个值代表同步状态,现在就成了这样,一条数据既要有【唯一的锁名】、【线程标识】、【同步状态】,所以Redisson在加锁的时候为了解决可重入的操纵,它是基于redis的Hash实现的:
每次重入要value+1
相反地,释放锁value-1
private RFuture<Boolean> tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
if (leaseTime != -1) {//leaseTime 代表锁自动释放的时间,我们没有传的话,就会是默认值-1
return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
}
//加锁tryLockInnerAsync
RFuture<Boolean> ttlRemainingFuture = tryLockInnerAsync(waitTime,
commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e != null) {
return;
}
// lock acquired
if (ttlRemaining) {
scheduleExpirationRenewal(threadId);
}
});
return ttlRemainingFuture;
}
加锁代码:
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
return evalWriteAsync(getName(), LongCodec.INSTANCE, command,
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"return redis.call('pttl', KEYS[1]);",
Collections.singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}
可以看出Redisson为了保证加锁操作的原子性,也是通过Lua脚本实现的,不过它是写死在代码中,Lua脚本就是加锁的具体逻辑,分析如下:
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
"return nil;" +
"end; " +
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
"if (counter > 0) then " +
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
"return 0; " +
"else " +
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +//释放锁,发布一条消息通知
"return 1; " +
"end; " +
"return nil;",
Arrays.asList(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
}
Redisson锁在tryLock()获取锁时,可以传3个参数:
如果要实现锁重试机制,第一个参数一定要传的
// 2秒内,不断地尝试获取锁,获取到就立即返回true,如果时间到了,锁还未获取到,返回fasle
boolean isLock = lock.tryLock(2, TimeUnit.SECONDS);
锁尝试的源码:
@Override
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
long time = unit.toMillis(waitTime);
long current = System.currentTimeMillis();
long threadId = Thread.currentThread().getId();
Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId); //成功返回nil(null),失败返回ttl,锁的剩余有效期、
// lock acquired
if (ttl == null) {//成功返回true
return true;
}
time -= System.currentTimeMillis() - current;//time减去获取锁的消耗时间等于剩余时间
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
current = System.currentTimeMillis();
RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);//订阅释放锁的信号(之前讲释放锁的Lua脚本的时候,锁释放会发布一个信号)
if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {//等待释放锁的信号time时间
if (!subscribeFuture.cancel(false)) {
subscribeFuture.onComplete((res, e) -> {
if (e == null) {
unsubscribe(subscribeFuture, threadId);
}
});
}
acquireFailed(waitTime, unit, threadId);
return false;
}
try {
time -= System.currentTimeMillis() - current;//time 减去 等待释放锁的信号消耗色时间为还应该再尝试的时间
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
while (true) {//尝试获取锁
long currentTime = System.currentTimeMillis();
ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return true;
}
time -= System.currentTimeMillis() - currentTime;//每次都重新计算下次需要等大的时间
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
// waiting for message
currentTime = System.currentTimeMillis();
if (ttl >= 0 && ttl < time) {
subscribeFuture.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
subscribeFuture.getNow().getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
}
time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
}
} finally {
unsubscribe(subscribeFuture, threadId);
}
// return get(tryLockAsync(waitTime, leaseTime, unit));
}
锁重试的源码逻辑:
注意锁重试的两个重要关键点:1、订阅释放锁的信号量 (pubsub机机制)2、每次读重新计算等待时间
这也是锁重试的巧妙之处
这里推荐一篇原文Redisson的看门狗机制
大家都知道,如果负责储存这个分布式锁的Redisson节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。
如果我们在tryLock方法中传入leaseTime,leaseTime代表的就是锁自动释放的时间,如果没有传这个时间leaseTime,那它的默认值就是-1,其实在加锁的时候,没有传leaseTime,Redisson会使用看门狗机制(lockWatchdogTimeout = 30 * 1000),另外起一个定时任务,每过三分之一lockWatchdogTimeout 时间(10秒),就会重置超时释放时间。这样就防止了线程还在执行中,超时自动释放锁的问题,当线程真正的执行unLock方式释放锁时,便会取消看门狗
private void redissonDoc() throws InterruptedException {
//1. 普通的可重入锁
RLock lock = redissonClient.getLock("generalLock");
// 拿锁失败时会不停的重试
// 具有Watch Dog 自动延期机制 默认续30s 每隔30/3=10 秒续到30s
lock.lock();
// 尝试拿锁10s后停止重试,返回false
// 具有Watch Dog 自动延期机制 默认续30s
boolean res1 = lock.tryLock(10, TimeUnit.SECONDS);
// 拿锁失败时会不停的重试
// 没有Watch Dog ,10s后自动释放
lock.lock(10, TimeUnit.SECONDS);
// 尝试拿锁100s后停止重试,返回false
// 没有Watch Dog ,10s后自动释放
boolean res2 = lock.tryLock(100, 10, TimeUnit.SECONDS);
//2. 公平锁 保证 Redisson 客户端线程将以其请求的顺序获得锁
RLock fairLock = redissonClient.getFairLock("fairLock");
//3. 读写锁 没错与JDK中ReentrantLock的读写锁效果一样
RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("readWriteLock");
readWriteLock.readLock().lock();
readWriteLock.writeLock().lock();
}
只要传了leaseTime,看门狗就会失效
如果拿到分布式锁的节点宕机,且这个锁正好处于锁住的状态时,会出现锁死的状态,为了避免这种情况的发生,锁都会设置一个过期时间。这样也存在一个问题,加入一个线程拿到了锁设置了30s超时,在30s后这个线程还没有执行完毕,锁超时释放了,就会导致问题,Redisson给出了自己的答案,就是 watch dog 自动延期机制。
注意这个30秒不是 你自己传release设置的,而是你没有传release时,默认设置的超时释放时间
Redisson提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期,也就是说,如果一个拿到锁的线程一直没有完成逻辑,那么看门狗会帮助线程不断的延长锁超时时间,锁不会因为超时而被释放。
默认情况下,看门狗的续期时间是30s,也可以通过修改Config.lockWatchdogTimeout来另行指定。
另外Redisson 还提供了可以指定leaseTime参数的加锁方法来指定加锁的时间。超过这个时间后锁便自动解开了,不会延长锁的有效期。
watch dog 核心源码解读
// 直接使用lock无参数方法
public void lock() {
try {
lock(-1, null, false);
} catch (InterruptedException e) {
throw new IllegalStateException();
}
}
// 进入该方法 其中leaseTime = -1
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
long threadId = Thread.currentThread().getId();
Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return;
}
//...
}
// 进入 tryAcquire(-1, leaseTime, unit, threadId)
private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
return get(tryAcquireAsync(waitTime, leaseTime, unit, threadId));
}
// 进入 tryAcquireAsync
private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
if (leaseTime != -1) {
return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
}
//当leaseTime = -1 时 启动 watch dog机制
RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(waitTime,
commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
//执行完lua脚本后的回调
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e != null) {
return;
}
if (ttlRemaining == null) {
// watch dog
scheduleExpirationRenewal(threadId);
}
});
return ttlRemainingFuture;
}
scheduleExpirationRenewal 方法开启监控:
private void scheduleExpirationRenewal(long threadId) {
ExpirationEntry entry = new ExpirationEntry();
//将线程放入缓存中
ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
//第二次获得锁后 不会进行延期操作
if (oldEntry != null) {
oldEntry.addThreadId(threadId);
} else {
entry.addThreadId(threadId);
// 第一次获得锁 延期操作
renewExpiration();
}
}
// 进入 renewExpiration()
private void renewExpiration() {
ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
//如果缓存不存在,那不再锁续期
if (ee == null) {
return;
}
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ent == null) {
return;
}
Long threadId = ent.getFirstThreadId();
if (threadId == null) {
return;
}
//执行lua 进行续期
RFuture<Boolean> future = renewExpirationAsync(threadId);
future.onComplete((res, e) -> {
if (e != null) {
log.error("Can't update lock " + getName() + " expiration", e);
return;
}
if (res) {
//延期成功,继续循环操作
renewExpiration();
}
});
}
//每隔internalLockLeaseTime/3=10秒检查一次
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
//lua脚本 执行包装好的lua脚本进行key续期
protected RFuture<Boolean> renewExpirationAsync(long threadId) {
return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return 1; " +
"end; " +
"return 0;",
Collections.singletonList(getName()),
internalLockLeaseTime, getLockName(threadId));
}
关键结论
上述源码读过来我们可以记住几个关键情报:
// 锁释放
public void unlock() {
try {
get(unlockAsync(Thread.currentThread().getId()));
} catch (RedisException e) {
if (e.getCause() instanceof IllegalMonitorStateException) {
throw (IllegalMonitorStateException) e.getCause();
} else {
throw e;
}
}
}
// 进入 unlockAsync(Thread.currentThread().getId()) 方法 入参是当前线程的id
public RFuture<Void> unlockAsync(long threadId) {
RPromise<Void> result = new RedissonPromise<Void>();
//执行lua脚本 删除key
RFuture<Boolean> future = unlockInnerAsync(threadId);
//回调函数
future.onComplete((opStatus, e) -> {
// 无论执行lua脚本是否成功 执行cancelExpirationRenewal(threadId) 方法来删除EXPIRATION_RENEWAL_MAP中的缓存
cancelExpirationRenewal(threadId);
if (e != null) {
result.tryFailure(e);
return;
}
if (opStatus == null) {
IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
+ id + " thread-id: " + threadId);
result.tryFailure(cause);
return;
}
result.trySuccess(null);
});
return result;
}
// 此方法会停止 watch dog 机制
void cancelExpirationRenewal(Long threadId) {
ExpirationEntry task = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (task == null) {
return;
}
if (threadId != null) {
task.removeThreadId(threadId);
}
if (threadId == null || task.hasNoThreads()) {
Timeout timeout = task.getTimeout();
if (timeout != null) {
timeout.cancel();
}
EXPIRATION_RENEWAL_MAP.remove(getEntryName());
}
}
释放锁的操作中 有一步操作是从 EXPIRATION_RENEWAL_MAP 中获取 ExpirationEntry 对象,然后将其remove,结合watch dog中的续期前的判断:
EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ent == null) {
return;
}
可以得出结论:
如果释放锁操作本身异常了,watch dog 还会不停的续期吗?不会,因为无论释放锁操作是否成功,EXPIRATION_RENEWAL_MAP中的目标 ExpirationEntry 对象已经被移除了,watch dog 通过判断后就不会继续给锁续期了。
下面就是Redisson怎么实现锁重入、锁重试、锁超时的原理
如果redis是单节点的话,那么这台redis服务器宕机后,势必会影响整个基于redis实现分布式锁的系统,为了解决这一问题,redis必须使用集群模式,搭建主从节点(主节点负责写,从节点负责读),主节点数据向从节点同步数据的时候,势必会存在延迟
主从关系是导致一致性问题发生的原因,那干脆不要主从方式的redis集群了,所有的节点都是独立的节点,没有主从,那么获取锁的方式也就改变了:必须多有的redis节点都执行setnx成功,才算加锁成功
不管你是几个redis节点,获取锁时都要从各个节点获取,只有都成功了,才算获取锁成,这样不仅仅解决了主从一致性问题,要保证了可用性,如果你想要更高的可用性,也可以给多个redis的节点配置从节点:
如果一个主节点宕机了,分两种情况:
配置与redis节点相同的客户端数:分别连接各个redis服务器
1、集群搭建,省略了
2、配置各个redis节点的客户端
@Configuration
public class RedisConfig {
@Bean
public RedissonClient redissonClient() {
// 配置类
Config config = new Config();
// 添加redis地址,这里添加了单点的地址,也可以使用config.useClusterServers()添加集群地址
config.useSingleServer().setAddress("redis://192.168.150.101:6379").setPassword("123321");
// 创建客户端
return Redisson.create(config);
}@Bean
public RedissonClient redissonClient1() {
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.150.101:6380");
return Redisson.create(config);
}
@Bean
public RedissonClient redissonClient2() {
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.150.101:6381");
return Redisson.create(config);
}
}
3、注入客户端
@Resource
private RedissonClient redissonClient;
@Resource
private RedissonClient redissonClient1;
@Resource
private RedissonClient redissonClient2;
4、创建连锁并使用
//创建锁对象
RLock lock1= redissonClient.getLock("lock:order:" + userId);
RLock lock2 = redissonClient1.getLock("lock:order:" + userId);
RLock lock3 = redissonClient2.getLock("lock:order:" + userId);
//创建连锁
//这里用哪个client调用都可以,也可以使用
RLock lock = redissonClient.getMultiLock(lock1, lock2, lock3);
//获取锁
boolean isLock = lock.tryLock(2, TimeUnit.SECONDS);
multiLock锁的原理:多个独立的Redis节点,必须在所有节点都获取重入锁,才算获取锁成功
我们之前实现的秒杀,逻辑是这样子
思考一下,这样能做到“秒”吗?显然是不可以的,因为整个秒杀的逻辑都是同步的,而且在查询优惠券、查询订单、查库存、减库存、创建订单都需要连接数据库进行读或者写操作,而数据库的并发场景的是很差的,所以同步执行是不可取得,必须异步秒杀,传统得实现秒杀得方式是,基于mq消息队列和redis等缓存件实现的,本文我们仅仅基于redis实现一个简单的异步秒杀,仅仅提供思路
首先我们要解决的问题:
1、秒杀资格判断:
这里简单说两个方面,一个是库存的校验,另一个是一人一单的校验。
以前,我们校验库存是基于分布式锁从数据库取库存,然后校验库存,一人一单的验证也是如此,先从数据库取一下,判断是不是已经下过单了,这样并发度很低。
因此,我建议把库存的校验和一人一单的校验放到redis中,而redis的并发度远大于mysql,那怎么存放库存和下单人的信息?,库存采用redis的string类型,下单人和优惠券属于多对一的关系,而且为实现一人一单,我推荐用set类型。
把库存的校验和一人一单的校验写道Lua脚本中即可,利用Lua脚本的原子性,保证线程安全
2、异步
经过秒杀资格判断后,就需要创建订单,为了提高系统的吞吐量,必须要把资格校验和创建订单的逻辑实现异步。
我们知道创建订单在生产中是比较复杂的逻辑,而且多数场景都比较慢,秒杀活动要求响应的及时性,所以在经过资格判断后,如果通过,就创建预订单保存在消息队列中(推荐mq,没有mq也可以用redis实现,后面我会说仅有redis怎么实现),直接可以反馈用户秒杀成功,后续有监控线程取消息队列的预订单数据,创建订单。。。
以上就是一个简单的秒杀系统的设计思路,逻辑流程如下图所示
代码逻辑流程
1、首先实现Lua脚本,脚本是用于校验秒杀资格的,这里涉及到库存校验和一人一旦的校验
--1 参数列表
-- 1.1优惠券Id
local voucherId = ARGC[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
if (tonumber(redis.call('get', stockKey))) then
-- 库存不足 返回1
return 1
end
-- 3.2 判断用户是否下单 SISMEMBER orderKey userId
if (redis.call('sismember', orderKey, userId)) 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)
-- 返回0,表示秒杀成功
return 0
基于阻塞队列实现秒杀(不推荐)
private BlockingQueue<VoucherOrder> orderTask = new ArrayBlockingQueue<>(1024 * 1024);
private static ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();
@PostConstruct
private void init() {
SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
}
private class VoucherOrderHandler implements Runnable {
@Override
public void run() {
try {
//获取队列的订单信息
VoucherOrder voucherOrder = orderTask.take();
//创建订单
handleVouvherOrder(voucherOrder);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
主要在于LPOP和RPOP都是取出消息后,都把redis中的消息删除了,如果消息处理异常,就没办法重试
redis的发布订阅不支持持久化,不适合对可靠性要求高的场景
Stream 消息是不会删除的,所以可以回溯
--1 参数列表
-- 1.1优惠券Id
local voucherId = ARGC[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))) then
-- 库存不足 返回1
return 1
end
-- 3.2 判断用户是否下单 SISMEMBER orderKey userId
if (redis.call('sismember', orderKey, userId)) 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)
-- 3.6 发布消息
redis.call('xadd','stream_orders','*','userId',userId,'voucherId',voucherId,'Id',orderId)
-- 返回0,表示秒杀成功
return 0
消费者:
while(true){
try{
//1.获取消息
List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
Consumer.from("group1", "consumer1"),
StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
StreamOffset.create("message.queue", ReadOffset.lastConsumed())
//2.判断消息是否存在
if(list == null || list.isEmpty()){
continue;
}
//3.解析消息
Map map = list.get(0).getValue();
ENtity entity = BeanUtil.fillBeanWithMap(map,new Entity(),true);
//4.处理消息
handleMsg(entity);
//5.对消息进行确认
ack(queue_name,group,id)
}catch(Exception e){
while(true){
try{
//1.获取消息
List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
Consumer.from("group1", "consumer1"),
StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
StreamOffset.create("message.queue", ReadOffset.from("0"))
//2.判断消息是否存在
if(list == null || list.isEmpty()){
break;
}
//3.解析消息
Map map = list.get(0).getValue();
ENtity entity = BeanUtil.fillBeanWithMap(map,new Entity(),true);
//4.处理消息
handleMsg(entity);
//5.对消息进行确认
ack(queue_name,group,id)
}catch(e){
log.debug("消息处理异常");
}
}
}
}
}