Redis事务和锁相关笔记

Redis事务和锁

Redis 事务

redis事务就是一个命令执行的队列,将一系列预定义命令包装成一个整体(一个队列)。当执行时,一次性 按照添加顺序依次执行,中间不会被打断或者干扰。

redisTemplate.multi();  //开启事务
redisTemplate.discard(); //取消事务,需要放在开启和执行中间才能取消。
redisTemplate.exec();   //执行事务

事务工作流:

若当前不是事务状态,则识别该指令是普通指令(执行)还是Mutil指令(进入事务状态);
若当前是事务状态,则识别该指令是普通指令(加入队列),还是exec指令(执行事务),还是discard指令(取消当前事务,放弃之前的指令)。

(图源自水印)

在这里插入图片描述

  • redis事务执行前先检查一遍命令并放到队列里。命令入队阶段出错(命令格式错误等),命令还没执行,数据没改变,不影响。

  • 在事务中加入命令时,出现discard,之前的命令取消执行,事务终止。

  • 使用exec()后执行事务出错时,已经执行完毕的命令对应的数据不会自动回滚,需要程序员自己在代码中实现回滚。未执行的命令也放弃。

redis如何实现回滚?

执行事务中出现错误,已经执行完毕的命令对应的数据不会自动回滚,需要程序员自己在代码中实现回滚。未执行的命令也放弃。

为什么redis是单线程的已经保证了原子性,但是还是需要加锁?
  • redis是单线程,本身的指令不会产生并发问题。指令都是one by one 执行的。
  • 但是具体到业务上,我们希望比如 **获取余额,判断,扣款,写回 **整个过程就必须是原子性的,否则就可能出错。这就需要分布式锁,来保证整个过程的原子性。
为什么分布式应用需要分布式锁而synchronized不起作用?
  • 在传统单体应用单机部署的情况下,可以使用Java并发相关的锁,如ReentrantLcok或synchronized进行互斥控制。因为单体状态下,多线程运行在同一个JVM里,多线程由于可以共享堆内存,因此可以简单的采取内存作为标记存储位置。
  • 而分布式环境下,系统被部署在多机器多JVM上同时提供服务,这使得原单机部署情况下的并发控制锁策略失效了,需要一种跨JVM的互斥机制来控制共享资源的访问,于是出现了分布式锁。因为进程之间甚至可能都不在同一台物理机上,因此需要将标记存储在一个所有进程都能看到的地方,可以用redis实现分布式锁。
什么时候用synchronized?什么时候用分布式锁?
  • 如果应用是前后端分离的单体应用,也就是说应用在同一台机器上运行,那么一般来说使用 synchronized 就可以满足同步的需求了。因为后端只有一个进程,只运行在一个JVM上。
  • 分布式锁通常用于协调在多个机器上运行的进程/线程之间的访问共享资源。当你的后端部署在多个JVM上,就需要用到分布式锁。

redis锁

1、Watch

对 key 添加监视锁,在执行exec前如果key发生了变化,终止事务执行
若设置一下重试次数就相当于乐观锁。

redisTemplate.watch(ARTICLE_VIEWS_COUNT); //添加对key的监视
redisTemplate.unwatch();  //取消对所有 key 的监视

redisTemplate.multi();  //开启事务
...... //业务操作
redisTemplate.exec();   //执行事务

应用场景:

  • 多个客户端有可能同时操作同一组数据,并且该数据一旦被操作修改后,将不适用于继续操作。即不能重复操作同一个数据的场景。
  • 在操作之前锁定要操作的数据,一旦发生变化,终止当前操作
  • 比如货物上架,一个货物上架了,再次上架就失败。
2、分布式锁setnx
加锁

使用 setnx 设置一个公共锁。发展历史:

  • setnx key value
    这种方式的缺点是容易产生死锁,因为有可能忘记解锁,或者解锁失败。
  • setnx key value;expire key seconds
    给锁增加了过期时间,避免出现死锁。但这两个命令不是原子的,第二步可能会失败,依然无法避免死锁问题。
  • set key value nx ex seconds
    将加锁、过期命令编排到一起,它们是原子操作了,可以避免死锁。

