百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

你所不知道的堆外缓存

toyiye 2024-05-25 20:12 23 浏览 0 评论

在互联网项目中,一般以堆内缓存的使用居多,无论是Guava,Memcache,还是JDK自带的HashMap,ConcurrentHashMap等,都是在堆内内存中做数据计算操作。这样做的好处显而易见,用户完全不必在意数据的分配,溢出,回收等操作,全部交由JVM来进行处理。由于JVM提供了诸多的垃圾回收算法,可以保证在不影响甚至微影响系统的前提下,做到堆内内存接近完美的管控。君不见,小如图书管理这样的系统,大如整个电商交易平台,都在JVM的加持下,服务于几个,十几个,乃至于上亿用户,而在这些系统中,堆内缓存组件所带来的收益可是居功至伟。在自下而上的互联网架构中,堆内缓存就像把卫这宫廷入口的剑士,神圣而庄严,真可谓谁敢横刀立马,唯我堆内缓存将军。

堆内缓存的劣势

但是,事物都是有两面性的,堆内缓存在JVM的管理下,纵然无可挑剔,但是在GC过程中产生的程序小停顿和程序大停顿,则像一把利剑一样,斩断了对构造出完美高并发系统的念想。简单的以HashMap这个JDK自带的缓存组件为例,benchmark结果如下:

Benchmark Mode Cnt Score Error Units
localCacheBenchmark.testlocalCacheSet thrpt 20 85056.759 ± 126702.544 ops/s

其插入速度最快为85056.759+126702.544=211759.303ops,最慢为0,也就是每秒插入速度最快为20w,最慢为0。之所以为0,是因为HashMap中的数据在快速的增长过程中,引起了频繁的GC操作,为了给当前HashMap腾出足够的空间进行插入操作,不得不释放一些对象。频繁的GC,势必对插入速度有不小的影响,造成应用的偶尔性暂停。所以这也能解释为啥最慢的时候,ops为0了。 同时从benchmark数据,我们可以看到误差率为126702.544ops,比正常操作的85056.756要大很多,说明GC的影响,对HashMap的插入操作影响特别的大。

由于GC的存在,堆内缓存操作的ops会受到不小的影响,会造成原本小流量下10ms能够完成的内存计算,大流量下500ms还未完成。如果内存计算过于庞杂,则造成整体流程的ops吞吐量降低,也是极有可能的事儿。所以从这里可以看出,堆内缓存组件,在高并发的压力下,如果计算量巨大,尤其是写操作巨大,使其不会成为护城的利剑,反而成了性能的帮凶,何其可惧。

堆外缓存的优势

为了缓解在高并发,高写入操作下,堆内缓存组件造成的频繁GC问题,堆外缓存应运而生。从前面的描述我们知道,堆内缓存是受JVM管控的,所以我们不必担心垃圾回收的问题。但是堆外缓存是不受JVM管控的,所以也不受GC的影响导致的应用暂停问题。但是由于堆外缓存的使用,是以byte数组来进行的,所以需要自己进行序列化反序列化操作。目前已知的知名开源项目中,netty4的buffer pool采用了堆外缓存实现,具体的比对信息可以参考此处,具体的比对信息截图如下:

带有Direct字眼的即为offheap堆外Buffer,x轴为分配的内存大小,Y轴为耗时。从上面可以看出,小块内存分配,JVM要稍微优秀一点;但是大块内存分配,明显的堆外缓存要优秀一些。由于堆外Buffer操作不受GC影响,实际上性能更好一些。但是需要的垃圾回收管控也需要自己去做,要麻烦很多。

堆外缓存实现原理

说到堆外缓存实现原理,不可不提到sun.misc.Unsafe这个package包。此包提供了底层的Unsafe操作方法,让我们可以直接在堆外内存做数据分配操作。由于是底层包,所以用户层面很少用到,只是一些jdk里面的核心类库会用到。其实例的初始化方式如下:

public static Unsafe getUnsafe() {
 Class cc = sun.reflect.Reflection.getCallerClass(2);
 if (cc.getClassLoader() != null)
 throw new SecurityException("Unsafe");
 return theUnsafe;
}

