草庐IT

基于美团Leaf-Segment的双buffer方案实现序列号生成器

luffylv 2024-03-26 原文

业务背景

       有时项目中对于流水号有一些特殊的需求。比如,和业务A有关数据,我们在落库时想要给每条数据添加一个流水号字段,用于作为全局唯一标识。流水号格式规则如下,如:BTA(业务A代号)+年月日(20221208)+序列号。并且对序列号的长度有要求,如序列号要求为5位,即从00001到99999,当序列号达到99999后,再次获取则继续从00001开始累加循环。流水号的形式如TX2022120800001。在此之前需要对业务A有关数据每日的数据量进行评估,以上述为例,若一天的单据量超过99999,再次循环可能会造成流水号重复,以致流水号不唯一,所以序列号最大值可以设的稍大一位。

初期方案

最开始实现的方案是每次需要序列号,则通过累加DB中对应业务的序列号的值并获取来实现。这种方案的缺点十分明显,效率低,也会造成某段时间DB压力巨大,当DB宕机时将无法获取到序列号,造成整个系统瘫痪。

基于美团Leaf的改进方案

接触到美团Leaf生成分布式ID的方案后决定将其用于生成序列号的实现方案。但原生的美团Leaf并不太适用于上面的需求。原因如下

1、美团Leaf是独立部署的、作为内部公共的基础技术设施的一个分布式ID的proxy-server。对于一些没有使用病独立部署leaf服务的公司,若某个项目使用该方案,除项目服务外。还需要部署leaf服务节点,代价有点大,得不偿失。

2、美团Leaf的segment方案生成的ID是趋势递增的不支持若序列号要求为5位,当序列号达到99999后,再次获取则继续从00001开始这样的一个需求。

针对以上两点,站在美团leaf的肩膀上,将其改造为项目中的工具类,并且满足我们对于序列号的长度的要求。

代码如下:

建表语句

drop table if exists psm_serial_no_record;
create table psm_serial_no_record
(
   id                   bigint(20) not null auto_increment comment '主键',
   biz_source           varchar(64) not null default '' comment '业务类型',
   max_id               int(11) not null default 1 comment '最大值',
   is_delete            tinyint(1) not null default 0 comment '删除标记:0未删除 1已删除',
   create_time          datetime not null default CURRENT_TIMESTAMP comment '创建时间',
   update_time          datetime not null default CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP comment '修改时间',
   primary key (id),
   unique key `idx_unique_biz_source` (`biz_source`) using btree
)ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8 COMMENT='序列号表';

PO

import lombok.Data;

import java.io.Serializable;
import java.util.Date;

/**
 *@Description 序列号记录
 **/
@Data
public class PsmSerialNoRecord implements Serializable {

    private static final long serialVersionUID = -42765629647798182L;

    /**
     * ID
     */
    private Long id;

    /**
     * 业务类型
     */
    private String bizSource;

    /**
     * 最大值
     */
    private long maxId;

    /**
     * 删除标记
     */
    private Integer isDelete;

    /**
     * 新增时间
     */
    private Date createTime;

    /**
     * 更新时间
     */
    private Date updateTime;
}

PsmSerialNoRecordDao

import com.example.serialnodemo.po.PsmSerialNoRecord;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

@Mapper
public interface PsmSerialNoRecordDao {

    int updateMaxId(@Param("key") String key, @Param("step") int step, @Param("limit") long limit);

    List<String> selectForLock(@Param("keyList") List<String> keyList);

    void insertRecord(PsmSerialNoRecord record);

    PsmSerialNoRecord selectRecordByKey(@Param("key") String key);
}

