痛点:类似今日头条、抖音、淘宝这样的额用户访问级别都是亿级的,请问如何处理?
存的进+取得快+多维度
统计多个集合元素的聚合结果(交并差集合统计) set集合
抖音短视频最新评论留言的场景,设计一个展现列表
在面对需要展示最新列表、排行榜等场景时,如果数据更新频繁或者需要分页显示,建议使用zset
集合元素的取值就只有 0 和 1 ,来记录签到还是没签到 bitmap
只统计一个集合中不重复的元素个数 hyperloglog
既要埋头苦干,又要仰望星空
很多计数类场景,比如 每日注册 IP 数、每日访问 IP 数、页面实时访问数 PV、访问用户数 UV等。
因为主要的目标高效、巨量地进行计数,所以对存储的数据的内容并不太关心。
也就是说它只能用于统计巨量数量,不太涉及具体的统计对象的内容和精准性。
统计单日一个页面的访问量(PV),单次访问就算一次。
统计单日一个页面的用户访问量(UV),即按照用户为维度计算,单个用户一天内多次访问也只算一次。
多个key的合并统计,某个门户网站的所有模块的PV聚合统计就是整个网站的总PV。
基数
命令 | 作用 |
---|---|
pfadd key element… | 将所有元素添加到key中 |
pfcount key | 统计key的估算值(不精确) |
pgmerge new. key key1 key2. … | 合并key至新key |
基数统计就是 HyperLogLog
只是进行不重复的基数统计,不是集合也不保存数据,只记录数量而不是具体内容
有误差
用mysql
用redis的hash结构存储
hyperloglog
Redis使用了 2 14 = 16384 2^{14}=16384 214=16384个桶,按照上面的标准差,误差为0.81%, 精度相当高。Redis使用一个long型哈希值的前14个比特用来确定桶编号,剩下的50个比特用来做基数估计。而 2 6 = 64 2^6=64 26=64,所以只需要用6个比特表示下标值,在一 般情况下, 一个HLL数据结构占用内存的大小为 16384 ∗ 6 / 8 = 124 16384*6/8= 124 16384∗6/8=124kB, Redis将这种情况称为密集(dense) 存储。
HypeLogLogService
public interface HypeLogLogService {
public long uv();
}
HyperLogLogServiceImpl
import com.xfcy.service.HypeLogLogService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.Random;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
public class HypeLogLogServiceImpl implements HypeLogLogService {
@Resource
private RedisTemplate redisTemplate;
/**
* 模拟后台有用户点击网站首页,每个用户来自不同的IP地址
*
* @PostConstruct修饰的方法会在服务器加载Servlet的时候运行,并且只会被服务器执行一次。
*/
@PostConstruct
public void initIp() {
new Thread(() -> {
String ip = null;
for (int i = 0; i < 200; i++) {
Random random = new Random();
ip = random.nextInt(256) + "." + random.nextInt(256) + "." + random.nextInt(256) + "." + random.nextInt(256);
Long hll = redisTemplate.opsForHyperLogLog().add("hll", ip);
log.info("ip = {}, 该IP地址访问首页的次数={}",ip,hll);
try {
// 暂停3秒钟
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "t1").start();
}
public long uv() {
// PFCOUNT
return redisTemplate.opsForHyperLogLog().size("hll");
}
}
HyperLogLogController
import com.xfcy.service.HypeLogLogService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@Api(tags = "淘宝亿级UV的Redis统计方案")
@Slf4j
@RestController
public class HyperLogLogController {
@Resource
private HypeLogLogService hypeLogLogService;
@ApiOperation("获得IP去重复后的UV统计访问量")
@RequestMapping(value = "/uv",method = RequestMethod.GET)
public long uv(){
return hypeLogLogService.uv();
}
}
GEOADD添加经纬度坐标
会出现中文乱码
redis -cli -a 123456 --raw
GEOPOS
返回经纬度GEOHASH
返回坐标的 geohash 表示 (base32编码)主要分为三步
GEODIST
返回两个位置之间的距离
GEORADIUS
以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。GEORADIUSBYMEMBER 找出指定范围内的元素,中心点是由给定的位置元素决定
GEORADIUS: 以给定的经纬度为中心,找出某一半径内的元素
public interface GeoService {
String geoAdd();
Point position(String member);
String hash(String member);
Distance distance(String member1, String member2);
GeoResults radiusByxy();
GeoResults radiusMember();
}
import com.xfcy.service.GeoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.geo.*;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
@Slf4j
public class GeoServiceImpl implements GeoService {
public static final String CITY = "city";
@Autowired
private RedisTemplate redisTemplate;
@Override
public String geoAdd() {
Map<String, Point> map = new HashMap<>();
map.put("白马寺", new Point(112.610356,34.728481));
map.put("龙门石窟", new Point(112.484071,34.564375));
map.put("老君山", new Point(111.663,33.75186));
map.put("白马寺的公共厕所1", new Point(112.608311,34.726809));
map.put("白马寺的公共厕所2", new Point(112.610356,34.728481));
redisTemplate.opsForGeo().add(CITY, map);
return null;
}
@Override
public Point position(String member) {
// 获取经纬度坐标
List<Point> position = redisTemplate.opsForGeo().position(CITY, member);
return position.get(0);
}
@Override
public String hash(String member) {
// geohash 算法生成的base32编码
List<String> hash = redisTemplate.opsForGeo().hash(CITY, member);
return hash.get(0);
}
@Override
public Distance distance(String member1, String member2) {
// 获取两个给定位置之间的距离
Distance distance = redisTemplate.opsForGeo().distance(CITY, member1, member2,
RedisGeoCommands.DistanceUnit.KILOMETERS);
return distance;
}
@Override
public GeoResults radiusByxy() {
// 通过经纬度查找附近的,白马寺的位置 112.610356,34.728481
Circle circle = new Circle(112.610356, 34.728481, Metrics.KILOMETERS.getMultiplier());
// 返回50条
RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeCoordinates().sortDescending().limit(50);
GeoResults<RedisGeoCommands.GeoLocation<String>> geoResults = redisTemplate.opsForGeo().radius(CITY, circle, args);
return geoResults;
}
@Override
public GeoResults radiusMember() {
// 通过地方查找附近,洛阳白马寺为例
String member = "白马寺";
// 返回10条
RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().sortAscending().limit(10);
// 半径 10 公里内
Distance distance = new Distance(10, Metrics.KILOMETERS);
GeoResults<RedisGeoCommands.GeoLocation<String>> geoResults = redisTemplate.opsForGeo().radius(CITY, member, distance, args);
return geoResults;
}
}
import com.xfcy.service.GeoService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResults;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import org.springframework.data.geo.Point;
@Api(tags = "美团地图位置附近的酒店推送GEO")
@Slf4j
@RestController
public class GeoController {
@Resource
private GeoService geoService;
@ApiOperation("添加经纬度坐标")
@RequestMapping(value = "/geoadd",method = RequestMethod.GET)
public String geoAdd() {
return geoService.geoAdd();
}
@ApiOperation("获取经纬度坐标geopos")
@RequestMapping(value = "/geopos",method = RequestMethod.GET)
public Point position(String member){
return geoService.position(member);
}
@ApiOperation("获取经纬度生成的base32编码值geohash")
@RequestMapping(value = "/geohash",method = RequestMethod.GET)
public String hash(String member){
return geoService.hash(member);
}
@ApiOperation("获取两个给定位置之间的距离")
@RequestMapping(value = "/geodist",method = RequestMethod.GET)
public Distance distance(String member1, String member2){
return geoService.distance(member1,member2);
}
@ApiOperation("通过经纬度查找洛阳白马寺附近的")
@RequestMapping(value = "/georadius",method = RequestMethod.GET)
public GeoResults radiusByxy(){
return geoService.radiusByxy();
}
@ApiOperation("通过地方查找附近,白马寺为例")
@RequestMapping(value = "/georadiusByMember",method = RequestMethod.GET)
public GeoResults radiusMember(){
return geoService.radiusMember();
}
}
CREATE TABLE user_sign
(
keyid BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT,
user_key VARCHAR(200),#京东用户ID
sign_date DATETIME,#签到日期(20210618)
sign_count INT #连续签到天数
)
INSERT INTO user_sign(user_key,sign_date,sign_count)
VALUES ('20210618-xxxx-xxxx-xxxx-xxxxxxxxxxxx','2020-06-18 15:11:12',1);
SELECT
sign_count
FROM
user_sign
WHERE
user_key = '20210618-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
AND sign_date BETWEEN '2020-06-17 00:00:00' AND '2020-06-18 23:59:59'
ORDER BY
sign_date DESC
LIMIT 1;
签到量用户小这个可以,如何解决这个点
一条签到记录对应一条记录,会占据越来越大的空间。
一个月最多31天,刚好我们的int类型是32位,那这样一个int类型就可以搞定一个月,32位大于31天,当天来了位是1没来就是0。
一条数据直接存储一个月的签到记录,不再是存储一天的签到记录。
基于redis的 Bitmap 实现签到日历
在签到统计时,每个用户一天的签到用1个bit位就能表示,
一个月(假设是31天)的签到情况用31个bit位就可以,一年的签到也只需要用365个bit位,根本不用太复杂的集合类型
SETBIT key offset value // 将第offset的值设为value value只能是0或1 offset 从0开始
GETBIT key offset // 获得第offset位的值
STRLEN key // 得出占多少字节 超过8位后自己按照8位一组一byte再扩容
BITCOUNT key // 得出该key里面含有几个1
BITOP and destKey key1 key2 // 对一个或多个 key 求逻辑并,并将结果保存到 destkey
BITOP or destKey key1 key2 // 对一个或多个 key 求逻辑或,并将结果保存到 destkey
BITOP XOR destKey key1 key2 // 对一个或多个 key 求逻辑异或,并将结果保存到 destkey
BITOP NOT destKey key1 key2 // 对一个或多个 key 求逻辑非,并将结果保存到 destkey