可以看出是一个单例模式。让我们来尝试使用一下(下面代码是先分配了一个100bytes的空间,得到分配好的地址,然后在此地址里面放入1,最后将此地址里面的数据取出,打印出来):

 long address = unsafe.allocateMemory(100);
 unsafe.putLong(address,1);
 System.out.println(unsafe.getLong(address));

但是在运行的过程中,我们却遇到了如下的错误:

java.lang.SecurityException: Unsafe
 at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
 at UnsafeTest.testUnsafe(UnsafeTest.java:18)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
 at java.lang.reflect.Method.invoke(Method.java:498)
 .......
Process finished with exit code -1

可以看出,由于安全性的原因,我们是无法直接使用Unsafe的实例来进行数据操作的,主要原因是因为cc.getClassLoader()对theUnsafe实例做了过滤限制。但是我们可以直接用theUnsafe来实现,由于是private修饰,我们可以用反射来将private修饰改成public修饰,让其暴露出来供我们使用:

 Field f = Unsafe.class.getDeclaredField("theUnsafe");
 f.setAccessible(true);
 Unsafe unsafe = (Unsafe) f.get(null);
 long address = unsafe.allocateMemory(100);
 unsafe.putLong(address,1);
 System.out.println(unsafe.getLong(address));

这样就可以了,能够正确的获取运行结果。从这里我们可以看出,堆外内存必须自己分配地址空间,那么对应的,自己需要控制好地址边界,如果控制不好,经典的OOM Exception将会出现。这也是比堆内内存使用麻烦的地方。

上面的代码展示,其实已经说明了Unsafe方法的基本使用方式。如果想查看更多的Unsafe实现方式,个人推荐可以看看Cassandra源码中的中的Object mapper - Caffinitas里面关于Unsafe的实现。此类的名称为Uns.java,由于类精简,个人认为很值得一看,我贴出部分代码来:

 static
 {
 try
 {
 Field field = Unsafe.class.getDeclaredField("theUnsafe");
 field.setAccessible(true);
 unsafe = (Unsafe) field.get(null);
 if (unsafe.addressSize() > 8)
 throw new RuntimeException("Address size " + unsafe.addressSize() + " not supported yet (max 8 bytes)");
 if (__DEBUG_OFF_HEAP_MEMORY_ACCESS)
 LOGGER.warn("Degraded performance due to off-heap memory allocations and access guarded by debug code enabled via system property " + OHCacheBuilder.SYSTEM_PROPERTY_PREFIX + "debugOffHeapAccess=true");
 IAllocator alloc;
 String allocType = __ALLOCATOR != null ? __ALLOCATOR : "jna";
 switch (allocType)
 {
 case "unsafe":
 alloc = new UnsafeAllocator();
 LOGGER.info("OHC using sun.misc.Unsafe memory allocation");
 break;
 case "jna":
 default:
 alloc = new JNANativeAllocator();
 LOGGER.info("OHC using JNA OS native malloc/free");
 }
 allocator = alloc;
 }
 catch (Exception e)
 {
 throw new AssertionError(e);
 }
 }
 。。。。。。
 static long getLongFromByteArray(byte[] array, int offset)
 {
 if (offset < 0 || offset + 8 > array.length)
 throw new ArrayIndexOutOfBoundsException();
 return unsafe.getLong(array, (long) Unsafe.ARRAY_BYTE_BASE_OFFSET + offset);
 }
 static int getIntFromByteArray(byte[] array, int offset)
 {
 if (offset < 0 || offset + 4 > array.length)
 throw new ArrayIndexOutOfBoundsException();
 return unsafe.getInt(array, (long) Unsafe.ARRAY_BYTE_BASE_OFFSET + offset);
 }
 static short getShortFromByteArray(byte[] array, int offset)
 {
 if (offset < 0 || offset + 2 > array.length)
 throw new ArrayIndexOutOfBoundsException();
 return unsafe.getShort(array, (long) Unsafe.ARRAY_BYTE_BASE_OFFSET + offset);
 }

堆外缓存实现进阶

写到这里,原理什么的大概都懂了,我们准备进阶一下,写个基于Off-heap堆外缓存的Int数组,由于On-heap Array的空间请求分配到了堆上,所以这里自然而然的就把空间分配到了堆外。代码如下:

