前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >微信抢红包是怎么设计的?

微信抢红包是怎么设计的?

作者头像
用户5927304
发布2019-07-31 14:55:31
2.8K0
发布2019-07-31 14:55:31
举报
文章被收录于专栏:无敌码农无敌码农无敌码农

前言

高并发场景越来越多的出现在互联网业务上。 本文将重点介绍悲观锁、乐观锁、Redis分布式锁在高并发环境下的如何使用以及优缺点分析。

三种方式介绍

悲观锁

悲观锁,假定会发生并发冲突,在你开始改变此对象之前就将该对象给锁住,直到更改之后再释放锁。 其实,悲观锁是一种利用数据库内部机制提供的锁的方法,也就是对更新的数据进行加锁。这样在并发期间一旦有一个事务持有了数据库记录的锁,其他的线程将不能再对数据进行更新了,这就是悲观锁的实现方式。

悲观锁的实现方式: SQL + FOR UPDATE
1    <!--悲观锁-->
2    <select id="getRedPacketForUpdate" parameterType="int" resultType="com.demo.entity.RedPacket">
3        select id, user_id as userId, amount, send_date as sendDate, total, unit_amount as unitAmount,
4        stock, version, note
5        from t_red_packet
6        where
7        id = #{id} for update
8    </select>

