Redis事务和Redis乐观锁详解

1 Redis事务介绍

Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

  • Redis的事务是通过multi、exec、discard和watch这四个命令来完成的。
  • Redis的单个命令都是原子性的,所以这里需要确保事务性的对象是命令集合。
  • Redis将命令集合序列化并确保处于同一事务的命令集合连续且不被打断的执行。
  • Redis不支持回滚操作

1.1 命令介绍

  • multi:用于标记事务块的开始,Redis会将后续的命令逐个放入队列中,然后使用exec原子化执行这个命令队列 。
  • exec:执行命令队列
  • discard:清除命令队列
  • watch:在执行multi之前,先执行watch key1 [key2],可以监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断(可以利用Watch特性实现Redis乐观锁)
  • unwatch:取消 WATCH 命令对所有 key 的监视(如果在执行 WATCH 命令之后,EXEC 命令或DISCARD 命令先被执行了的话,那么就不需要再执行UNWATCH 了)。

1.2 事务流程

从输入multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入exec命令后,redis会将之前的命令队列中的命令依次执行。

  • 组队的过程中可以通过discard来放弃组队。

  • 如果组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消。

  • 如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。

Redis事务和Redis乐观锁详解_第1张图片

Redis事务和Redis乐观锁详解_第2张图片

上图说明:

  • 1.客户端1watch user:001 ,
  • 2.客户端1 开启事务multi
  • 3.客户端1,执行命令set user:001 lisi
  • 4.在客户端1,执行exec之前,客户端2,执行set user:001 xiaoming
  • 5.客户端1,执行exec出错,事务被打断

命令演示:

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set user:001 zhangsan
QUEUED
127.0.0.1:6379> set user:002 lisi
QUEUED
127.0.0.1:6379> get user:001
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK
3) "zhangsan"


127.0.0.1:6379> multi
OK
127.0.0.1:6379> set user:001 xiaoming
QUEUED
127.0.0.1:6379> set user:002 xiaozhang
QUEUED
127.0.0.1:6379> discard    # 使用discard命令取消队列
OK
127.0.0.1:6379> exec    # 执行exec报错
(error) ERR EXEC without MULTI

# watch 命令演示

# 客户端2,在客户端1执行exec之前,执行 set user:001 xiaoming,客户端1的事务被打断
127.0.0.1:6379> get user:001
"zhangsan"
127.0.0.1:6379> watch user:001
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set user:001 lisi
QUEUED
127.0.0.1:6379> exec   # 客户端1的事务被打断
(nil)
127.0.0.1:6379> get user:001
"xiaoming"


2 Redis实现乐观锁

2.1 乐观锁与悲观锁介绍

  • 悲观锁

    悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会被阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁表锁等,读锁写锁等,都是在做操作之前先上锁。

  • 乐观锁

    乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。

2.2 Redis乐观锁实现原理

Redis乐观锁的实现,是利用watch命令特性。数据进行提交更新的时候,对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。

Redis通过数据版本(Version)记录机制实现乐观锁,这是乐观锁最常用的一种实现方式。

客户端1 客户端2
age字段初始版本为1 age字段初始版本为1
watch age multi
set age 25 版本加一,目前数据库版本为2
set age 30 exec 当前操作版本为1,小于数据中版本,提交失败。

客户端2客户端1提交事务之前,对据库版本version进行更新一次,客户端1事务提交的时候对比版本号,要是此次版本号低于数据库当前版本号,就会提交失败。

2.3 Redis乐观锁秒杀案例

  1. 创建Spring boot项目引入以下依赖

    <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-data-redisartifactId>
            dependency>
            <dependency>
                <groupId>org.apache.commonsgroupId>
                <artifactId>commons-pool2artifactId>
            dependency>
    
  2. 配置文件

    spring:
      redis:
        host: 192.168.235.131
        port: 6379
        database: 0
        connect-timeout: 1800000
        password: 123456
        lettuce:
          pool:
            #连接池最大连接数(使用负值表示没有限制)
            max-active: 20
            #最大阻塞等待时间(负数表示没限制)
            max-wait: -1
            #连接池中的最大空闲连接
            max-idle: 8
            #连接池中的最小空闲连接
            min-idle: 0
    
  3. service 代码

    @Service
    @Slf4j
    public class OrderServiceImpl implements OrderService {
    
        @Autowired
        private StringRedisTemplate redisTemplate;
        //库存
        private final String STOCK_KEY="stock:num";
        //秒杀成功的用户
        private final String USER_KEY="success:user";
        /**
         * Redis乐观锁秒杀案例
         * @param userId  用户ID
         * @return
         */
        @Override
        public boolean secKill(String userId) {
            Object stockObj = redisTemplate.opsForValue().get(STOCK_KEY);
            if (stockObj==null){
                log.info("库存为空,秒杀还未开始!");
                return false;
            }
            int stockNum=Integer.parseInt(stockObj.toString());
            if (stockNum<=0){
                log.info("库存为0,秒杀已经结束!");
                return false;
            }
            //判断当前用户是否已经秒杀成功
            Boolean member = redisTemplate.opsForSet().isMember(USER_KEY, userId);
            if (member){
                log.info("您已经秒杀成功,不能重复参与!");
                return false;
            }
    
            List txList =redisTemplate.execute(new SessionCallback<List<Object>>() {
               @Override
               public List<Object> execute(RedisOperations operations) throws DataAccessException {
                   //监听库存
                    operations.watch(STOCK_KEY);
                   //开启事务
                   operations.multi();
                   //扣减库存
                   operations.opsForValue().decrement(STOCK_KEY);
                   //把秒杀成功的用户加入到set集合
                   operations.opsForSet().add(USER_KEY,userId);
                   //执行事务
                   List<Object> result=operations.exec();
                   return result;
               }
           });
            if (txList==null||txList.size()==0){
                log.info("用户:{},秒杀失败",userId);
                return false;
            }
            log.info("用户:{},秒杀成功",userId);
            return true;
        }
    }
    
  4. Controller代码

    /**
     * 秒杀
     * @return
     */
    @RequestMapping("secKill")
    public String secKill(){
        String userId= UUID.randomUUID().toString();
        boolean res = orderService.secKill(userId);
        if (res){
            return "秒杀成功";
        }else {
            return "秒杀失败";
        }
    }
    

使用linux上的ab进行并发测试:

ab -n 500 -c 100  http://192.168.1.171/order/secKill

你可能感兴趣的:(nosql,redis,redisson,乐观锁)