PsmSerialNoRecordMapper

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.serialnodemo.dao.PsmSerialNoRecordDao">

    <resultMap id="BaseResultMap" type="com.example.serialnodemo.po.PsmSerialNoRecord">
        <id column="id" jdbcType="BIGINT" property="id"/>
        <result column="biz_source" jdbcType="VARCHAR" property="bizSource"/>
        <result column="max_id" jdbcType="BIGINT" property="maxId"/>
        <result column="is_delete" jdbcType="INTEGER" property="isDelete"/>
        <result column="create_time" jdbcType="TIMESTAMP" property="createTime"/>
        <result column="update_time" jdbcType="TIMESTAMP" property="updateTime"/>
    </resultMap>

    <sql id="Base_Column_List">
      id, biz_source, max_id, is_delete, create_time, update_time
    </sql>

    <update id="updateMaxId">
        UPDATE psm_serial_no_record
        SET max_id =
                CASE WHEN max_id = #{limit,jdbcType=BIGINT} THEN #{step,jdbcType=INTEGER}
                     WHEN max_id + #{step,jdbcType=INTEGER} &lt; #{limit,jdbcType=BIGINT} THEN max_id + #{step,jdbcType=INTEGER}
                     WHEN max_id + #{step,jdbcType=INTEGER} &gt;= #{limit,jdbcType=BIGINT} THEN #{limit,jdbcType=BIGINT}
                END
        WHERE biz_source = #{key,jdbcType=VARCHAR}
    </update>

    <select id="selectForLock" resultType="string">
        SELECT
        biz_source
        FROM psm_serial_no_record
        WHERE is_delete = 0
        <if test="keyList != null and keyList.size()>0">
            AND biz_source IN
            <foreach collection="keyList" item="key" open="(" close=")" separator=",">
                #{key}
            </foreach>
        </if>
        FOR UPDATE
    </select>

    <insert id="insertRecord" keyColumn="id" keyProperty="id"
            parameterType="com.example.serialnodemo.po.PsmSerialNoRecord" useGeneratedKeys="true">
        INSERT INTO psm_serial_no_record
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="bizSource != null">
                biz_source,
            </if>
            <if test="maxId != null">
                max_id,
            </if>
            <if test="isDelete != null">
                is_delete,
            </if>
            <if test="createTime != null">
                create_time,
            </if>
            <if test="updateTime != null">
                update_time,
            </if>
        </trim>
        <trim prefix="values (" suffix=")" suffixOverrides=",">
            <if test="bizSource != null">
                #{bizSource,jdbcType=VARCHAR},
            </if>
            <if test="maxId != null">
                #{maxId,jdbcType=BIGINT},
            </if>
            <if test="isDelete != null">
                #{isDelete,jdbcType=BOOLEAN},
            </if>
            <if test="createTime != null">
                #{createTime,jdbcType=TIMESTAMP},
            </if>
            <if test="updateTime != null">
                #{updateTime,jdbcType=TIMESTAMP},
            </if>
        </trim>
    </insert>

    <select id="selectRecordByKey" resultMap="BaseResultMap">
        SELECT
        <include refid="Base_Column_List"/>
        FROM psm_serial_no_record
        WHERE biz_source = #{key,jdbcType=VARCHAR}
    </select>

</mapper>

PsmSerialNoRecordService

import com.example.serialnodemo.po.PsmSerialNoRecord;

import java.util.List;

public interface PsmSerialNoRecordService {

    int modifyMaxId(String key, int step, long limit);


    List<String> queryForLock(List<String> keyList);

    void addRecord(PsmSerialNoRecord record);

    PsmSerialNoRecord findRecordByKey(String key);

    PsmSerialNoRecord modifyMaxIdAndGet(String key, int step, long limit) throws Exception;
}

PsmSerialNoRecordServiceImpl

import com.example.serialnodemo.po.PsmSerialNoRecord;
import com.example.serialnodemo.dao.PsmSerialNoRecordDao;
import com.example.serialnodemo.service.PsmSerialNoRecordService;
import com.google.common.collect.Lists;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

import java.util.List;

/**
 * @Description
 * @Program psm-report
 **/
@Service
public class PsmSerialNoRecordServiceImpl implements PsmSerialNoRecordService {

    @Autowired
    private PsmSerialNoRecordDao psmSerialNoRecordDao;

    @Override
    public int modifyMaxId(String key, int step, long limit) {
        if (StringUtils.isNotBlank(key) && step > 0 && limit > 0L) {
            return psmSerialNoRecordDao.updateMaxId(key, step, limit);
        }
        return -1;
    }

