今天是学习redis的第四天,我会继续学习,并持续更新🚚

redis实战篇

📘优惠卷秒杀

📌添加秒杀券

@RestController
@RequestMapping("/voucher")
public class VoucherController {

	@Resource
	private IVoucherService voucherService;

	/ **
	* 新增秒杀券
	* @param voucher 优惠券信息,包含秒杀信息
	* @return 优惠券id
	*/
	@PostMapping("seckill")
	public Result addSeckillVoucher(@RequestBody Voucher voucher) {
		voucherService.addSeckillVoucher(voucher);
		return Result.ok(voucher.getId());
	}
}	

在这里插入图片描述

📌实现秒杀下单

下单时需要判断两点:

  • 秒杀是否开始或结束,如果尚未开始或已经结束就无法下单

  • 库存是否充足,不足无法下单
    在这里插入图片描述

  • Controller层

    @RestController
    @RequestMapping("/voucher-order")
    public class VoucherOrderController {
    
        @Resource
        private IVoucherOrderService voucherOrderService;
        @PostMapping("seckill/{id}")
        public Result seckillVoucher(@PathVariable("id") Long voucherId) {
    
            return voucherOrderService.seckillVoucher(voucherId);
        }
    }
    
    
  • Service层

    @Service
    public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
    
        @Resource
        private ISeckillVoucherService seckillVoucherService;
        @Resource
        private RedisWorker redisWorker;
    
    
        @Override
        @Transactional
        public Result seckillVoucher(Long voucherId) {
            //1.查询优惠卷
            SeckillVoucher byId = seckillVoucherService.getById(voucherId);
            //2.判断秒杀是否开始
            if (byId.getBeginTime().isAfter(LocalDateTime.now())){
                return Result.fail("秒杀尚未开始");
            }
            //3.判断秒杀是否结束
            if (byId.getEndTime().isBefore(LocalDateTime.now())){
                return Result.fail("秒杀已结束");
            }
            //4.判断库存是否充足
            if (byId.getStock() < 1){
                return Result.fail("库存不足");
            }
            //5.扣减库存
            boolean success = seckillVoucherService.update()
                    //自定义SQL语句
                    .setSql("stock = stock - 1")
                    .eq("voucher_id", voucherId)
                    .update();
            if (!success){
                return Result.fail("库存不足");
            }
            //6.创建订单
            VoucherOrder voucherOrder = new VoucherOrder();
            //6.1.生成订单id
            long order = redisWorker.nextId("order");
            voucherOrder.setId(order);
            //6.2.用户id
            Long id = UserHolder.getUser().getId();
            voucherOrder.setUserId(id);
            //6.3.代金券id
            voucherOrder.setVoucherId(voucherId);
            save(voucherOrder);
            //7.返回订单结果
            return Result.ok(order);
        }
    }
    
    

📌超卖问题

在这里插入图片描述
超卖问题是典型的多线程安全问题,针对这一问题的常见解决方案就是加锁:
在这里插入图片描述
乐观锁实现的两种常见方法:

  1. 🔥版本号法
    为表添加一个version字段,每次查询时查询库存和版本号,减少库存时将版本号加1,同时加上where条件判断,如果version和一开始查询到的版本号相同再更改,否则不能修改
    在这里插入图片描述
  2. CAS法
    再版本号法的基础上将where条件判断改为判断stock库存是否变化
    在这里插入图片描述

📌乐观锁解决超卖

  • Service层

     		 //5.扣减库存
            boolean success = seckillVoucherService.update()
                    //自定义SQL语句
                    .setSql("stock = stock - 1")//set stock = stock - 1
                    .eq("voucher_id", voucherId).gt("stock", 0)// where id = ? and stock > 0
                    .update();
            if (!success){
                return Result.fail("库存不足");
            }
    

超卖这样的线程安全问题,解决方案有哪些?

  1. 悲观锁:添加同步锁,让线程串行执行

    • 优点:简单粗暴
    • 缺点:性能一般
  2. 乐观锁:不加锁,在更新时判断是否有其它线程在修改

    • 优点:性能好

    • 缺点:存在成功率低的问题

