Processing math: 100%
1 Star 3 Fork 0

ninesun/SpringBoot 整合 Redis实现签到功能

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
贡献代码
同步代码
ninesun- ninesun fix 4758024 2年前
取消
提示: 由于 Git 不支持空文件夾,创建文件夹后会生成空的 .keep 文件
Loading...
README
Apache-2.0

最近项目里需要集成签到和统计功能,连续签到后会给用户发放一些优惠券和奖品,以此来吸引用户持续在该品台进行活跃。下面我们一些来聊一聊目前主流的实现方案。

因为签到和统计的功能涉及的数据量比较大,所以在如此大的数据下利用传统的关系型数据库进行计算和统计是非常耗费性能的,所以目前市面上主要依赖于高性能缓存RedisBitMap 功能来实现。

先看看利用Mysql实现以上功能会有哪些缺陷和短板。

1.使用Mysql实现签到功能

首先我们需要一个签到表

DROP TABLE IF EXISTS `tb_sign`;
CREATE TABLE `tb_sign` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `user_id` int(11) NOT NULL COMMENT '用户Id',
  `year` year(4) NOT NULL COMMENT '签到的年',
  `month` tinyint(2) NOT NULL COMMENT '签到的月',
  `date` date NOT NULL COMMENT '签到日期',
  `is_backup` tinyint(1) NOT NULL COMMENT '是否补签',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

用户一次签到,就是一条记录,假如有1000万用户,平均每人每年签到次数为10次,则这张表一年的数据量为 1亿条

每签到一次需要使用(8 + 8 + 1 + 1 + 3 + 1)共22 字节的内存,一个月则最多需要600多字节

这样的坏处,占用内存太大了,极大的消耗内存空间!

我们可以根据 Redis中 提供的 BitMap 位图功能来实现,每次签到与未签到用0 或1 来标识 ,一次存31个数字,只用了2字节 这样我们就用极小的空间实现了签到功能

2.Redis BitMap

2.1 BitMap 的操作指令

  • SETBIT :向指定位置(offset)存入一个0或1
  • GETBIT :获取指定位置(offset)的bit值
  • BITCOUNT :统计BitMap中值为1的bit位的数量
  • BITFIELD :操作(查询、修改、自增)BitMap中bit数组中的指定位置(offset)的值
  • BITFIELD_RO :获取BitMap中bit数组,并以十进制形式返回
  • BITOP :将多个BitMap的结果做位运算(与 、或、异或)
  • BITPOS :查找bit数组中指定范围内第一个0或1出现的位置

2.2 使用 BitMap 完成功能实现

利用SETBIT新增key 进行存储

SETBIT bm1 0 1

看不懂上面的指令?没关系,我们可以通过help指令查看提示

help SETBIT

在这里插入图片描述

通过这个指令可以看出Redis SETBIT 命令用于对 key 所储存的字符串值,设置或清除指定偏移量上的位(bit)。位的设置或清除取决于 value,可以是 0 或者是 1 。

当 key 不存在时,自动生成一个新的字符串值。字符串会进行伸展以确保它可以将 value 保存在指定的偏移量上。当字符串值进行伸展时,空白位置以 0 填充。 offset 参数必须大于或等于 0 ,小于 2^32 (bit 被限制在 512 MB 之内)。

提示:如果 offset 偏移量的值较大,计算机进行内存分配时可能会造成 Redis 服务器被阻塞。

在这里插入图片描述

这样的话我们就可以通过偏移量设置每一天的签到情况:

  • 偏移量:表示天
  • val值:表示是否签到
    • 已签到设置为1
    • 未签到设置0

下面我们只需要通过GETBIT命令就可以查看每一天的签到情况

GETBIT bm1 2

表示查看bm1用户第二天的签到情况 在这里插入图片描述

同样,我们可以通过BITCOUNT可以统计出该用户签到了多少天

BITCOUNT bm1 

在这里插入图片描述