根据加锁的粒度,当对主键查询进行加锁时,意味着将持有对数据库记录的行更新锁(因为这里使用主键查询,所以只会对行加锁。如果使用的是非主键查询,要考虑是否对全表加锁的问题,加锁后可能引发其他查询的阻塞〉,那就意味着在高并发的场景下,当一条事务持有了这个更新锁才能往下操作,其他的线程如果要更新这条记录,都需要等待,这样就不会出现超发现象引发的数据一致性问题了。

对于悲观锁来说,当一条线程抢占了资源后,其他的线程将得不到资源,那么这个时候, CPU 就会将这些得不到资源的线程挂起,挂起的线程也会消耗CPU 的资源,尤其是在高井发的请求中。

一旦线程l 提交了事务,那么锁就会被释放,这个时候被挂起的线程就会开始竞争资源,那么竞争到的线程就会被CPU 恢复到运行状态,继续运行。

于是频繁挂起,等待持有锁线程释放资源,一旦释放资源后,就开始抢夺,恢复线程,周而复始直至所有红包资源抢完。试想在高并发的过程中,使用悲观锁就会造成大量的线程被挂起和恢复,这将十分消耗资源,这就是为什么使用悲观锁性能不佳的原因。有些时候,我们也会把悲观锁称为独占锁,毕竟只有一个线程可以独占这个资源,或者称为阻塞锁,因为它会造成其他线程的阻塞。无论如何它都会造成并发能力的下降,从而导致CPU频繁切换线程上下文,造成性能低下。为了克服这个问题,提高并发的能力,避免大量线程因为阻塞导致CPU进行大量的上下文切换,程序设计大师们提出了乐观锁机制,乐观锁已经在企业中被大量应用了。

乐观锁

乐观锁是一种不会阻塞其他线程并发的机制,它不会使用数据库的锁进行实现,它的设计里面由于不阻塞其他线程,所以并不会引发线程频繁挂起和恢复,这样便能够提高井发能力,所以也有人把它称为非阻塞锁。

它的实现思路是,在更新时会判断其他线程在这之前有没有对数据进行修改,一般用版本号机制。 读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提 交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据 版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。

 1    <!--乐观锁-->
 2    <update id="decreaseRedPacketByVersion">
 3        update t_red_packet
 4        set
 5          stock = stock - 1,
 6          version = version + 1
 7        where
 8          id = #{id}
 9        and version = #{version}
10    </update>

但是,仅仅这样是不行的,在高并发的情景下,由于版本不一致的问题,存在大量红包争抢失败的问题。为了提高抢红包的成功率,我们加入重入机制。

重入机制
按时间戳重入(比如100ms时间内)
1        // 记录开始的时间
2        long start = System.currentTimeMillis();        // 无限循环,当抢包时间超过100ms或者成功时退出
3        while(true) {            // 循环当前时间
4            long end = System.currentTimeMillis();            // 如果抢红包的时间已经超过了100ms,就直接返回失败
5            if(end - start > 100) {                return FAILED;
6            }
7            ....
8
9        }
按次数重入(比如3次机会之内)
 1        // 允许用户重试抢三次红包
 2        for(int i = 0; i < 3; i++) {            // 获取红包信息, 注意version信息
 3            RedPacket redPacket = redPacketDao.getRedPacket(redPacketId);            // 如果当前的红包大于0
 4            if(redPacket.getStock() > 0) {                // 再次传入线程保存的version旧值给SQL判断,是否有其他线程修改过数据
 5                int update = redPacketDao.decreaseRedPacketByVersion(redPacketId, redPacket.getVersion());                // 如果没有数据更新,说明已经有其他线程修改过数据,则继续抢红包
 6                if(update == 0) {                    continue;
 7                }
 8            ....
 9            }
10            ...
11        }

这样就可以消除大量的请求失败,避免非重入的时候大量请求失败的场景。

Redis

我们知道当数据量非常大时,频繁的存取数据库,对于数据库的压力是非常大的。这时我们可以采用缓存技术,利用Redis的轻量级、便捷、快速的机制解决高并发问题。

通过流程图,我们看到整个流程与数据库交互只有两次,用户抢红包操作的过程其实都是在Redis中完成的,这显然提高了效率。

但是如何解决数据不一致带来的超发问题呢?

分布式锁

通俗的讲,分布式锁就是说,缓存中存入一个值(key-value),谁拿到这个值谁就可以执行代码。 在并发环境下,我们通过锁住当前的库存,来确保数据的一致性。知道信息存入缓存、库存-1之后,我们再重新释放锁。

为了防止死锁的发生,可以设置锁的过期时间来解决。

加锁
 1// 先判断缓存中是否存在值,没有返回true,并保存value,已经有值就不保存,返回false
 2        if(stringRedisTemplate.opsForValue().setIfAbsent(key, value)) {            return true;
 3        }
 4        String curentValue = stringRedisTemplate.opsForValue().get(key);        // 如果锁过期
 5        if(!StringUtils.isEmpty(curentValue) && Long.parseLong(curentValue) < System.currentTimeMillis()) {            // getAndSet设置新值,并返回旧值
 6            // 获取上一个锁的时间
 7            String oldValue = stringRedisTemplate.opsForValue().getAndSet(key, value);            if(!StringUtils.isEmpty(curentValue) && oldValue.equals(curentValue)) {                return true;
 8            }
 9        }       
10 return false;
解锁
1try {
2            String currentValue = stringRedisTemplate.opsForValue().get(key);            if(!StringUtils.isEmpty(currentValue) && currentValue.equals(value)) {
3                stringRedisTemplate.opsForValue().getOperations().delete(key);
4            }
5        } catch (Exception e) {
6            logger.error("RedisLock 解锁异常:" + e.getMessage());
7        }

总结

悲观锁使用了数据库的锁机制,可以消除数据不一致性,对于开发者而言会十分简单,但是,使用悲观锁后,数据库的性能有所下降,因为大量的线程都会被阻塞,而且需要有大量的恢复过程,需要进一步改变算法以提高系统的井发能力。

使用乐观锁有助于提高并发性能,但是由于版本号冲突,乐观锁导致多次请求服务失败的概率大大提高,而我们通过重入(按时间戳或者按次数限定)来提高成功的概率,这样对于乐观锁而言实现的方式就相对复杂了,其性能也会随着版本号冲突的概率提升而提升,并不稳定。使用乐观锁的弊端在于, 导致大量的SQL被执行,对于数据库的性能要求较高,容易引起数据库性能的瓶颈,而且对于开发还要考虑重入机制,从而导致开发难度加大。

使用Redis去实现高并发,消除了数据不一致性,并且在整个过程中尽量少的涉及数据库。但是这样使用的风险在于Redis的不稳定性,因为其事务和存储都存在不稳定的因素,所以更多的时候,建议使用独立Redis服务器做高并发业务,一方面可以提高Redis的性能,另一方面即使在高并发的场合,Redis服务器岩机也不会影响现有的其他业务,同时也可以使用备机等设备提高系统的高可用,保证网站的安全稳定。

以上讨论了3 种方式实现高并发业务技术的利弊,妥善规避风险,同时保证系统的高可用和高效是值得每一位开发者思考的问题。

本文来源:https://blog.csdn.net/qq_33764491/article/details/81083644

—————END—————

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-09-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 无敌码农 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 三种方式介绍
    • 悲观锁
      • 悲观锁的实现方式: SQL + FOR UPDATE
    • 乐观锁
      • 重入机制
        • 按时间戳重入(比如100ms时间内)
        • 按次数重入(比如3次机会之内)
      • Redis
        • 分布式锁
          • 加锁
            • 解锁
            • 总结
            相关产品与服务
            云数据库 Redis
            腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档