    @Override
    public List<String> queryForLock(List<String> keyList) {
        if (!CollectionUtils.isEmpty(keyList)) {
            return psmSerialNoRecordDao.selectForLock(keyList);
        }
        return Lists.newArrayList();
    }

    @Override
    public void addRecord(PsmSerialNoRecord record) {
        if (record != null) {
            psmSerialNoRecordDao.insertRecord(record);
        }
    }

    @Override
    public PsmSerialNoRecord findRecordByKey(String key) {
        if (StringUtils.isNotBlank(key)) {
            return psmSerialNoRecordDao.selectRecordByKey(key);
        }
        return null;
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public PsmSerialNoRecord modifyMaxIdAndGet(String key, int step, long limit) throws Exception {
        int update = this.modifyMaxId(key, step, limit);
        // 若没有该业务类型的序列号记录,则报错
        if (update <= 0) {
            throw new Exception("更新序列号失败,key:" + key);
        }
        return this.findRecordByKey(key);
    }
}

Segment

import lombok.Data;

import java.util.concurrent.atomic.AtomicLong;

/**
 * @description: Segment
 */
@Data
public class Segment {

    /**
     * 步长
     */
    public volatile int step;

    /**
     * 自增值
     */
    private AtomicLong value = new AtomicLong(0);

    /**
     * 最大值
     */
    private volatile long max;

    /**
     * buffer
     */
    private SegmentBuffer buffer;

    public Segment(SegmentBuffer buffer) {
        this.buffer = buffer;
    }

    /**
     * @description: 获取空闲数
     *
     * @return long 空闲数
     */
    public long getIdle() {
        return this.getMax() - getValue().get();
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder("Segment(");
        sb.append("value:");
        sb.append(value);
        sb.append(",max:");
        sb.append(max);
        sb.append(",step:");
        sb.append(step);
        sb.append(")");
        return sb.toString();
    }

}

SegmentBuffer

import lombok.Data;

import java.util.Arrays;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * @description: 双buffer
 */
@Data
public class SegmentBuffer {

    /**
     * key,唯一值
     */
    private String key;

    /**
     * 双buffer段数组,这里只有2段
     */
    private Segment[] segments;

    /**
     * 当前使用的segment的index
     */
    private volatile int currentPos;

    /**
     * 下一个segment是否处于可切换状态
     */
    private volatile boolean nextReady;

    /**
     * 线程是否在运行中
     */
    private final AtomicBoolean threadRunning;

    /**
     * 读写锁
     */
    private final ReadWriteLock lock;

    /**
     * 步长
     */
    private volatile int step;

    /**
     * 最大步长不超过100,0000
     */
    public static final int MAX_STEP = 100;

    /**
     * 最小步长不小于200
     */
    public static final int MIN_STEP = 10;

    /**
     * 更新Segment时时间戳
     */
    private volatile long updateTimestamp;

    public SegmentBuffer() {
        segments = new Segment[]{new Segment(this), new Segment(this)};
        currentPos = 0;
        nextReady = false;
        threadRunning = new AtomicBoolean(false);
        lock = new ReentrantReadWriteLock();
    }

    /**
     * 获取当前的segment
     */
    public Segment getCurrent() {
        return segments[currentPos];
    }

    /**
     * 获取下一个segment
     */
    public int nextPos() {
        return (currentPos + 1) % 2;
    }

    /**
     * 切换segment
     */
    public void switchPos() {
        currentPos = nextPos();
    }

    /**
     * segmentBuffer加读锁
     */
    public Lock rLock() {
        return lock.readLock();
    }

    /**
     * segmentBuffer加写锁
     */
    public Lock wLock() {
        return lock.writeLock();
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("SegmentBuffer{");
        sb.append("key='").append(key).append('\'');
        sb.append(", segments=").append(Arrays.toString(segments));
        sb.append(", currentPos=").append(currentPos);
        sb.append(", nextReady=").append(nextReady);
        sb.append(", threadRunning=").append(threadRunning);
        sb.append(", step=").append(step);
        sb.append(", updateTimestamp=").append(updateTimestamp);
        sb.append('}');
        return sb.toString();
    }
}

BizSourceTypeEnum

import com.google.common.collect.Lists;
import lombok.AllArgsConstructor;
import lombok.Getter;

import java.util.List;

/**
 *@Description 业务类型枚举
 **/
@Getter
@AllArgsConstructor
public enum BizSourceTypeEnum {