📌一人一单

因为这里是要用新增记录,所以要使用悲观锁synchronized

  • Service层

    @Service
    public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
    
        @Resource
        private ISeckillVoucherService seckillVoucherService;
        @Resource
        private RedisWorker redisWorker;
    
    
        @Override
    
        public Result seckillVoucher(Long voucherId) {
            //1.查询优惠卷
            SeckillVoucher byId = seckillVoucherService.getById(voucherId);
            //2.判断秒杀是否开始
            if (byId.getBeginTime().isAfter(LocalDateTime.now())) {
                return Result.fail("秒杀尚未开始");
            }
            //3.判断秒杀是否结束
            if (byId.getEndTime().isBefore(LocalDateTime.now())) {
                return Result.fail("秒杀已结束");
            }
            //4.判断库存是否充足
            if (byId.getStock() < 1) {
                return Result.fail("库存不足");
            }
            Long userid = UserHolder.getUser().getId();
            synchronized (userid.toString().intern()) {
                //获取当前对象的代理对象,因为Spring的事务是通过代理对象来实现的,不获取代理对象,就会导致事务失效
                IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
                return proxy.createVourcherOrder(voucherId);
            }
        }
    
        @Transactional
        public Result createVourcherOrder(Long voucherId) {
            //5.一人一单
            Long userid = UserHolder.getUser().getId();
    
            //5.1.查询订单
            int count = query().eq("user_id", userid).eq("voucher_id", voucherId).count();
            //5.2.判断订单是否存在
            if (count > 0) {
                //用户已经购买过
                return Result.fail("不能重复下单");
            }
    
            //6.扣减库存
            boolean success = seckillVoucherService.update()
                    //自定义SQL语句
                    .setSql("stock = stock - 1")//set stock = stock - 1
                    .eq("voucher_id", voucherId).gt("stock", 0)// where id = ? and stock > 0
                    .update();
            if (!success) {
                return Result.fail("库存不足");
            }
            //7.创建订单
            VoucherOrder voucherOrder = new VoucherOrder();
            //7.1.生成订单id
            long order = redisWorker.nextId("order");
            voucherOrder.setId(order);
            //7.2.用户id
            Long id = UserHolder.getUser().getId();
            voucherOrder.setUserId(id);
            //7.3.代金券id
            voucherOrder.setVoucherId(voucherId);
            save(voucherOrder);
            //8.返回订单结果
            return Result.ok(order);
        }
    
    }
    
  • 引入依赖

    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
    </dependency>
    
  • 启动方法添加自动代理注解

    @MapperScan("com.hmdp.mapper")
    @SpringBootApplication
    @EnableAspectJAutoProxy(exposeProxy = true)
    public class HmDianPingApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(HmDianPingApplication.class, args);
        }
    
    }
    
    

安全问题
当我们使用集群部署时,synchronized是基于jvm的,即它只会监视当前jvm的线程不能对其他jvm的线程做出操作,所以synchronized不能解决集群下的一人一单问题,这是就要使用分布式锁
在这里插入图片描述

📌分布式锁

分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。
在这里插入图片描述

🧠 对比项💙 MySQL💚 Redis(推荐)🧡 Zookeeper
🔒 互斥实现数据库锁机制SETNX + 过期时间临时有序节点
🟢 高可用✔️ 好✔️ 好✔️ 好
⚡ 性能⚠️ 一般🚀 很高(首选)⚠️ 一般
🛡️ 安全性断开连接自动释放⏰ 超时自动释放📌 会话断开自动删除节点
💡 使用建议简单场景可用🔥 高并发首选方案适合强一致 / 分布式协调

实现分布式锁时需要实现的两个基本方法:

  • 获取锁:
    • 互斥:确保只能有一个线程获取锁
    • 非阻塞:尝试一次,成功返回true,失败返回false
      添加锁,NX是互斥、EX是设置超时时间
      SET lock thread1 NX EX 10
      
  • 释放锁:
    • 手动释放
    • 超时释放:获取锁时添加一个超时时间
      释放锁,删除即可
      DEL key
      