BITFIELD

通过BITFIELD以原子方式操作(查询、修改、自增)BitMap中bit数组中的指定位置(offset)的值

BITFIELD复杂度是O(n),其中n是访问的计数器数。

语法:

BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP|SAT|FAIL]

参数说明:

  • key :需要操作的键名。
  • GET type offset :获取指定位数的值
  • type表示数据类型,可以是无符号整数(u)或有符号整数(i)
  • offset表示偏移量,如果位不存在,返回0。
  • SET type offset value :设置指定位数的值,type和offset同上,value表示需要设置的值,因为此命令只支持设置8位或更少的位,所以value不能超过8个二进制位。
  • INCRBY type offset increment :将指定位的值增加指定的增量,type和offset同上,increment表示需要增加的值,也必须不超过8个二进制位。
  • OVERFLOW :做溢出的处理,默认是WRAP(循环),其他两个选项是SAT(饱和,超出范围的值都设置成最大或最小值)和FAIL(不允许溢出,会返回一个错误)。

使用方法

  • 获取一个整数的二进制位

举个例子,我们有一个整数值10,它的二进制位是00001010,我们想获取它的第3位到第5位的值,即001。那么可以这样使用:

127.0.0.1:6379> BITFIELD myint GET u3 3 u1 6
1) (integer) 1
2) (integer) 0

u3:表示3位无符号整数

指定myint键值,通过GET命令获取它的第3位到第5位,结果返回一个二进制数001,也就是十进制的1。第二个返回值是0,表示其他未指定的位都是0。

  • 设置一个整数的二进制位 现在我们想把刚才的整数值10的第3位到第5位修改为101,即值为5。可以这样设置:
127.0.0.1:6379> BITFIELD myint SET u3 3 5
(integer) 10

执行成功后,该键值指定的整数值变为13(二进制为00001101)。

所以统计每个用户的签到情况和积分,可以使用 Redis BITFIELD 命令记录每个用户每天的签到情况。

每个用户一年365天,需要使用整数类型的BITFIELD信息记录,每个用户需要365个二进制位来表示签到情况(已签到为1,未签到为0),再需要一个整数位去表示用户的总积分,就可以方便地统计用户签到情况并进行排名。

当然该命令的功能远不止于此,在某些系统中,需要对某些数据做计数,比如对每个IP地址访问次数的计数。可以使用字符串类型的计数器来达到这个目的,首先在Redis中创建一个字符串类型的计数器(值为0),通过INCRBY命令执行增减操作,每次给IP地址所代表的计数器加1,最后获取到的长度即为IP地址对应的访问次数。如果访问量过大,可以使用整数类型的BITFIELD存储计数器,通过INCRBY命令执行增减操作。这样可以优化性能并减少内存占用。

BITPOS 查询1 和 0 第一次出现的坐标 在这里插入图片描述

3.SpringBoot整合Redis实现签到功能

3.1 思路分析

考虑到每月初需要重置连续签到次数,我们可以把 年和月 作为BitMap的key,然后保存到一个BitMap中,每次签到就到对应的位上把数字从0 变为1,只要是1,就代表是这一天签到了,反之咋没有签到。

key的格式为:"USER_SIGN_KEY:" + userId + keySuffix

  • USER_SIGN_KEY:前缀
  • userId:用户id
  • keySuffix:yyyyMM

Value则采用长度为4个字节(32位)的位图(最大月份只有31天)。位图的每一位代表一天的签到,1表示已签,0表示未签。

例如:USER_SIGN_KEY:122101:202309:表示ID=122101的用户在2023年9月的签到记录

# 用户9月17号签到
SETBIT USER_SIGN_KEY:122101:202309 16 1 # 偏移量是从0开始,所以要把17减1
 
# 检查9月17号是否签到
GETBIT USER_SIGN_KEY:122101:202309 16 # 偏移量是从0开始,所以要把17减1
 