    BTA("bta", "业务A"),

    BTB("btb", "业务B"),

    BTC("btc", "业务C");

    private String type;

    private String desc;

    /**
     * 根据类型获取对应的中文名字
     * @param type type
     * @return
     */
    public static String getDescByType(String type) {
        for (BizSourceTypeEnum e : BizSourceTypeEnum.values()) {
            if (e.getType().equals(type)) {
                return e.getDesc();
            }
        }
        return "";
    }

    public static List<String> getTypeList() {
        List<String> typeList = Lists.newArrayList();
        for (BizSourceTypeEnum e : BizSourceTypeEnum.values()) {
            typeList.add(e.getType());
        }
        return typeList;
    }
}

序列号生成器

import com.example.serialnodemo.common.enums.BizSourceTypeEnum;
import com.example.serialnodemo.po.PsmSerialNoRecord;
import com.example.serialnodemo.common.segment.model.Segment;
import com.example.serialnodemo.common.segment.model.SegmentBuffer;
import com.example.serialnodemo.service.PsmSerialNoRecordService;
import com.google.common.base.Stopwatch;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.PostConstruct;
import java.text.NumberFormat;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;


/**
 * @description: 序列号生成器
 */
@Slf4j
@Component
public class SerialNoGenerator {

    @Autowired
    private PsmSerialNoRecordService psmSerialNoRecordService;

    /**
     * 可以自定义线程池
     */
    @Autowired
    private ThreadPoolExecutor segmentUpdateThreadPool;

    private Map<String, SegmentBuffer> cache = new ConcurrentHashMap<>();


    /**
     * 一个Segment维持时间为15分钟
     */
    private static final long SEGMENT_DURATION = 15 * 60 * 1000L;

    /**
     * 最大序列号
     */
    public static final int SERIAL_NO_LIMIT = 9999999;

    /**
     * 序列号位数
     */
    public static final int SERIAL_LENGTH = 7;


    /**
     * @description: 初始化DB序列号
     */
    @PostConstruct
    @Transactional
    public void init() {
        log.info("Init ...");
        List<String> allTypeList = BizSourceTypeEnum.getTypeList();
        List<String> existTypeList = psmSerialNoRecordService.queryForLock(allTypeList);
        BizSourceTypeEnum[] bizSourceTypeEnums = BizSourceTypeEnum.values();
        int existTypeNum = bizSourceTypeEnums.length;
        if (allTypeList.size() == existTypeNum) return;
        for (BizSourceTypeEnum typeEnum : bizSourceTypeEnums) {
            String type = typeEnum.getType();
            if (!existTypeList.contains(type)) {
                PsmSerialNoRecord record = new PsmSerialNoRecord();
                record.setBizSource(type);
                record.setMaxId(0);
                psmSerialNoRecordService.addRecord(record);
            }
        }
    }


    /**
     * @description: 获取序列号
     *
     * @param key 唯一键
     * @return long 序列号
     */
    public String getSerialNo(final String key) throws Exception {
        SegmentBuffer buffer = cache.get(key);
        if (Objects.isNull(buffer)) {
            synchronized (key) {
                buffer = cache.get(key);
                if (Objects.isNull(buffer)) {
                    buffer = new SegmentBuffer();
                    buffer.setKey(key);
                    updateSegmentFromDb(key, buffer.getCurrent());
                    cache.put(key, buffer);
                }
            }
        }
        long seq = getSerialNoFromSegmentBuffer(buffer);
        NumberFormat serialFormat = NumberFormat.getNumberInstance();
        serialFormat.setMinimumIntegerDigits(SERIAL_LENGTH);
        serialFormat.setGroupingUsed(false);
        return serialFormat.format(seq);
    }

