• Java Web(1)高并发业务


      互联网无时无刻不面对着高并发问题,例如商品秒杀、微信群抢红包、大麦网抢演唱会门票等。

      当一个Web系统,在一秒内收到数以万计甚至更多的请求时,系统的优化和稳定是至关重要的。

      互联网的开发包括Java后台、NoSQL、数据库、限流、CDN、负载均衡等。

      一、互联系统应用架构基础分析

      

      防火墙的功能是防止互联网上的病毒和其他攻击,正常的请求通过防火墙后,最先到达的就是负载均衡器。

      负载均衡器的主要功能:

    • 对业务请求做初步的分析,决定分不分发请求到Web服务器,常见的分发软件比如Nginx和Apache等反向代理服务器,它们在关卡处可以通过配置禁止一些无效的请求,比如封禁经常作弊的IP地址,也可以使用Lua、C语言联合 NoSQL 缓存技术进行业务分析,这样就可以初步分析业务,决定是否需要分发到服务器
    • 提供路由算法,它可以提供一些负载均衡算法,根据各个服务器的负载能力进行合理分发,每一个Web服务器得到比较均衡的请求,从而降低单个服务器的压力,提高系统的响应能力。
    • 限流,对于一些高并发时刻,如双十一,需要通过限流来处理,因为可能某个时刻通过上述的算法让有效请求过多到达服务器,使得一些Web服务器或者数据库服务器产生宕机。当某台机器宕机后,会使得其他服务器承受更大的请求量,这样就容易产生多台服务器连续宕机的可能性,持续下去就会引发服务器雪崩。因此,在这种情况下,负载均衡器有限流的算法,对于请求过多的时刻,可以告知用户系统繁忙,稍后再试,从而保证系统持续可用。

      为了应对复杂的业务,可以把业务存储在 NoSQL 上,通过C语言或者Lua语言进行逻辑判断,它们的性能比Web服务器判断的性能要快速得多,从而降低Web服务器的压力,提高互联网系统的响应速度。

      

      二、应对无效请求

      

      在负载均衡器转发给Web服务器之前,使用C语言和Redis进行判断是否是无效请求。对于黄牛组织,可以考虑僵尸账号排除法进行应对。

      

      三、系统设计

      高并发系统往往需要分布式的系统分摊请求的压力,要尽量根据 Web 服务器的性能进行均衡分配请求。  

      划分系统可以按照业务划分,即水平划分。也可以不按照业务分,即垂直划分。

      按照业务划分可以提高开发效率以及更方便地设计数据库。但是,还要通过RPC(Remote Procedure Call Protocol)远程过程调用协议处理这些信息,例如Dubbo、Thrift和Hessian等。

      

      四、数据库设计

      为了得到高性能,可以使用分表或分库技术,从而提高系统的响应能力。

      分表是指在一个数据库内本来一张表可以保存的数据,设计成多张表去保存。例如,将每一年的交易记录分别分成交易表,而不是只有一张交易表来记录所有的交易记录。

      分库是把表数据分配在不同的数据库中,分库首先需要一个路由算法确定数据在哪个数据库上,然后才能进行查询,这里可以把用户和对应业务的数据库的信息缓存到Redis中,这样路由算法就可以通过Redis从读取的数据来决定使用哪个数据库进行查询了。

      另外,还可以考虑SQL优化,建立索引等优化,提高数据库的性能。

      五、动静分离技术

      

      对于互联网而言,大部分数据都是静态数据,只有少数使用动态数据,动态数据的数据包很小,不会造成网络瓶颈,而静态的数据则不一样,静态数据包含图片、CSS、JavaScript 和视频等互联网的应用,尤其是图片和视频占据的流量很大,如果都从动态服务器(比如Tomcat、WildFly和WebLogic等)获取,那么动态服务器的带宽压力会很大,这个时候应该考虑动静分离技术。

      可以使用静态HTTP服务器,例如Apache,将静态数据分离到静态HTTP服务器上,这样图片、HTML、脚本等资源都可以从静态服务器上获取,尽量使用 Cookie 等技术,让客户端缓存能够缓存数据,避免多次请求,降低服务器的压力。

      企业可以还可以使用高级的动静分离技术,例如CDN(Content Delivery Network,即内容分发网络),它允许企业将自己的静态数据缓存到网络 CDN 的节点中,对于用户的请求,直接通过CDN算法去指定的CDN节点去响应请求。

      

      六、锁和高并发

      无论区分有效请求和无效请求、水平划分还是垂直划分、动静分离技术,还是数据库分表、分库技术,都无法避免动态数据,而动态数据的请求最终也会落在一台 Web 服务器上。

      例如,发放一个总额为 20 万元的红包,拆分成 2 万个金额为 10 元 的小红包,供给网站的 3 万个会员在线抢夺,这就是一个典型的高并发的场景。

      由于会出现多个线程同时发起请求,由于线程每一步完成的顺序不一样,这样会导致数据的一致性问题。

      为了保证数据一致性,可以使用加锁的方式,但是加锁会影响并发,从而影响系统的性能,而不加锁就难以保证数据的一致性,这就是锁和高并发的矛盾。

      

      为了解决锁和高并发的矛盾,大部分企业提出了悲观锁和乐观锁的概念,

    • 对于数据库而言,如果在短时间内需要执行大量SQL,对于服务器的压力可想而知,需要优化数据库的表设计、索引、SQL语句等。
    • 还可以使用 Redis 事务和 Lua 语言所提供的原子性来取代现有的数据库技术,从而提高数据的存储响应,以应对高并发场景,但是严格来说也属于乐观锁。

      0.T_RED_PACKET为红包表,T_USER_RED_PACKET为用户抢红包表

      (0)未加锁情况下,并发导致了数据的不一致

        <!-- 查询红包具体信息 -->
        <select id="getRedPacket" parameterType="long"
            resultType="com.ssm.chapter22.pojo.RedPacket">
            select id, user_id as userId, amount, send_date as
            sendDate, total,
            unit_amount as unitAmount, stock, version, note from
            T_RED_PACKET
            where id = #{id}
        </select>

      业务逻辑:

        @Override
        @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
        public int grapRedPacket(Long redPacketId, Long userId) {
            // 获取红包信息
            // RedPacket redPacket = redPacketDao.getRedPacket(redPacketId);
            // 悲观锁
            RedPacket redPacket = redPacketDao.getRedPacketForUpdate(redPacketId);
            // 当前小红包库存大于0
            if (redPacket.getStock() > 0) {
                redPacketDao.decreaseRedPacket(redPacketId);
                // 生成抢红包信息
                UserRedPacket userRedPacket = new UserRedPacket();
                userRedPacket.setRedPacketId(redPacketId);
                userRedPacket.setUserId(userId);
                userRedPacket.setAmount(redPacket.getUnitAmount());
                userRedPacket.setNote("抢红包 " + redPacketId);
                // 插入抢红包信息
                int result = userRedPacketDao.grapRedPacket(userRedPacket);
                return result;
            }
            // 失败返回
            return FAILED;
        }

      

      1.使用数据库的悲观锁和乐观锁进行设计

      (1)悲观锁

      悲观锁是一种利用数据库内部机制提供的锁的方法,也就是对更新的数据加锁,这样在并发期间一旦有一个事务持有了数据库记录的锁,其他的线程将不能再对数据进行更新了。

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

      但是,悲观锁会导致系统性能下降。对于悲观锁来说,当一条线程抢占了资源后,其他的线程将得不到资源,那么这个时候,CPU 就会将这些得不到资源的线程挂起,挂起的线程也会消耗 CPU 的资源。由于高并发环境下的频繁挂起线程和恢复线程,导致CPU频繁切换线程上下文,从而使CPU资源得到了极大的消耗,造成了性能不佳的问题。悲观锁也称独占锁。

      业务逻辑和不加锁时一致。

        <!-- 查询红包具体信息 -->
        <select id="getRedPacketForUpdate" parameterType="long"
            resultType="com.ssm.chapter22.pojo.RedPacket">
            select id, user_id as userId, amount, send_date as
            sendDate, total,
            unit_amount as unitAmount, stock, version, note
            from
            T_RED_PACKET where id = #{id} for update
        </select>

      (2)乐观锁

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

      

      CAS原理并不排斥并发,也不独占资源,只是在线程开始阶段就读入线程共享数据,保存为旧值。当处理完逻辑,需要更新数据的时候,会进行一次比较,即比较各个线程当前共享的数据是否和旧值保持一致,如果一致,就开始更新数据,如果不一致,就认为该数据已经被其他线程修改了,那么就不再更新数据,可以考虑重试或者放弃。有时候可以重试,这样就是一个可重入锁。

      CAS原理存在ABA问题,ABA问题是因为业务逻辑存在回退的可能性。如果加入一个非业务逻辑的属性,比如在一个数据中加入版本号,每次修改变量的数据时,强制版本号只能递增,而不会回退,即使是其他业务数据回退,它也会递增,那么就解决了ABA问题。

      对于查询来说,没有“for update”语句,避免了锁的发生,就不会造成线程阻塞。然后,增加了对版本号的判断,其次每次扣减都会对版本号加1,这样就可以避免ABA问题了。

        <!-- 通过版本号扣减抢红包 每更新一次,版本增1, 其次增加对版本号的判断 -->
        <update id="decreaseRedPacketForVersion">
            update T_RED_PACKET
            set stock = stock - 1,
            version = version + 1
            where id = #{id}
            and version = #{version}
        </update>

      在Service中使用乐观锁,无重入的代码:其中,redPacket先获取到了红包的记录,其redPacket.getVersion()表示的就是version版本值,在执行更新数据库方法时,将redPacket.getVersion()传入作为version变量,由于在判断时增加了“and version = #{version}”语句,因此,如果不相等,就不执行update SQL语句,因此update返回值就为0,表明版本号发生了变化。

        // 乐观锁,无重入
        @Override
        @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
        public int grapRedPacketForVersion(Long redPacketId, Long userId) {
            // 获取红包信息,注意version值
            RedPacket redPacket = redPacketDao.getRedPacket(redPacketId);
            // 当前小红包库存大于0
            if (redPacket.getStock() > 0) {
                // 再次传入线程保存的version旧值给SQL判断,是否有其他线程修改过数据
                int update = redPacketDao.decreaseRedPacketForVersion(redPacketId, redPacket.getVersion());
                // 如果没有数据更新,则说明其他线程已经修改过数据,本次抢红包失败
                if (update == 0) {
                    return FAILED;
                }
                // 生成抢红包信息
                UserRedPacket userRedPacket = new UserRedPacket();
                userRedPacket.setRedPacketId(redPacketId);
                userRedPacket.setUserId(userId);
                userRedPacket.setAmount(redPacket.getUnitAmount());
                userRedPacket.setNote("抢红包 " + redPacketId);
                // 插入抢红包信息
                int result = userRedPacketDao.grapRedPacket(userRedPacket);
                return result;
            }
            // 失败返回
            return FAILED;
        }

      在实际测试的情况下,经过3万次的抢夺,原来的2万个红包中,由于版本号version不一致导致了还有8千多个没有被抢到,也就是说,抢红包失败的记录太大了。

      (3)乐观锁重入机制

      为了克服这个问题,提高成功率,还会考虑使用锁重入机制。也就是一旦因为版本原因没有抢到红包,则重新尝试抢红包,但是过多的重入会造成大量的SQL执行,有两种方式:

    • 按时间戳重入,也就是在一定的时间内(例如:当前时间+100毫秒),不成功的会循环到成功为止,直至超过时间戳,不成功才会退出,返回失败。
    • 按次数重入,比如限定3次,如果超过3次尝试后还失败,那么就判定此次失败

      按时间戳重入:有时候时间戳并不是那么稳定,也会随着系统的空闲或者繁忙导致重试次数不一。

        // 乐观锁,按时间戳重入
        @Override
        @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
        public int grapRedPacketForVersion(Long redPacketId, Long userId) {
            // 记录开始时间
            long start = System.currentTimeMillis();
            // 无线循环,直到成功或者满100毫秒退出
            while (true) {
                // 获取循环当前时间
                long end = System.currentTimeMillis();
                // 如果超过了100毫秒就结束尝试
                if (end - start > 100) {
                    return FAILED;
                }
    ... 和乐观锁部分一样
    }

      按次数重入:限制重试次数,这样就能避免过多的重试导致过多的SQL被执行,从而保证数据库的性能。

        // 乐观锁,按重试次数重入
        @Override
        @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
        public int grapRedPacketForVersion(Long redPacketId, Long userId) {
            for (int i = 0; i < 3; i++) {
                // 获取红包信息,主要是version值
                RedPacket redPacket = redPacketDao.getRedPacket(redPacketId);
                // 当前小红包库存大于0
                if (redPacket.getStock() > 0) {
                    // 再次传入线程保存的version旧值给SQL判断,是否有其他线程修改过数据
                    int update = redPacketDao.decreaseRedPacketForVersion(redPacketId, redPacket.getVersion());
                    // 如果没有数据更新,则说明其他线程已经修改过数据,则重新抢夺
                    if (update == 0) {
                        continue;
                    }
    ...
    }

      

      2.使用Redis进行设计

      数据库最终会将数据保存到磁盘中,而Redis使用的是内存,内存的速度比磁盘速度快得多。

      Redis的Lua语言是原子性的,且功能更为强大,因此优先选择Lua语言实现抢红包业务。

      Redis并非是一个长久存储数据的地方,它存储的数据是非严格和安全的环境,更多的时候只是为了提供更为快速的缓存,因此当红包金额为0或者红包超时的时候,将红包数据保存到数据库中,这样才能够保证数据的安全性和严格性。  

      

      (0)表设计

      使用下面的Redis命令在Redis中初始化了一个编号为5的大红包,其中库存为2万个,每个10元

    127.0.0.1:6379> hset red_packet_5 stock 20000
    (integer) 1
    127.0.0.1:6379> hset red_packet_5 unit_amount 10
    (integer) 1

      在数据库中通过下面的SQL语句创建用户抢红包信息表:

    create table T_USER_RED_PACKET 
    (
       id                   int(12)                        not null auto_increment,
       red_packet_id        int(12)                        not null,
       user_id              int(12)                        not null,
       amount               decimal(16,2)                  not null,
       grab_time            timestamp                      not null,
       note                 varchar(256)                   null,
       primary key clustered (id)
    );

      (1)Lua脚本设计

        // Lua脚本
        String script = "local listKey = 'red_packet_list_'..KEYS[1] 
    "   // 被抢红包列表 key
                 + "local redPacket = 'red_packet_'..KEYS[1] 
    "    // 当前被抢红包 key
                    + "local stock = tonumber(redis.call('hget', redPacket, 'stock')) 
    " // 读取当前红包库存
                    + "if stock <= 0 then return 0 end 
    "  // 没有库存,返回0
                    + "stock = stock -1 
    "           // 库存减1
                    + "redis.call('hset', redPacket, 'stock', tostring(stock)) 
    "  // 保存当前库存
                    + "redis.call('rpush', listKey, ARGV[1]) 
    "     // 往Redis链表中加入当前红包信息
                    + "if stock == 0 then return 2 end 
    "     // 如果是最后一个红包,则返回2,表示抢红包已经结束,需要将Redis列表中的数据保存到数据库中
                    + "return 1 
    ";    // 如果并非最后一个红包,则返回1,表示抢红包成功。

      当返回为2的时候,说明红包已经没有库存,会触发数据库对链表数据的保存,这是一个大数据量的保存,因为有20000条记录。为了不影响最后一次抢红包的响应,在实际的操作中往往会考虑使用 JMS 消息发送到别的服务器进行操作。这里只是创建了一条新的线程去运行保存 Redis 链表数据到数据库的程序。

      保存 Redis 抢红包信息到数据库的服务类:

    package com.ssm.chapter22.service.impl;
    ...
    @Service
    public class RedisRedPacketServiceImpl implements RedisRedPacketService {
    
        private static final String PREFIX = "red_packet_list_";
        // 每次取出1000条,避免一次取出消耗太多内存
        private static final int TIME_SIZE = 1000;
    
        @Autowired
        private RedisTemplate redisTemplate = null; // RedisTemplate
    
        @Autowired
        private DataSource dataSource = null; // 数据源
    
        @Override
        // 开启新线程运行
        @Async
        public void saveUserRedPacketByRedis(Long redPacketId, Double unitAmount) {
            System.err.println("开始保存数据");
            Long start = System.currentTimeMillis();
            // 获取列表操作对象
            BoundListOperations ops = redisTemplate.boundListOps(PREFIX + redPacketId);
            Long size = ops.size();
            Long times = size % TIME_SIZE == 0 ? size / TIME_SIZE : size / TIME_SIZE + 1;
            int count = 0;
            List<UserRedPacket> userRedPacketList = new ArrayList<UserRedPacket>(TIME_SIZE);
            for (int i = 0; i < times; i++) {
                // 获取至多TIME_SIZE个抢红包信息
                List userIdList = null;
                if (i == 0) {
                    userIdList = ops.range(i * TIME_SIZE, (i + 1) * TIME_SIZE);
                } else {
                    userIdList = ops.range(i * TIME_SIZE + 1, (i + 1) * TIME_SIZE);
                }
                userRedPacketList.clear();
                // 保存红包信息
                for (int j = 0; j < userIdList.size(); j++) {
                    String args = userIdList.get(j).toString();
                    String[] arr = args.split("-");
                    String userIdStr = arr[0];
                    String timeStr = arr[1];
                    Long userId = Long.parseLong(userIdStr);
                    Long time = Long.parseLong(timeStr);
                    // 生成抢红包信息
                    UserRedPacket userRedPacket = new UserRedPacket();
                    userRedPacket.setRedPacketId(redPacketId);
                    userRedPacket.setUserId(userId);
                    userRedPacket.setAmount(unitAmount);
                    userRedPacket.setGrabTime(new Timestamp(time));
                    userRedPacket.setNote("抢红包 " + redPacketId);
                    userRedPacketList.add(userRedPacket);
                }
                // 插入抢红包信息
                count += executeBatch(userRedPacketList);
            }
            // 删除Redis列表
            redisTemplate.delete(PREFIX + redPacketId);
            Long end = System.currentTimeMillis();
            System.err.println("保存数据结束,耗时" + (end - start) + "毫秒,共" + count + "条记录被保存。");
        }
    
        /**
         * 使用JDBC批量处理Redis缓存数据.
         * 
         * @param userRedPacketList 抢红包列表
         * @return 抢红包插入数量.
         */
        private int executeBatch(List<UserRedPacket> userRedPacketList) {
            Connection conn = null;
            Statement stmt = null;
            int[] count = null;
            try {
                conn = dataSource.getConnection();
                conn.setAutoCommit(false);
                stmt = conn.createStatement();
                for (UserRedPacket userRedPacket : userRedPacketList) {
                    String sql1 = "update T_RED_PACKET set stock = stock-1 where id=" + userRedPacket.getRedPacketId();
                    DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                    String sql2 = "insert into T_USER_RED_PACKET(red_packet_id, user_id, " + "amount, grab_time, note)"
                            + " values (" + userRedPacket.getRedPacketId() + ", " + userRedPacket.getUserId() + ", "
                            + userRedPacket.getAmount() + "," + "'" + df.format(userRedPacket.getGrabTime()) + "'," + "'"
                            + userRedPacket.getNote() + "')";
                    stmt.addBatch(sql1);
                    stmt.addBatch(sql2);
                }
                // 执行批量
                count = stmt.executeBatch();
                // 提交事务
                conn.commit();
            } catch (SQLException e) {
                /********* 错误处理逻辑 ********/
                throw new RuntimeException("抢红包批量执行程序错误");
            } finally {
                try {
                    if (conn != null && !conn.isClosed()) {
                        conn.close();
                    }
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            // 返回插入抢红包数据记录
            return count.length / 2;
        }
    }

      这里的@Async注解表示让Spring自动创建另外一条线程去运行它,这样它便不在抢最后一个红包的线程之内,因为这个方法是一个较长时间的方法,如果在同一个线程内,那么对于最后抢红包的用户来说就需要等待相当长的时间,影响用户体验。

      为了使用@Async注解,还需要在 Spring 中配置一个任务池:

    package com.ssm.chapter22.config;
    ...
    @EnableAsync
    public class WebConfig extends AsyncConfigurerSupport { 
      ...
        @Override
        public Executor getAsyncExecutor() {
            ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
            taskExecutor.setCorePoolSize(5);
            taskExecutor.setMaxPoolSize(10);
            taskExecutor.setQueueCapacity(200);
            taskExecutor.initialize();
            return taskExecutor;
        }
    }

      用户抢红包逻辑:grapRedPacketByRedis接收的参数,第一个是大红包名称“red_packet_5”中的5,而userId是jsp文件中发起抢红包请求的唯一标识[0,30000]中的某一个i值。

      当[0,30000]中的某一个值i发起请求后,假设 i 为 1000

    • jsp中直接异步post到url: "./userRedPacket/grapRedPacketByRedis.do?redPacketId=8&userId=" + i,然后调用grapRedPacketByRedis(Long redPacketId, Long userId)方法,其中redPacketId=5,而userId=1000。
    • 然后通过Object res = jedis.evalsha(sha1, 1, redPacketId + "", args);执行自定义的Lua脚本,其中,1表示key的个数,args表示grapRedPacketByRedis方法的两个参数值。定义用户抢红包信息在Redis中的键listKey=red_packet_list_5,大红包在Redis中的键red_packet_5,然后查询red_packet_5中的stock,返回还剩红包的数量stock,此时假设stock=7777,则由于还有红包,于是将stock变为7776,并更新到red_packet_5的stock中,然后将键值对red_packet_list_5-ARGV[1](也就是userId),即red_packet_list_5 - 1000写入Redis中。
    • 当返回结果为 2 时,说明最后一个红包已经被抢了,这个时候,jedis.hget("red_packet_" + redPacketId, "unit_amount");得到red_packet_5 → unit_amount 即单个小红包的金额10赋给变量unitAmount,然后saveUserRedPacketByRedis(redPacketId, unitAmount);方法将Redis中键red_packet_list的信息加上每个小红包金额信息,以及其他各种信息对应成用户抢红包数据库表定义,另开一个线程,将20000个数据库记录添加到数据库中保存起来。
        @Autowired
        private RedisTemplate redisTemplate = null;
    
        @Autowired
        private RedisRedPacketService redisRedPacketService = null;
    
        // Lua脚本
        String script = "local listKey = 'red_packet_list_'..KEYS[1] 
    " 
                 + "local redPacket = 'red_packet_'..KEYS[1] 
    "
                    + "local stock = tonumber(redis.call('hget', redPacket, 'stock')) 
    " 
                    + "if stock <= 0 then return 0 end 
    "
                    + "stock = stock -1 
    " 
                    + "redis.call('hset', redPacket, 'stock', tostring(stock)) 
    "
                    + "redis.call('rpush', listKey, ARGV[1]) 
    " 
                    + "if stock == 0 then return 2 end 
    " 
                    + "return 1 
    ";
    
        // 在缓存LUA脚本后,使用该变量保存Redis返回的32位的SHA1编码,使用它去执行缓存的LUA脚本[加入这句话]
        String sha1 = null;
    
        @Override
        public Long grapRedPacketByRedis(Long redPacketId, Long userId) {
            // 当前抢红包用户和日期信息
            String args = userId + "-" + System.currentTimeMillis();
            Long result = null;
            // 获取底层Redis操作对象
            Jedis jedis = (Jedis) redisTemplate.getConnectionFactory().getConnection().getNativeConnection();
            try {
                // 如果脚本没有加载过,那么进行加载,这样就会返回一个sha1编码
                if (sha1 == null) {
                    sha1 = jedis.scriptLoad(script);
                }
                // 执行脚本,返回结果
                Object res = jedis.evalsha(sha1, 1, redPacketId + "", args);
                result = (Long) res;
                // 返回2时为最后一个红包,此时将抢红包信息通过异步保存到数据库中
                if (result == 2) {
                    // 获取单个小红包金额
                    String unitAmountStr = jedis.hget("red_packet_" + redPacketId, "unit_amount");
                    // 触发保存数据库操作
                    Double unitAmount = Double.parseDouble(unitAmountStr);
                    System.err.println("thread_name = " + Thread.currentThread().getName());
                    redisRedPacketService.saveUserRedPacketByRedis(redPacketId, unitAmount);
                }
            } finally {
                // 确保jedis顺利关闭
                if (jedis != null && jedis.isConnected()) {
                    jedis.close();
                }
            }
            return result;
        }

       控制器方法:

        @RequestMapping(value = "/grapRedPacketByRedis")
        @ResponseBody
        public Map<String, Object> grapRedPacketByRedis(Long redPacketId, Long userId) {
            Map<String, Object> resultMap = new HashMap<String, Object>();
            Long result = userRedPacketService.grapRedPacketByRedis(redPacketId, userId);
            boolean flag = result > 0;
            resultMap.put("result", flag);
            resultMap.put("message", flag ? "抢红包成功": "抢红包失败");
            return resultMap;
        }

      模拟高并发的jsp文件,其中,由于post是异步请求,所以可以模拟多个用户同时请求的情况:

    <%@ page language="java" contentType="text/html; charset=UTF-8"
        pageEncoding="UTF-8"%>
    <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
    <html>
    <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>参数</title>
    <!-- 加载Query文件-->
    <script type="text/javascript"
        src="https://code.jquery.com/jquery-3.2.0.js">
            </script>
    <script type="text/javascript">
                $(document).ready(function () {
                      //模拟30000个异步请求,进行并发
                    var max = 30000;
                    for (var i = 1; i <= max; i++) {
                        //jQuery的post请求,请注意这是异步请求
                        $.post({
                            //请求抢id为1的红包
                            //根据自己请求修改对应的url和大红包编号
                            url: "./userRedPacket/grapRedPacketByRedis.do?redPacketId=8&userId=" + i,
                            //成功后的方法
                            success: function (result) {
                            }
                        });
                    }
                });
            </script>
    </head>
    <body>
    </body>
    </html>

      最后结果,在3万用户同时争抢2万个红包的场景下,Redis 只需要4秒,乐观锁需要33秒,而悲观锁需要54秒,可见Redis是多么的高效。

      使用Redis的风险在于Redis的不稳定性,因为其事务和存储都存在不稳定的因素,所以更多的时候,应该使用独立的Redis服务器做高并发业务,一方面可以提高Redis的性能,另一个方面即使在高并发的场合,Redis服务器宕机也不会影响现有的其他业务,同时也可以使用备机等设备提高系统的高可用,保证网络的安全稳定。

      

  • 相关阅读:
    异常处理与调试2
    异常处理与调试
    异常处理与调试
    身份证校验程序(下)- 零基础入门学习Delphi49
    身份证校验程序(下)- 零基础入门学习Delphi49
    身份证校验程序(上)- 零基础入门学习Delphi48
    身份证校验程序(上)- 零基础入门学习Delphi48
    python -- 面向对象三大特性
    python -- 面向对象初识
    python成长之路八 -- 内置函数
  • 原文地址:https://www.cnblogs.com/BigJunOba/p/9795169.html
Copyright © 2020-2023  润新知