# 统计9月份的签到次数
BITCOUNT USER_SIGN_KEY:122101:202309
 
# 获取9月份前30天的签到数据,u30表示取0-29位的数据
BITFIELD USER_SIGN_KEY:122101:202309 get u30 0
 
# 获取9月份首次签到的日期
BITPOS USER_SIGN_KEY:122101:202309 1 # 返回的首次签到的偏移量,加上1即为当月的某一天

有了上面的理论支撑,下面我开始在项目里去集成,为了节约篇幅,只展示核心代码,项目地址在文章末尾,完整代码可下载之后自行观看

首先需要我们项目里集成redis,可以参考:《springBoot集成redis(jedis)详解》

3.2 签到功能

签到功能实现方式如下:

    @GetMapping("sign")
    public Object sign() {
        //1. 获取登录用户
        Long userId = 122101L;
        //2.获取签到使用的key
        String key = RedisKeyUtils.createSignKey(userId);
        //3. 获取今天是本月的第几天设置偏移量
        LocalDateTime now = LocalDateTime.now();
        int offset = now.getDayOfMonth() - 1;
        System.out.println(String.format("入参:key:%s,offset:%s", JSON.toJSONString(key), JSON.toJSONString(offset)));
        //5. 写入redis setbit key offset 1
        Boolean res = jedis.setbit(key, offset, true);
        System.out.println(String.format("出参:%s", JSON.toJSONString(res)));
        return String.format("%s签到成功,签到结果:%s", JSON.toJSONString(now.format(DateTimeFormatter.ofPattern("yyyyMM"))), JSON.toJSONString(res));
    }

测试: 在这里插入图片描述

在这里插入图片描述

签到功能我们已经实现,只需要将对应该月的某天设置为1则表示签到成功

检查用户是否签到

    @GetMapping("checkSign")
    public Object checkSign(@RequestParam(value = "sigDate") String sigDate) {
        //1. 获取登录用户
        Long userId = 122101L;
        //2.获取签到使用的key
        LocalDateTime time = TimeUtils.str2LocalDateTime(TimeUtils.PATTERN.YYYYMMDD, sigDate);
        if (Objects.isNull(time)) {
            return false;
        }
        String key = RedisKeyUtils.createSignKey(time, userId);
        //3. 获取今天是本月的第几天设置偏移量
        int offset = time.getDayOfMonth() - 1;
        System.out.println(String.format("入参:key:%s,offset:%s", JSON.toJSONString(key), JSON.toJSONString(offset)));
        Boolean res = jedis.getbit(key, offset);
        System.out.println(String.format("出参:%s", JSON.toJSONString(res)));
        return res;
    }

访问:http://127.0.0.1:8080/checkSign?sigDate=2023-09-07 在这里插入图片描述

下面我们继续看看签到统计功能

3.3 签到统计功能

Q1:什么叫连续签到天数?

从最后一次签到开始向前统计,直到遇到第一次未签到为止,计算总的签到次数,就是连续签到天数。 在这里插入图片描述

所以我们统计的方式很简单:

❝获得当前这个月的最后一次签到数据,定义一个计数器,然后不停的向前统计,直到获得第一个非0的数字即可,每得到一个非0的数字计数器+1,直到遍历完所有的数据,就可以获得当前月的签到总天数了❞

Q2:如何得到本月到今天为止的所有签到数据?

那我们则需要借助BITFIELD指令来进行实现

BITFIELD key GET u[dayOfMonth-1] 0

假设今天是5号,那么我们就可以从当前月的第一天开始,获得到当前这一天的位数,是5号,那么就是5位,去拿这段时间的数据,就能拿到所有的数据了,那么这5天里边签到了多少次呢?统计有多少个1即可。

Q3:如何从后向前遍历每个Bit位?

值得我们注意的是:❝bitMap返回的数据是10进制,哪假如说返回一个数字8,那么我哪儿知道到底哪些是0,哪些是1呢?❞