    /**
     * @description: 从DB获取数据更新segment
     *
     * @param key 唯一键
     * @param segment segment
     */
    private void updateSegmentFromDb(String key, Segment segment) throws Exception {
        Stopwatch sw = Stopwatch.createStarted();
        SegmentBuffer buffer = segment.getBuffer();
        PsmSerialNoRecord record;
        if (buffer.getUpdateTimestamp() == 0) {
            record = psmSerialNoRecordService.modifyMaxIdAndGet(key, SegmentBuffer.MIN_STEP, SERIAL_NO_LIMIT);
            buffer.setUpdateTimestamp(System.currentTimeMillis());
            buffer.setStep(SegmentBuffer.MIN_STEP);
        } else {
            long duration = System.currentTimeMillis() - buffer.getUpdateTimestamp();
            int nextStep = buffer.getStep();
            if (duration < SEGMENT_DURATION) {
                if (nextStep << 1 > SegmentBuffer.MAX_STEP) {
                    // do nothing
                } else {
                    nextStep = nextStep << 1;
                }
            } else if (duration < SEGMENT_DURATION << 1) {
                // do nothing with nextStep
            } else {
                nextStep = nextStep >> 1 >= SegmentBuffer.MIN_STEP ? nextStep >> 1 : nextStep;
            }
            log.info("SerialNoGenerator updateSegmentFromDb key:{}, step:{}, duration:{}min, nextStep{}", key, buffer.getStep(), String.format("%.2f", ((double) duration / (1000 * 60))), nextStep);
            record = psmSerialNoRecordService.modifyMaxIdAndGet(key, nextStep, SERIAL_NO_LIMIT);
            buffer.setUpdateTimestamp(System.currentTimeMillis());
            buffer.setStep(nextStep);
        }
        long value = SERIAL_NO_LIMIT != record.getMaxId() ? record.getMaxId() - buffer.getStep() + 1 : buffer.getCurrent().getMax() + 1;
        segment.getValue().set(value);
        segment.setMax(record.getMaxId());
        segment.setStep(buffer.getStep());
        log.info("SerialNoGenerator updateSegmentFromDb 数据库更新耗时:{}", sw.stop().elapsed(TimeUnit.MILLISECONDS));
    }

    /**
     * @description: 从buffer中获取序列号
     *
     * @param buffer buffer
     * @return long
     */
    private long getSerialNoFromSegmentBuffer(final SegmentBuffer buffer) throws Exception {
        while (true) {
            buffer.rLock().lock();
            try {
                final Segment segment = buffer.getCurrent();
                // 如果是不可切换状态 && 空闲数量 < 90% && 如果线程状态是没有运行,则将状态设置为运行状态
                if (!buffer.isNextReady() && (segment.getIdle() < 0.9 * segment.getStep())
                        && buffer.getThreadRunning().compareAndSet(false, true)) {
                    segmentUpdateThreadPool.execute(() -> {
                        Segment nextSegment = buffer.getSegments()[buffer.nextPos()];
                        // 数据是否加载完毕,默认false
                        boolean updateOk = false;
                        try {
                            updateSegmentFromDb(buffer.getKey(), nextSegment);
                            updateOk = true;
                            log.info("SerialNoGenerator getIdFromSegmentBuffer key:{},更新segment:{}", buffer.getKey(), nextSegment);
                        } catch (Exception e) {
                            log.error("SerialNoGenerator getIdFromSegmentBuffer key:{},更新segment异常:{}", buffer.getKey(), e.getMessage(), e);
                        } finally {
                            if (updateOk) {
                                buffer.wLock().lock();
                                buffer.setNextReady(true);
                                buffer.getThreadRunning().set(false);
                                buffer.wLock().unlock();
                            } else {
                                buffer.getThreadRunning().set(false);
                            }
                        }
                    });
                }
                long value = segment.getValue().getAndIncrement();
                if (value <= segment.getMax()) {
                    return value;
                }
            } finally {
                buffer.rLock().unlock();
            }
            // 如果上面当前段没有拿到序列号,说明当前段序列用完了,需要切换,下一段是在线程池中运行的,所以这里等待一小会儿
            waitAndSleep(buffer);
            buffer.wLock().lock();
            try {
                // 再次拿当前段,因为有可能前面一个线程已经切换好了
                final Segment segment = buffer.getCurrent();
                long value = segment.getValue().getAndIncrement();
                if (value <= segment.getMax()) {
                    return value;
                }
                // 如果是处于可以切换状态,就切换段并设置为不可切换状态,下次获取时就可以在线程池中加载下一段
                if (buffer.isNextReady()) {
                    buffer.switchPos();
                    buffer.setNextReady(false);
                } else {
                    log.error("SerialNoGenerator getIdFromSegmentBuffer 两个Segment都未从数据库中加载 buffer:{}!", buffer);
                    throw new Exception("SerialNoGenerator getIdFromSegmentBuffer 两个Segment都未从数据库中加载");
                }
            } finally {
                buffer.wLock().unlock();
            }
        }
    }