在这里插入图片描述


基于redis的分布式锁

  • Utils层

    public interface ILock {
        /*
        * 尝试获取锁
        *timeoutSec:超时时间
        * return:是否获取成功
        * */
        boolean tryLock(long timeoutSec);
    
        /*
        * 释放锁
        * */
        void unLock();
    }
    
    
    public class SimpleRedisLock implements ILock{
    
        private String name;
        private StringRedisTemplate stringRedisTemplate;
        private final static String KEY_PREFIX = "lock:";
    
        public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
            this.name = name;
            this.stringRedisTemplate = stringRedisTemplate;
        }
    
        @Override
        public boolean tryLock(long timeoutSec) {
            //获取线程的id
            long thread_id = Thread.currentThread().getId();
            //获取锁
            Boolean success = stringRedisTemplate.opsForValue()
                    .setIfAbsent(KEY_PREFIX + name, thread_id + "", timeoutSec, TimeUnit.SECONDS);
            return Boolean.TRUE.equals(success);
        }
    
        @Override
        public void unLock() {
            //释放锁
            stringRedisTemplate.delete(KEY_PREFIX + name);
        }
    }
    
    
  • Service层

    //创建锁对象
    SimpleRedisLock simpleRedisLock = new SimpleRedisLock("order:" + userid, stringRedisTemplate);
    //获取锁
    boolean success = simpleRedisLock.tryLock(10);
    //判断获取锁成功
    if (!success) {
       return Result.fail("一人只允许下一单");
    }
    try {
        //获取当前对象的代理对象,因为Spring的事务是通过代理对象来实现的,不获取代理对象,就会导致事务失效
        IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
        return proxy.createVourcherOrder(voucherId);
    } finally {
        //释放锁
        simpleRedisLock.unLock();
    }
    

分布式锁误删问题
在这里插入图片描述
解决步骤

  1. 在获取锁时存入线程标示(可以用UUID表示)

  2. 在释放锁时先获取锁中的线程标示,判断是否与当前线程标示一致

    • 如果一致则释放锁

    • 如果不一致则不释放锁
      在这里插入图片描述

  • Utils层

    public class SimpleRedisLock implements ILock {
    
        private String name;
        private StringRedisTemplate stringRedisTemplate;
        private final static String KEY_PREFIX = "lock:";
        private final static String ID_PREFIX = UUID.randomUUID().toString();
    
        public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
            this.name = name;
            this.stringRedisTemplate = stringRedisTemplate;
        }
    
        @Override
        public boolean tryLock(long timeoutSec) {
            //获取线程的id
            String thread_id = ID_PREFIX + Thread.currentThread().getId();
            //获取锁
            Boolean success = stringRedisTemplate.opsForValue()
                    .setIfAbsent(KEY_PREFIX + name, thread_id , timeoutSec, TimeUnit.SECONDS);
            return Boolean.TRUE.equals(success);
        }
    
        @Override
        public void unLock() {
            //获取线程的id
            String thread_id = ID_PREFIX + Thread.currentThread().getId();
            //获取锁中标识
            String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
            //判断标识是否一致
            if (thread_id.equals(id)) {
                //释放锁
                stringRedisTemplate.delete(KEY_PREFIX + name);
            }
        }
    }
    

使用lua脚本解决分布式锁原子性问题
在这里插入图片描述
Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言,它的基本语法大家可以参考网站:lua脚本官方网站

这里重点介绍Redis提供的调用函数,语法如下:

-- 执行redis命令
redis.call('命令名称','key',‘其它参数',…)

例如,我们要执行set name jack,则脚本是这样:

 -- 执行set name jack
redis.call('set', 'name', 'jack')

例如,我们要先执行set name Rose,再执行get name,则脚本如下:

--先执行 set name jack
redis.call('set', 'name', 'jack')
-- 再执行 get name
local name = redis.call('get', 'name')
-- 返回
return name

例如,我们要执行redis.call(‘set’,‘name’,‘jack’)这个脚本,语法如下:

-- 调用脚本
EVAL "return redis.call('set', 'name', 'jack' )" 0
-- EVAL后面跟的是脚本内容,脚本内容后面跟的是脚本需要的key类型的变量
  • lua脚本

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

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

Redisson
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。
官方链接

  • 步骤

    1. 引入依赖
    <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.13.6</version>
        </dependency>
    
    
    1. 配置Redisson客户端
    @Configuration
    public class RedissonConfig {
        @Bean
        public RedissonClient redissonClient() {
            //配置
            Config config = new Config();
            config.useSingleServer().setAddress("redis://192.168.228.129:6379").setPassword("1234");
            //创建RedissonClient对象
            return Redisson.create(config);
        }
    }
    
    1. 使用Redisson分布式锁
    @Resource
    private RedissonClient redissonClient;
    
    @Test
    void testRedisson() throws InterruptedException {
    	//获取锁(可重入),指定锁的名称
    	RLock lock = redissonClientlgetLock("anyLock");
    	// 尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
    	boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS);
    	// 判断释放获取成功
    	if(isLock) {
    		try {
    			System.out.println("执行业务”);
    		}finally {
    			// 释放锁
    			lock.unlock();
    		}
    	}
    }
    

🧠Redisson可重入锁

  • 原理:
    🚀 加锁流程
  1. 判断锁是否存在
    • ❌ 不存在:

      • 直接加锁(SETNX)
      • 创建 Hash 结构
      • field = 线程标识
      • value = 1(重入次数)
      • ⏰ 设置过期时间(避免死锁)
    • ✅ 已存在:

      • 判断 field(线程标识) 是否是自己

        • 👍 是自己:

          • 说明是重入
          • value + 1
        • ❌ 不是自己:

          • 获取锁失败

🔓 释放锁流程

  1. 判断当前线程是否为锁持有者

    • ❌ 不是自己:

      • 提示:锁已被其他线程释放或无权限
    • 👍 是自己:

      • value - 1(减少重入次数)
  2. 判断是否真正释放锁:

    • 🔢 value > 0:

      • 仍有重入,不释放锁
    • 🧹 value = 0:

      • 删除锁(释放)
        在这里插入图片描述
        获取锁的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
        	-- 大于说明不能释放锁,重置有效期然后返回
        	redis.call('EXPIRE', key, releaseTime);
        	return nil;
        else -- 等于0说明可以释放锁,直接删除
        	redis.call('DEL', key);
        	return nil;
        end;
        

🧠Redisson锁重试和WacthDog机制
在这里插入图片描述
Redisson分布式锁原理:

  • 可重入:利用hash结构记录线程id和重入次数

  • 可重试:利用信号量和PubSub功能实现等待、唤醒,获取

锁失败的重试机制

  • 超时续约:利用watchDog,每隔一段时间(releaseTime/3),重置超时时间

🧠Redisson分布式锁主从一致性问题
在这里插入图片描述

1)不可重入Redis分布式锁:

  • 原理:利用setnx的互斥性;利用ex避免死锁;释放锁时判
    断线程标示

  • 缺陷:不可重入、无法重试、锁超时失效

2)可重入的Redis分布式锁:

  • 原理:利用hash结构,记录线程标示和重入次数;利用
    watchDog延续锁时间;利用信号量控制锁重试等待

  • 缺陷:redis宕机引起锁失效问题

3)Redisson的multiLock:

  • 原理:多个独立的Redis节点,必须在所有节点都获取重入
    锁,才算获取锁成功

  • 缺陷:运维成本高、实现复杂

🧾总结:今天在学习redis实战篇,还是太高估自己的自治力了,想着在51期间学习的,结果玩了3天,今天才开始学习,在学redisson的时候根本就听不懂完全被拷打,原理这一部分对于初学者还是太难了.今天快学完了优惠劵的秒杀了,争取明天能学完实战篇,明天继续,加油!🔥🔥🔥

更多推荐