public class OffheapIntArray {
 /**
 * 此list分配的地址
 */
 private long address;
 /**
 * 默认分配空间大小
 */
 private static final int defaultSize = 1024;
 /**
 * 带参构造
 * 由于Integer类型在java中占用4个字节,所以在分配地址的时候,一个integer,需要分配 4*8 = 32 bytes的空间
 * @param size
 * @throws NoSuchFieldException
 * @throws IllegalAccessException
 */
 public OffheapIntArray(Integer size) throws NoSuchFieldException, IllegalAccessException {
 if (size == null) {
 address = alloc(defaultSize * 4 * 8);
 } else {
 address = alloc(size * 4 * 8);
 }
 }
 public int get(int index) throws NoSuchFieldException, IllegalAccessException {
 return getUnsafe().getInt(address + index * 4 * 8);
 }
 public void set(int index, int value) throws NoSuchFieldException, IllegalAccessException {
 getUnsafe().putInt(address + index * 4 * 8, value);
 }
 private Unsafe getUnsafe() throws IllegalAccessException, NoSuchFieldException {
 Field f = Unsafe.class.getDeclaredField("theUnsafe");
 f.setAccessible(true);
 return (Unsafe) f.get(null);
 }
 private long alloc(int size) throws NoSuchFieldException, IllegalAccessException {
 long address = getUnsafe().allocateMemory(size);
 return address;
 }
 public void free() throws NoSuchFieldException, IllegalAccessException {
 if (address == 0) {
 return;
 }
 getUnsafe().freeMemory(address);
 }
}

我们来简单的测试一下:

 @Test
 public void testOffheap() throws NoSuchFieldException, IllegalAccessException {
 OffheapIntArray offheapArray = new OffheapIntArray(10);
 offheapArray.set(0,11111);
 offheapArray.set(1,1112);
 offheapArray.set(2,1113);
 offheapArray.set(3,1114);
 System.out.println(offheapArray.get(0));
 System.out.println(offheapArray.get(1));
 System.out.println(offheapArray.get(2));
 System.out.println(offheapArray.get(3));
 offheapArray.free();
 }

输出结果如下:

11111
1112
1113
1114

可以看到得到了正确的输出结果。当然我这里只是简单的模拟使用。具体的使用方式,推荐如下两篇文章,可以对堆外内存的使用有更近一步的认识:

Guide to sun.misc.Unsafe

Java Magic. Part 4: sun.misc.Unsafe

堆外缓存组件实战

知道了堆外缓存的简单使用后,这里我们要更近一步,使用现有的堆外缓存组件到项目中。

目前在市面上,有诸多的缓存组件,比如mapdb,ohc,ehcache3等,但是由于ehcache3收费,所以这里不做讨论,主要讨论mapdb和ohc这两个。我们先通过benchmark来筛选一下二者的性能差异,由于这两个缓存组件提供的都是基于key-value模型的数据存储,所以benchmark的指标有9个,分别是get,set方法,hget,hset方法(value存储的是hashmap),sadd,smember方法(value存储的是set),zadd,zrange方法(value存储的是treeset)。

benchmark结果如下:

Benchmark Mode Cnt Score Error Units
OffheapCacheBenchmark.testMapdbGet thrpt 20 69699.610 ± 4578.888 ops/s
OffheapCacheBenchmark.testMapdbHGet thrpt 20 63663.523 ± 3595.413 ops/s
OffheapCacheBenchmark.testMapdbHGetAll thrpt 20 64235.582 ± 4009.039 ops/s
OffheapCacheBenchmark.testMapdbHSet thrpt 20 25777.077 ± 480.461 ops/s
OffheapCacheBenchmark.testMapdbSAdd thrpt 20 335.973 ± 39.353 ops/s
OffheapCacheBenchmark.testMapdbSet thrpt 20 39417.070 ± 830.689 ops/s
OffheapCacheBenchmark.testMapdbSmember thrpt 20 67432.314 ± 2799.983 ops/s
OffheapCacheBenchmark.testMapdbZAdd thrpt 20 21220.595 ± 1128.103 ops/s
OffheapCacheBenchmark.testMapdbZRange thrpt 20 45425.162 ± 4533.071 ops/s
Benchmark Mode Cnt Score Error Units
OhcheapOHCBenchmark.testOhcGet thrpt 20 1196976.452 ± 27291.669 ops/s
OhcheapOHCBenchmark.testOhcHGet thrpt 20 348383.355 ± 23304.696 ops/s
OhcheapOHCBenchmark.testOhcHGetAll thrpt 20 350798.417 ± 11870.685 ops/s
OhcheapOHCBenchmark.testOhcHSet thrpt 20 349370.322 ± 8619.813 ops/s
OhcheapOHCBenchmark.testOhcSAdd thrpt 20 11700.160 ± 611.794 ops/s
OhcheapOHCBenchmark.testOhcSet thrpt 20 538314.544 ± 132111.037 ops/s
OhcheapOHCBenchmark.testOhcSmember thrpt 20 458817.772 ± 15817.159 ops/s
OhcheapOHCBenchmark.testOhcZAdd thrpt 20 323979.906 ± 9842.344 ops/s
OhcheapOHCBenchmark.testOhcZRange thrpt 20 192776.479 ± 12988.484 ops/s

从上面的结果可以看出,ohc属于性能怪兽类型,性能十倍于mapdb。而且由于ohc本身支持entry过期,但是mapdb不支持。所以这里综合一下,选择ohc作为我们的堆外缓存组件。需要说明一下的是,在我进行benchmark测试过程中,堆外缓存中会进行大量的数据读写操作,但是这些读写ops整体非常平稳,从error和score的对比就可以看出。不会出现应用暂停的情况。说明GC对堆外缓存的影响是非常小的。

整体类结构图如下(考虑到扩展性,暂时将mapdb加入到了结构图中):

从整体的类组织结构图看来,使用了策略模式+模板模式组合的方式来进行。 屏蔽不同cache底层接口的不一致性,用的是策略模式;为不同的堆外缓存组件提供一致的操作方法用的是模板模式。组合起来使用就使得开发和扩展显得非常容易。

部分类的封装方式如下:

public class OhcCacheStrategy implements CacheStrategy {
 /**
 * 日志
 */
 private static Logger logger = LoggerFactory.getLogger(OhcCacheStrategy.class);
 /**
 * 缓存组件
 */
 public OHCache<byte[], byte[]> dataCache;
 /**
 * 过期时间组件
 */
 public OHCache<byte[], byte[]> expireCache;
 /**
 * 缓存table最大容量
 */
 private long level2cacheMax = 1024000L;
 /**
 * 锁
 */
 private final Object lock = new Object();
 /**
 * 键过期回调
 */
 public ExpirekeyAction expirekeyAction;
 /**
 * db引擎初始化
 */
 @PostConstruct
 public void initOhcEngine() {
 try {
 dataCache = OHCacheBuilder.<byte[], byte[]>newBuilder()
 .keySerializer(new OhcSerializer())
 .valueSerializer(new OhcSerializer())
 .segmentCount(2 * 4)
 .hashTableSize((int) level2cacheMax / 102400)
 .capacity(2 * 1024 * 1024 * 1024L)
 .defaultTTLmillis(OffheapCacheConst.EXPIRE_DEFAULT_SECONDS * 1000)
 .timeouts(true)
 .timeoutsSlots(64)
 .timeoutsPrecision(512)
 .eviction(Eviction.LRU)
 .build();
 logger.error("ohc data cache init ok...");
 expireCache = OHCacheBuilder.<byte[], byte[]>newBuilder()
 .keySerializer(new OhcSerializer())
 .valueSerializer(new OhcSerializer())
 .segmentCount(1)
 .hashTableSize((int) level2cacheMax / 102400)
 .capacity(2 * 1024 * 1024 * 1024L)
 .defaultTTLmillis(OffheapCacheConst.EXPIRE_DEFAULT_SECONDS * 1000)
 .timeouts(true)
 .timeoutsSlots(64)
 .timeoutsPrecision(512)
 .eviction(Eviction.NONE)
 .build();
 logger.error("ohc expire cache init ok...");
 } catch (Exception ex) {
 logger.error(OffheapCacheConst.PACKAGE_CONTAINER_OHC + OffheapCacheConst.ENGINE_INIT_FAIL, ex);
 AlarmUtil.alarm(OffheapCacheConst.PACKAGE_CONTAINER_OHC + OffheapCacheConst.ENGINE_INIT_FAIL, ex.getMessage());
 throw ex;
 }
 }
 @Override
 public <T> boolean putEntry(String key, T entry, long expireAt) {
 synchronized (lock) {
 byte[] entryKey = SerializationUtils.serialize(key);
 byte[] entryVal = SerializationUtils.serialize((Serializable) entry);
 //缓存数据入库
 if (dataCache.put(entryKey, entryVal, expireAt)) {
 //过期时间入库
 putExpire(key, expireAt);
 //返回执行结果
 return true;
 }
 return false;
 }
 }
 @Override
 public <T> T queryEntry(String key) {
 byte[] result = dataCache.get(SerializationUtils.serialize(key));
 if (result == null) {
 return null;
 }
 return SerializationUtils.deserialize(result);
 }
 @Override
 public long queryExpireTime(String key) {
 byte[] entryKey = SerializationUtils.serialize(key);
 return expireCache.get(entryKey) == null ? 0 : SerializationUtils.deserialize(expireCache.get(entryKey));
 }
 @Override
 public boolean removeEntry(String key) {
 byte[] entryKey = SerializationUtils.serialize(key);
 if (dataCache.remove(entryKey)) {
 removeExpire(key);
 return true;
 }
 return false;
 }
 @Override
 public boolean removeAll() {
 Iterable<byte[]> dataKey = () -> dataCache.keyIterator();
 dataCache.removeAll(dataKey);
 Iterable<byte[]> expireKey = () -> expireCache.keyIterator();
 expireCache.removeAll(expireKey);
 return true;
 }
 @Override
 public List<String> queryKeys() {
 List<String> list = new ArrayList<>();
 Iterator<byte[]> iterator = expireCache.keyIterator();
 while (iterator.hasNext()) {
 list.add(SerializationUtils.deserialize(iterator.next()));
 }
 return list;
 }
 /**
 * key过期时间同步入库
 *
 * @param key
 * @param expireAt
 */
 private void putExpire(String key, long expireAt) {
 try {
 expireCache.put(SerializationUtils.serialize(key), SerializationUtils.serialize(expireAt));
 } catch (Exception ex) {
 logger.error("key[" + key + "]过期时间入库失败...");
 }
 }
 /**
 * 同步清理过期键
 *
 * @param key
 */
 private void removeExpire(String key) {
 try {
 if (expireCache.remove(SerializationUtils.serialize(key))) {
 if (expirekeyAction != null) {
 expirekeyAction.keyExpiredNotification(key);
 }
 }
 } catch (Exception ex) {
 logger.error("key[" + key + "]过期时间清除失败...");
 }
 }
}

上面这个类是堆外缓存的核心策略类。所有其他的数据模型读写操作都可以依据此类来扩展,比如类似redis的sortedset,value可以存储一个Treeset即可。需要说明一下,上面代码中,dataCache主要用于存储数据部分,expireCache主要用于存储键过期时间。以便于可以实现键主动过期和被动过期功能。用户添加删除键的时候,会同步删除expireCache中的键,以便于二者能够统一。由于ohc本身并未实现keyExpireCallback,所以这里我实现了这个功能,只要有键被移除(主动删除还是被动删除,都会触发通知),就会通知用户,用户可以按照如下方式使用:

 @PostConstruct
 public void Init() {
 ohcCacheTemplate.registerExpireKeyAction(key -> {
 logger.error("key " + key + " expired...");
 });
 }

键被动过期功能,模仿了redis的键被动驱逐方式,实现如下:

public class OffheapCacheWorker {
 /**
 * 带参注入
 *
 * @param cacheStrategy
 */
 public OffheapCacheWorker(CacheStrategy cacheStrategy) {
 this.cacheStrategy = cacheStrategy;
 this.offheapCacheHelper = new OffheapCacheHelper();
 }
 /**
 * 日志
 */
 private static Logger logger = LoggerFactory.getLogger(OffheapCacheWorker.class);
 /**
 * 缓存帮助类
 */
 private OffheapCacheHelper offheapCacheHelper;
 /**
 * 缓存构建器
 */
 private CacheStrategy cacheStrategy;
 /**
 * 过期key检测线程
 */
 private Thread expireCheckThread;
 /**
 * 线程状态
 */
 private volatile boolean started;
 /**
 * 线程开启
 *
 * @throws IOException
 */
 public synchronized void start() {
 if (started) {
 return;
 }
 expireCheckThread = new Thread("expire key check thread") {
 @Override
 public void run() {
 logger.error("expire key check thread start...");
 while (!Thread.currentThread().isInterrupted()) {
 try {
 processLoop();
 } catch (RuntimeException suppress) {
 logger.error("Thread `" + getName() + "` occured a error, suppressed.", suppress);
 throw suppress;
 } catch (Exception exception) {
 logger.error("Thread `" + getName() + "` occured a error, exception.", exception);
 }
 }
 logger.info("Thread `{}` was stopped normally.", getName());
 }
 };
 expireCheckThread.start();
 started = true;
 }
 /**
 * 线程停止
 *
 * @throws IOException
 */
 public synchronized void stop() throws IOException {
 started = false;
 if (expireCheckThread != null) {
 expireCheckThread.interrupt();
 }
 }
 /**
 * 过期键驱逐
 * 模仿的redis键过期机制
 */
 private void processLoop() throws InterruptedException {
 //每次采集样本数
 int sampleCheckNumber = 20;
 //过期key计数
 int sampleExpiredCount = 0;
 //抽样次数迭代
 int sampleCheckIteration = 0;
 //缓存的key
 List<String> keys = cacheStrategy.queryKeys();
 //抽样开始时间
 long start = System.currentTimeMillis();
 //循环开始
 do {
 //键数量
 long expireContainerSize = keys.size();
 //默认为键数量
 long loopCheckNumber = expireContainerSize;
 //每次检查的键数量,如果超过样本数,则以样本数为准
 if (loopCheckNumber > sampleCheckNumber) {
 loopCheckNumber = sampleCheckNumber;
 }
 //开始检测
 while (loopCheckNumber-- > 0) {
 //取随机下标
 int rndNum = offheapCacheHelper.getRandomNumber(toIntExact(expireContainerSize) + 1);
 //取随机键
 String rndKey = keys.get(rndNum);
 //获取过期时间
 long expireTime = cacheStrategy.queryExpireTime(rndKey);
 //过期时间比对
 if (expireTime <= System.currentTimeMillis()) {
 //键驱逐
 boolean result = cacheStrategy.removeEntry(rndKey);
 if (result) {
 expireContainerSize--;
 sampleExpiredCount++;
 }
 }
 }
 //抽样次数递增
 sampleCheckIteration++;
 //抽样达到16次(16的倍数,&0xf都为0)且本批次耗时超过0.5秒,将退出,避免阻塞正常业务操作
 if ((sampleCheckIteration % 16) == 0 && (System.currentTimeMillis() - start) > 300) {
 logger.error("清理数据库过期键操作耗时过长,退出,预备重新开始...");
 return;
 }
 } while (sampleExpiredCount > sampleCheckNumber / 4);
 Thread.sleep(1500);
 }
}

键被动驱逐,会随机抽取20个key检测,如果过期键小于5个,则直接进行下一次抽样。否则将进行键驱逐操作。一旦抽样次数达到限定次数且键驱逐耗时过长,为了不影响业务,将会退出本次循环,继续下一次循环操作。此worker在后台运行,实测6W个过期key一起过期,cpu占用控制在10%,60w个过期key基本上一起过期,cpu占用控制在60%左右。达到预期效果。在大量的读写操作过程中,可以看到堆内内存几乎没有变化。

写到最后,上面就是这次我要介绍的堆外缓存的整体内容了,从Unsafe讲到原理,从实现讲到ohc,希望大家能够提出更好的东西来,多谢。

码字不易看到最后了,那就点个关注呗,只收藏不点关注的都是在耍流氓!

关注并私信我“架构”,免费送一套Java架构资料,先到先得!

相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码