    /**
     * @description: 等待下一段加载完成
     *
     * @param buffer buffer
     */
    private void waitAndSleep(SegmentBuffer buffer) {
        int roll = 0;
        while (buffer.getThreadRunning().get()) {
            roll += 1;
            if (roll > 100) {
                try {
                    TimeUnit.MILLISECONDS.sleep(10);
                    break;
                } catch (InterruptedException e) {
                    log.error("SerialNoGenerator waitAndSleep 线程睡眠异常,Thread:{}, 异常:{}", Thread.currentThread().getName(), e);
                    break;
                }
            }
        }
    }

}

注意

需要注意的是,这种作为工具类的方式放到项目中的方式,若项目服务器宕机或重启会丢失缓存在segment中序列号,造成序列号黑洞,如每个segment的step为10,segment1缓存的是1-10,segment2缓存的是11-20,此时DB中maxid为20,当系统使用到序列号5时,系统宕机了,重启后,因DB中maxid为20,segment1缓存的是21-30,segment2缓存的是31-40,再次获取序列号获取到的是21,其中序列号6-20丢失了,并没有用到。所以序列号最大值可以适时比估计的每天的数据量大些。

参考:

Leaf——美团点评分布式ID生成系统

Leaf:美团分布式ID生成服务开源

Leaf项目Github地址

有关基于美团Leaf-Segment的双buffer方案实现序列号生成器的更多相关文章

  1. ruby - 在 jRuby 中使用 'fork' 生成进程的替代方案? - 2

    在MRIRuby中我可以这样做:deftransferinternal_server=self.init_serverpid=forkdointernal_server.runend#Maketheserverprocessrunindependently.Process.detach(pid)internal_client=self.init_client#Dootherstuffwithconnectingtointernal_server...internal_client.post('somedata')ensure#KillserverProcess.kill('KILL',

  2. ruby-on-rails - 如何在 Rails 3 中创建自定义脚手架生成器? - 2

    有这些railscast。http://railscasts.com/episodes/218-making-generators-in-rails-3有了这个,你就会知道如何创建样式表和脚手架生成器。http://railscasts.com/episodes/216-generators-in-rails-3通过这个,您可以了解如何添加一些文件来修改脚手架View。我想把两者结合起来。我想创建一个生成器,它也可以创建脚手架View。有点像RyanBates漂亮的生成器或web_app_themegem(https://github.com/pilu/web-app-theme)。我

  3. 叮咚买菜基于 Apache Doris 统一 OLAP 引擎的应用实践 - 2

    导读:随着叮咚买菜业务的发展,不同的业务场景对数据分析提出了不同的需求,他们希望引入一款实时OLAP数据库,构建一个灵活的多维实时查询和分析的平台,统一数据的接入和查询方案,解决各业务线对数据高效实时查询和精细化运营的需求。经过调研选型,最终引入ApacheDoris作为最终的OLAP分析引擎,Doris作为核心的OLAP引擎支持复杂地分析操作、提供多维的数据视图,在叮咚买菜数十个业务场景中广泛应用。作者|叮咚买菜资深数据工程师韩青叮咚买菜创立于2017年5月,是一家专注美好食物的创业公司。叮咚买菜专注吃的事业,为满足更多人“想吃什么”而努力,通过美好食材的供应、美好滋味的开发以及美食品牌的孵

  4. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  5. kvm虚拟机安装centos7基于ubuntu20.04系统 - 2