Redis 2.8 版本中作者加入了 set 指令的扩展参数,使得 setnx 和 expire 指令可以一起执行,彻底解决了分布式锁的乱象。
但是仍存在问题,就是A锁到期后自动解锁,但线程在A锁到期后才执行完任务并再次解锁,这个时候可能解锁的是别的线程的锁。

  • 所以只需要把到期时间设置得长一点,保证让线程能执行完任务,一般不会出现问题。
  • 另一种办法是:在加锁时就要给锁设置一个额外标识,进程获取锁后才能访问这个标识(即通过key获取锁,通过value标识)。当进程解锁的时候,要进行判断,当前锁的value是不是对应自己线程的value,对应才能释放。
  • 解锁时要先判断、再释放。这两部不是原子操作,就可能导致重复释放(一个在判断,一个在释放)。这就需要采用Lua 脚本,通过 Lua 脚本将两个命令编排在一起,而整个Lua脚本的执行过程是原子的。
解锁

解锁就是删除代表锁的那份数据。del key

获取分布式锁失败后怎么处理
  • 自旋直到成功获取

    while!redisLockUtils.getLock(COMMENT_USER_LIKE+COMMENT_LIKE_COUNT+LOCK, value);  //直到获取了锁,反回了true,才退出循环
    
  • 线程休眠一定时间,重新递归调用获取。

            Boolean getLock = redisLockUtils.getLock(COMMENT_USER_LIKE+COMMENT_LIKE_COUNT+LOCK, value);
    if (getLock) {
          ......  //业务代码
                            } 
    else {
          // 休眠重试获取锁
          Thread.sleep(RedisLockUtils.LOCK_REDIS_WAIT);
          this.saveCommentLike(commentId);
            }
    

Redis与Lua脚本

redis与lua实现分布式锁
  • lua脚本,通过redis的eval/evalsha命令,而这个命令是原子性的,保证了lua脚本原子性执行。

  • lua脚本原子性删除锁:

    -- KEYS和ARGV分别是以集合方式传入的参数,对应上文的key和value。
    -- 如果对应的value等于传入的value。
    if redis.call('get', KEYS[1]) == ARGV[1] 
        then 
    	-- 执行删除操作
            return redis.call('del', KEYS[1]) 
        else 
    	-- 不成功,返回0
            return 0 
    end
    
redia与lua实现限流

Redis作为缓存层

redis怎么作为缓存使用?

  • 在读取数据时,先从Redis中尝试获取,如果没有,则从数据库中读取,并将其存储到Redis中以备下次使用。
  • 在写入数据时,先将数据存储到Redis中,然后再将其写入到数据库中,以确保数据的一致性。
  • 数据更新时同步更新缓存:当数据库中的数据发生变化时,要及时更新Redis中的缓存数据,以保证缓存数据的一致性。
  • 限制缓存容量:为了防止Redis缓存中数据量过大而导致内存溢出,可以设置一个最大容量,并在达到容量限制时,将最近最少使用的数据删除。

把redis作为缓存层的基本操作流程?

 public void handleRequest(HttpServletRequest request, HttpServletResponse response) {
        // 1. 接收请求
        String param1 = request.getParameter("param1");
        String param2 = request.getParameter("param2");

        // 2. 验证请求
        if (param1 == null || param1.isEmpty() || param2 == null || param2.isEmpty()) {
            // 返回错误响应
            response.getWriter().write("Missing parameters");
            return;
        }

        // 3. 读取缓存
        String data = cache.get(param1, param2);
        
		// 4、判断缓存是否存在,不存在从数据库读取
		if(data == null){
			data = dao.get(param1, param2);
		}

        // 5. 处理数据
        String result = processRequest(data);
        // ...

        // 6. 写入缓存
        cache.put(param1, param2, result);

        // 7. 写入数据库
        dao.save(param1, param2, result);

        // 8. 返回响应
        response.getWriter().write(result);
    }

}

如果出现写入redis成功,而写入数据库失败导致数据不一致怎么办?

  • 可以使用Spring框架的声明式事务管理来实现,也就是在handleRequest()方法上添加@Transactional注解
  • 写入Redis成功,但写入数据库失败,事务管理器会回滚事务,撤销Redis中的写入操作,保证Redis和数据库中的数据一致性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值