这是一道很简单的位运算算法题

我们只需要让得到的10进制数字和1做与运算就可以了,因为1只有遇见1 才是1,其他数字都是0 ,我们把签到结果和1进行与操作,每与一次,就把签到结果向右移动一位,依次类推,我们就能完成逐个遍历的效果了。

通过上面的几个方法就可以很容易统计用户签到的情况。

下面看看代码的具体实现

  • 获取用户连续签到的天数

为了方便测试,我的当前时间为:2023-09-07,所以我对1,3,5,6,7五天的签到设置为1 在这里插入图片描述

那么当前缓存里的值则为:1010111,对应的10进制数为:87,最大连续1出现的个数为3,所以签到的天数为3 在这里插入图片描述

代码如下:

    @GetMapping("signCount")
    public Object signCount() {
        //1. 获取登录用户
        Long userId = 122101L;
        //2. 获取日期
        LocalDateTime now = LocalDateTime.now();
        //3. 拼接key
        String key = RedisKeyUtils.createSignKey(now, userId);
        //4. 获取今天是本月的第几天
        int offset = now.getDayOfMonth();
        //5. 获取本月截至今天为止的所有的签到记录,返回的是一个十进制的数字 BITFIELD USER_SIGN_KEY1:5:202309 GET u5 0
        String type = String.format("u%d", offset);
        System.out.println(String.format("入参:key:%s,operationType:%s,type:%s", JSON.toJSONString(key), JSON.toJSONString(RedisHelper.OPERATION_TYPE.GET.name()), JSON.toJSONString(type)));
        List<Long> result = RedisHelper.bitField(key, RedisHelper.OPERATION_TYPE.GET, type, "0");
        //没有任务签到结果
        if (result == null || result.isEmpty()) {
            return 0;
        }
        Long num = result.get(0);
        if (num == null || num == 0) {
            return 0;
        }
        //6. 循环遍历
        int count = 0;
        while (true) {
            //6.1 让这个数字与1 做与运算,得到数字的最后一个bit位 判断这个数字是否为0
            if ((num & 1) == 0) {
                //如果为0,签到结束
                break;
            } else {
                count++;
            }
            num >>>= 1;
        }
        System.out.println(String.format("缓存值:%s,签到天数:%d", JSON.toJSONString(result), count));
        return count;
    }

测试: 在这里插入图片描述

3.4 获取当月首次签到日期

利用Bitpos可以帮助我们轻松实现该功能 在这里插入图片描述

  • bitpos USER_SIGN_KEY:122101:202309 1:表示1首次出现的位置
  • bitpos USER_SIGN_KEY:122101:202309 0:表示0首次出现的位置
    @GetMapping("getFirstSignDate")
    public Object getFirstSignDate(@RequestParam(value = "time") String time) {
        //1. 获取登录用户
        Long userId = 122101L;
        //2. 获取日期
        LocalDateTime dateTime = TimeUtils.str2LocalDateTime(TimeUtils.PATTERN.YYYYMMDD, time);
        //3. 拼接key
        String key = RedisKeyUtils.createSignKey(dateTime, userId);
        //4. 获取首次出现的位置索引
        long pos = jedis.bitpos(key, true);
        //5. 转换为日期
        String res = pos < 0 ? "无签到记录" : TimeUtils.localDateTime2String(TimeUtils.PATTERN.YYYYMMDD, dateTime.withDayOfMonth((int) (pos + 1)));
        return res;
    }

在这里插入图片描述