    需求:要创建虚拟机,就需要给他提供一个虚拟的磁盘,我们就在/opt目录下创建一个10G大小的raw格式的虚拟磁盘CentOS-7-x86_64.raw命令格式:qemu-imgcreate-f磁盘格式磁盘名称磁盘大小qemu-imgcreate-f磁盘格式-o?1.创建磁盘qemu-imgcreate-fraw/opt/CentOS-7-x86_64.raw10G执行效果#ls/opt/CentOS-7-x86_64.raw2.安装虚拟机使用virt-install命令,基于我们提供的系统镜像和虚拟磁盘来创建一个虚拟机,另外在创建虚拟机之前,提前打开vnc客户端,在创建虚拟机的时候,通过vnc

  6. python - 帮我找到合适的 ruby​​/python 解析器生成器 - 2

    我使用的第一个解析器生成器是Parse::RecDescent,它的指南/教程很棒,但它最有用的功能是它的调试工具,特别是tracing功能(通过将$RD_TRACE设置为1来激活)。我正在寻找可以帮助您调试其规则的解析器生成器。问题是,它必须用python或ruby​​编写,并且具有详细模式/跟踪模式或非常有用的调试技术。有人知道这样的解析器生成器吗?编辑:当我说调试时,我并不是指调试python或ruby​​。我指的是调试解析器生成器,查看它在每一步都在做什么,查看它正在读取的每个字符,它试图匹配的规则。希望你明白这一点。赏金编辑:要赢得赏金,请展示一个解析器生成器框架,并说明它的

  7. Ruby 守护进程和 JRuby - 备选方案 - 2

    我有一个应用程序正在从Ruby迁移到JRuby(由于需要通过Java提供更好的Web服务安全支持)。我使用的gem之一是daemons创建后台作业。问题在于它使用fork+exec来创建后台进程,但这对JRuby来说是禁忌。那么-是否有用于创建后台作业的替代gem/wrapper?我目前的想法是只从shell脚本调用rake并让rake任务永远运行......提前致谢,克里斯。更新我们目前正在使用几个与Java线程相关的包装器,即https://github.com/jmettraux/rufus-scheduler和https://github.com/philostler/acts

  8. Ruby 等同于 Sphinx 文档生成器? - 2

    Ruby有一些不错的文档生成器,例如Yard、rDoc,甚至Glyph。问题是Sphinx可以做网站、PDF、epub、LaTex等。它在重组文本中完成所有这些事情。在Ruby世界中有替​​代方案吗?也许是程序的组合?如果我也能使用Markdown就更好了。 最佳答案 自1.0版以来,Sphinx有了“域”的概念,它是从Python和/或C以外的语言标记代码实体(如方法调用、对象、函数等)的方法。有一个rubydomain,所以你可以只使用Sphinx本身。您唯一会缺少的(我认为)是Sphinx使用autodoc从源代码自动创建文档

  9. ruby-on-rails - (Ruby,Rails) 基于角色的身份验证和用户管理...? - 2

    我正在寻找用于Rails的优质管理插件。似乎大多数现有的插件/gem(例如“restful_authentication”、“acts_as_authenticated”)都围绕着self注册等展开。但是,我正在寻找一种功能齐全的基于管理/管理角色的解决方案——但不是简单地附加到另一个非基于角色的解决方案。如果我找不到,我想我会自己动手......只是不想重新发明轮子。 最佳答案 RyanBates最近做了两个关于授权的railscast(注意身份验证和授权之间的区别;身份验证检查用户是否如她所说的那样,授权检查用户是否有权访问资源

  10. ruby - 在 Rakefile 中动态生成 Rake 测试任务(基于现有的测试文件) - 2

    我正在根据Rakefile中的现有测试文件动态生成测试任务。假设您有各种以模式命名的单元测试文件test_.rb.所以我正在做的是创建一个以“测试”命名空间内的文件名命名的任务。使用下面的代码,我可以用raketest:调用所有测试require'rake/testtask'task:default=>'test:all'namespace:testdodesc"Runalltests"Rake::TestTask.new(:all)do|t|t.test_files=FileList['test_*.rb']endFileList['test_*.rb'].eachdo|task|n

随机推荐