3.5 获取当月的签到情况

    @GetMapping("getSignInfo")
    public Object getSignInfo(@RequestParam(value = "time") String time) {
        //1. 获取登录用户
        Long userId = 122101L;
        //2. 获取日期
        LocalDateTime dateTime = TimeUtils.str2LocalDateTime(TimeUtils.PATTERN.YYYYMMDD, time);
        //当月天数
        int monthOfDays = TimeUtils.getDayNumsOfMonth(dateTime);
        Map<String, Boolean> signMap = new HashMap<>(dateTime.getDayOfMonth());
        //3. 拼接key
        String key = RedisKeyUtils.createSignKey(dateTime, userId);
        //4. 设置位数
        String type = String.format("u%d", monthOfDays);
        System.out.println(String.format("入参:key:%s,operationType:%s,type:%s", JSON.toJSONString(key), JSON.toJSONString(RedisHelper.OPERATION_TYPE.GET.name()), JSON.toJSONString(type)));
        List<Long> list = RedisHelper.bitField(key, RedisHelper.OPERATION_TYPE.GET, type, "0");
        if (!CollectionUtils.isEmpty(list)) {
            Long num = list.get(0);
            int i = monthOfDays;
            while (i > 0) {
                String d = TimeUtils.localDateTime2String(TimeUtils.PATTERN.YYYYMMDD, dateTime.withDayOfMonth(i--));
                if ((num & 1) == 0) {
                    signMap.put(d, false);
                } else {
                    signMap.put(d, true);
                }
                num >>>= 1;
            }
        }
        //按照日期排序
        TreeMap sortedMap = new TreeMap(signMap);
        System.out.println("出参:" + JSON.toJSONString(sortedMap));
        return sortedMap;
    }

在这里插入图片描述

至此签到所需要的功能我们就全部实现了

本文的核心在于bitMap的使用,但是任何技术都需要有具体的落地,所以借着签到的场景带着大家具体使用一下。

当然bitmap的落地场景远不止我上文中介绍的签到统计等业务层面的方案,针对缓存穿透的场景,bitMap也是一个极佳的选择。

4.利用BitMap解决缓存穿透

描述: 缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。

这种可以认为是系统漏洞,一旦发生这种情况一般都来自于恶意请求。

常见解决方案:

  • 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
  • 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击

第一种解决方案:遇到的问题是如果用户访问的是id不存在的数据,则此时就无法生效,如id远大于某个值,虽然不小于0,但是也无法进行过滤。

第二种解决方案:遇到的问题是:如果是不同的id那就可以防止下次过来直击数据

所以我们如何解决呢?

我们可以将数据库的数据,所对应的id写入到一个list集合中,当用户过来访问的时候,我们直接去判断list中是否包含当前的要查询的数据,如果说用户要查询的id数据并不在list集合中,则直接返回,如果list中包含对应查询的id数据,则说明不是一次缓存穿透数据,则直接放行。 在这里插入图片描述

现在的问题是这个主键其实并没有那么短,有的可能是通过各种拼接生成的Id,是很长的一个主键,所以如果采用以上方案,这个list也会很大,所以我们可以 使用bitmap来减少list的存储空间

我们可以把list数据抽象成一个非常大的bitmap,我们不再使用list,而是将db中的id数据利用哈希思想,比如:

id 求余bitmap长度 :index=id%bitmap.size算出当前这个id对应应该落在bitmap的哪个索引上,然后将这个值从0变成1,然后当用户来查询数据时,此时已经没有了list,让用户用他查询的id去用相同的哈希算法, 算出来当前这个id应当落在bitmap的哪一位,然后判断这一位是0,还是1,如果是0则表明这一位上的数据一定不存在,采用这种方式来处理,需要重点考虑一个事情,就是误差率, 所谓的误差率就是指当发生哈希冲突的时候,产生的误差 在这里插入图片描述

Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

简介

SpringBoot 整合 Redis实现签到统计功能 展开 收起
README
Apache-2.0
取消

发行版

暂无发行版

贡献者

全部

近期动态

不能加载更多了
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
Java
1
https://gitee.com/ninesuntec/redisBitMapSign.git
git@gitee.com:ninesuntec/redisBitMapSign.git
ninesuntec
redisBitMapSign
SpringBoot 整合 Redis实现签到功能
master

搜索帮助